diff --git a/CMakeLists.txt b/CMakeLists.txt index f1b9b2c..4959c90 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,12 +8,34 @@ project(sframe option(TESTING "Build tests" OFF) option(CLANG_TIDY "Perform linting with clang-tidy" OFF) option(SANITIZERS "Enable sanitizers" OFF) +option(NAMESPACE_SUFFIX "Namespace Suffix for CXX and CMake Export") + +if(NAMESPACE_SUFFIX) + set(SFRAME_CXX_NAMESPACE "sframe_${NAMESPACE_SUFFIX}" CACHE STRING "Top-level Namespace for CXX") + set(SFRAME_EXPORT_NAMESPACE "SFrame${NAMESPACE_SUFFIX}" CACHE STRING "Namespace for CMake Export") +else() + set(SFRAME_CXX_NAMESPACE "sframe" CACHE STRING "Top-level Namespace for CXX") + set(SFRAME_EXPORT_NAMESPACE "SFrame" CACHE STRING "Namespace for CMake Export") +endif() +message(STATUS "CXX Namespace: ${SFRAME_CXX_NAMESPACE}") +message(STATUS "CMake Export Namespace: ${SFRAME_EXPORT_NAMESPACE}") + +# Use -DCRYPTO=(OPENSSL_1_1 | OPENSSL_3 | BORINGSSL) to configure crypto +if(NOT DEFINED CRYPTO) + set(CRYPTO "OPENSSL_3") +endif() ### ### Global Config ### set_property(GLOBAL PROPERTY USE_FOLDERS ON) +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/namespace.h.in" + "${CMAKE_CURRENT_BINARY_DIR}/include/namespace.h" + @ONLY +) + set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) if (CMAKE_CXX_COMPILER_ID MATCHES "Clang" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU") @@ -49,7 +71,24 @@ endif() ### # External libraries -find_package(OpenSSL 1.1 REQUIRED) +if(${CRYPTO} STREQUAL "OPENSSL_1_1") + message(STATUS "Configuring with OpenSSL 1.1") + find_package(OpenSSL 1.1 EXACT REQUIRED) + add_compile_definitions(OPENSSL_1_1) + set(CRYPTO_LIB OpenSSL::Crypto) +elseif(${CRYPTO} STREQUAL "OPENSSL_3") + message(STATUS "Configuring with OpenSSL 3") + find_package(OpenSSL 3 EXACT REQUIRED) + add_compile_definitions(OPENSSL_3) + set(CRYPTO_LIB OpenSSL::Crypto) +elseif(${CRYPTO} STREQUAL "BORINGSSL") + message(STATUS "Configuring with BoringSSL") + find_package(OpenSSL REQUIRED) + add_compile_definitions(BORINGSSL) + set(CRYPTO_LIB OpenSSL::Crypto) +else() + message(FATAL_ERROR "Please select a crypto back-end (OPENSSL_1_1 or OPENSSL_3) [${CRYPTO}]") +endif() # gsl-lite fetched from GitHub include( ExternalProject ) @@ -82,14 +121,16 @@ target_include_directories( gsl INTERFACE ${GSL_LITE_INCLUDE_DIR} ) set(LIB_NAME "${PROJECT_NAME}") file(GLOB_RECURSE LIB_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h") +file(GLOB_RECURSE LIB_GENERATED_HEADERS CONFIGURE_DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/*.h") file(GLOB_RECURSE LIB_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp") -add_library(${LIB_NAME} ${LIB_HEADERS} ${LIB_SOURCES}) +add_library(${LIB_NAME} ${LIB_HEADERS} ${LIB_GENERATED_HEADERS} ${LIB_SOURCES}) add_dependencies(${LIB_NAME} gsl) target_link_libraries(${LIB_NAME} PRIVATE gsl OpenSSL::Crypto) target_include_directories(${LIB_NAME} PUBLIC $ + $ $ ) diff --git a/Makefile b/Makefile index 2d27036..7e50aba 100644 --- a/Makefile +++ b/Makefile @@ -10,6 +10,10 @@ CLANG_FORMAT=clang-format -i TEST_VECTOR_DIR=./build/test TEST_GEN=./build/cmd/test_gen/test_gen +OPENSSL_1_1_MANIFEST=alternatives/OPENSSL_1_1 +OPENSSL_3_MANIFEST=alternatives/OPENSSL_3 +BORINGSSL_MANIFEST=alternatives/BORINGSSL + .PHONY: all tidy test clean cclean format all: ${BUILD_DIR} @@ -19,7 +23,19 @@ ${BUILD_DIR}: CMakeLists.txt test/CMakeLists.txt cmake -B${BUILD_DIR} . dev: CMakeLists.txt test/CMakeLists.txt - cmake -B${BUILD_DIR} -DCLANG_TIDY=ON -DTESTING=ON -DSANITIZERS=ON . + cmake -B${BUILD_DIR} -DCMAKE_BUILD_TYPE=Debug -DTESTING=ON \ + -DCLANG_TIDY=ON -DSANITIZERS=ON \ + -DCRYPTO=OPENSSL_3 -DVCPKG_MANIFEST_DIR=${OPENSSL_3_MANIFEST} + +dev1: CMakeLists.txt test/CMakeLists.txt + cmake -B${BUILD_DIR} -DCMAKE_BUILD_TYPE=Debug -DTESTING=ON \ + -DCLANG_TIDY=ON -DSANITIZERS=ON \ + -DCRYPTO=OPENSSL_1_1 -DVCPKG_MANIFEST_DIR=${OPENSSL_1_1_MANIFEST} + +devB: CMakeLists.txt test/CMakeLists.txt + cmake -B${BUILD_DIR} -DCMAKE_BUILD_TYPE=Debug -DTESTING=ON \ + -DCLANG_TIDY=ON -DSANITIZERS=ON \ + -DCRYPTO=BORINGSSL -DVCPKG_MANIFEST_DIR=${BORINGSSL_MANIFEST} test: ${BUILD_DIR} test/* cmake --build ${BUILD_DIR} --target sframe_test diff --git a/alternatives/BORINGSSL/vcpkg.json b/alternatives/BORINGSSL/vcpkg.json new file mode 100644 index 0000000..aecee83 --- /dev/null +++ b/alternatives/BORINGSSL/vcpkg.json @@ -0,0 +1,14 @@ +{ + "name": "mlspp", + "version-string": "0.1", + "description": "Cisco MLS C++ library (BoringSSL 1.1)", + "dependencies": [ + { + "name": "boringssl", + "version>=": "2023-10-13" + }, + "doctest", + "nlohmann-json" + ], + "builtin-baseline": "eb33d2f7583405fca184bcdf7fdd5828ec88ac05" +} diff --git a/alternatives/OPENSSL_1_1/vcpkg.json b/alternatives/OPENSSL_1_1/vcpkg.json new file mode 100644 index 0000000..d5469fb --- /dev/null +++ b/alternatives/OPENSSL_1_1/vcpkg.json @@ -0,0 +1,20 @@ +{ + "name": "mlspp", + "version-string": "0.1", + "description": "Cisco MLS C++ library", + "dependencies": [ + { + "name": "openssl", + "version>=": "1.1.1n" + }, + "doctest", + "nlohmann-json" + ], + "builtin-baseline": "eb33d2f7583405fca184bcdf7fdd5828ec88ac05", + "overrides": [ + { + "name": "openssl", + "version-string": "1.1.1n" + } + ] +} diff --git a/alternatives/OPENSSL_3/vcpkg.json b/alternatives/OPENSSL_3/vcpkg.json new file mode 100644 index 0000000..e3d658b --- /dev/null +++ b/alternatives/OPENSSL_3/vcpkg.json @@ -0,0 +1,20 @@ +{ + "name": "mlspp", + "version-string": "0.1", + "description": "Cisco MLS C++ library (OpenSSL 3)", + "dependencies": [ + { + "name": "openssl", + "version>=": "3.0.7" + }, + "doctest", + "nlohmann-json" + ], + "builtin-baseline": "eb33d2f7583405fca184bcdf7fdd5828ec88ac05", + "overrides": [ + { + "name": "openssl", + "version": "3.0.7" + } + ] +} diff --git a/cmake/namespace.h.in b/cmake/namespace.h.in new file mode 100644 index 0000000..d4a9f3c --- /dev/null +++ b/cmake/namespace.h.in @@ -0,0 +1,4 @@ +#pragma once + +// Configurable top-level namespace +#define SFRAME_NAMESPACE @SFRAME_CXX_NAMESPACE@ \ No newline at end of file diff --git a/include/sframe/sframe.h b/include/sframe/sframe.h index d570076..98dd8ad 100644 --- a/include/sframe/sframe.h +++ b/include/sframe/sframe.h @@ -5,9 +5,10 @@ #include #include +#include #include -namespace sframe { +namespace SFRAME_NAMESPACE { struct openssl_error : std::runtime_error { @@ -147,4 +148,4 @@ class MLSContext : public SFrame KeyState& get_state(KeyID key_id) override; }; -} // namespace sframe +} // namespace SFRAME_NAMESPACE diff --git a/src/crypto.cpp b/src/crypto.cpp index 226323f..ee950c9 100644 --- a/src/crypto.cpp +++ b/src/crypto.cpp @@ -1,85 +1,47 @@ #include "crypto.h" -#include -#include - -namespace sframe { +namespace SFRAME_NAMESPACE { /// -/// Convert between native identifiers / errors and OpenSSL ones +/// Information about algorithms /// -openssl_error::openssl_error() - : std::runtime_error(ERR_error_string(ERR_get_error(), nullptr)) -{} - -static const EVP_MD* -openssl_digest_type(CipherSuite suite) +size_t +cipher_digest_size(CipherSuite suite) { switch (suite) { case CipherSuite::AES_CM_128_HMAC_SHA256_4: case CipherSuite::AES_CM_128_HMAC_SHA256_8: case CipherSuite::AES_GCM_128_SHA256: - return EVP_sha256(); + return 32; case CipherSuite::AES_GCM_256_SHA512: - return EVP_sha512(); + return 64; default: throw unsupported_ciphersuite_error(); } } -static const EVP_CIPHER* -openssl_cipher(CipherSuite suite) +size_t +cipher_key_size(CipherSuite suite) { switch (suite) { case CipherSuite::AES_CM_128_HMAC_SHA256_4: case CipherSuite::AES_CM_128_HMAC_SHA256_8: - return EVP_aes_128_ctr(); - case CipherSuite::AES_GCM_128_SHA256: - return EVP_aes_128_gcm(); - - case CipherSuite::AES_GCM_256_SHA512: - return EVP_aes_256_gcm(); - - default: - throw unsupported_ciphersuite_error(); - } -} - -static size_t -openssl_tag_size(CipherSuite suite) -{ - switch (suite) { - case CipherSuite::AES_CM_128_HMAC_SHA256_4: - return 4; - - case CipherSuite::AES_CM_128_HMAC_SHA256_8: - return 8; + return 16; - case CipherSuite::AES_GCM_128_SHA256: case CipherSuite::AES_GCM_256_SHA512: - return 16; + return 32; default: throw unsupported_ciphersuite_error(); } } -/// -/// Information about algorithms -/// - -size_t -cipher_digest_size(CipherSuite suite) -{ - return EVP_MD_size(openssl_digest_type(suite)); -} - size_t -cipher_key_size(CipherSuite suite) +cipher_enc_key_size(CipherSuite suite) { switch (suite) { case CipherSuite::AES_CM_128_HMAC_SHA256_4: @@ -110,382 +72,23 @@ cipher_nonce_size(CipherSuite suite) } } -/// -/// HMAC and HKDF -/// - -HMAC::HMAC(CipherSuite suite, input_bytes key) - : ctx(HMAC_CTX_new(), HMAC_CTX_free) -{ - auto type = openssl_digest_type(suite); - auto key_size = static_cast(key.size()); - if (1 != HMAC_Init_ex(ctx.get(), key.data(), key_size, type, nullptr)) { - throw openssl_error(); - } -} - -void -HMAC::write(input_bytes data) -{ - if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) { - throw openssl_error(); - } -} - -input_bytes -HMAC::digest() -{ - unsigned int size = 0; - if (1 != HMAC_Final(ctx.get(), md.data(), &size)) { - throw openssl_error(); - } - - return input_bytes(md.data(), size); -} - -static bytes -hmac_for_hkdf(CipherSuite suite, input_bytes key, input_bytes data) -{ - const auto type = openssl_digest_type(suite); - auto ctx = scoped_hmac_ctx(HMAC_CTX_new(), HMAC_CTX_free); - - // Some FIPS-enabled libraries are overly conservative in their interpretation - // of NIST SP 800-131A, which requires HMAC keys to be at least 112 bits long. - // That document does not impose that requirement on HKDF, so we disable FIPS - // enforcement for purposes of HKDF. - // - // https://doi.org/10.6028/NIST.SP.800-131Ar2 - static const auto fips_min_hmac_key_len = 14; - auto key_size = static_cast(key.size()); - if (FIPS_mode() != 0 && key_size < fips_min_hmac_key_len) { - HMAC_CTX_set_flags(ctx.get(), EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); - } - - // Guard against sending nullptr to HMAC_Init_ex - auto* key_data = key.data(); - static const auto dummy_key = bytes{ 0 }; - if (key_data == nullptr) { - key_data = dummy_key.data(); - } - - if (1 != HMAC_Init_ex(ctx.get(), key.data(), key_size, type, nullptr)) { - throw openssl_error(); - } - - if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) { - throw openssl_error(); - } - - auto md = bytes(cipher_digest_size(suite)); - unsigned int size = 0; - if (1 != HMAC_Final(ctx.get(), md.data(), &size)) { - throw openssl_error(); - } - - return md; -} - -bytes -hkdf_extract(CipherSuite suite, const bytes& salt, const bytes& ikm) -{ - return hmac_for_hkdf(suite, salt, ikm); -} - -// For simplicity, we enforce that size <= Hash.length, so that -// HKDF-Expand(Secret, Label) reduces to: -// -// HMAC(Secret, Label || 0x01) -bytes -hkdf_expand(CipherSuite suite, - const bytes& secret, - const bytes& info, - size_t size) -{ - // Ensure that we need only one hash invocation - if (size > cipher_digest_size(suite)) { - throw invalid_parameter_error("Size too big for hkdf_expand"); - } - - auto label = info; - label.push_back(0x01); - auto mac = hmac_for_hkdf(suite, secret, label); - mac.resize(size); - return mac; -} - -/// -/// AEAD Algorithms -/// - -static void -ctr_crypt(CipherSuite suite, - input_bytes key, - input_bytes nonce, - output_bytes out, - input_bytes in) -{ - if (out.size() != in.size()) { - throw buffer_too_small_error("CTR size mismatch"); - } - - auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw openssl_error(); - } - - static auto padded_nonce = - std::array{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; - std::copy(nonce.begin(), nonce.end(), padded_nonce.begin()); - - auto cipher = openssl_cipher(suite); - if (1 != - EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { - throw openssl_error(); - } - - int outlen = 0; - auto in_size_int = static_cast(in.size()); - if (1 != EVP_EncryptUpdate( - ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { - throw openssl_error(); - } - - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { - throw openssl_error(); - } -} - -static output_bytes -seal_ctr(CipherSuite suite, - const bytes& key, - const bytes& nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - auto tag_size = openssl_tag_size(suite); - if (ct.size() < pt.size() + tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - // Split the key into enc and auth subkeys - auto key_span = input_bytes(key); - auto enc_key_size = cipher_key_size(suite); - auto enc_key = key_span.subspan(0, enc_key_size); - auto auth_key = key_span.subspan(enc_key_size); - - // Encrypt with AES-CM - auto inner_ct = ct.subspan(0, pt.size()); - ctr_crypt(suite, enc_key, nonce, inner_ct, pt); - - // Authenticate with truncated HMAC - auto hmac = HMAC(suite, auth_key); - hmac.write(aad); - hmac.write(inner_ct); - auto mac = hmac.digest(); - auto tag = ct.subspan(pt.size(), tag_size); - std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); - - return ct.subspan(0, pt.size() + tag_size); -} - -static output_bytes -seal_aead(CipherSuite suite, - const bytes& key, - const bytes& nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) -{ - auto tag_size = openssl_tag_size(suite); - if (ct.size() < pt.size() + tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw openssl_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { - throw openssl_error(); - } - - int outlen = 0; - auto aad_size_int = static_cast(aad.size()); - if (aad.size() > 0) { - if (1 != EVP_EncryptUpdate( - ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { - throw openssl_error(); - } - } - - auto pt_size_int = static_cast(pt.size()); - if (1 != EVP_EncryptUpdate( - ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { - throw openssl_error(); - } - - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only computes the tag - if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { - throw openssl_error(); - } - - auto tag = ct.subspan(pt.size(), tag_size); - auto tag_ptr = const_cast(static_cast(tag.data())); - auto tag_size_downcast = static_cast(tag.size()); - if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { - throw openssl_error(); - } - - return ct.subspan(0, pt.size() + tag_size); -} - -output_bytes -seal(CipherSuite suite, - const bytes& key, - const bytes& nonce, - output_bytes ct, - input_bytes aad, - input_bytes pt) +size_t +cipher_overhead(CipherSuite suite) { switch (suite) { case CipherSuite::AES_CM_128_HMAC_SHA256_4: - case CipherSuite::AES_CM_128_HMAC_SHA256_8: { - return seal_ctr(suite, key, nonce, ct, aad, pt); - } - - case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return seal_aead(suite, key, nonce, ct, aad, pt); - } - } - - throw unsupported_ciphersuite_error(); -} - -static output_bytes -open_ctr(CipherSuite suite, - const bytes& key, - const bytes& nonce, - output_bytes pt, - input_bytes aad, - input_bytes ct) -{ - auto tag_size = openssl_tag_size(suite); - if (ct.size() < tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - auto inner_ct_size = ct.size() - tag_size; - auto inner_ct = ct.subspan(0, inner_ct_size); - auto tag = ct.subspan(inner_ct_size, tag_size); - - // Split the key into enc and auth subkeys - auto key_span = input_bytes(key); - auto enc_key_size = cipher_key_size(suite); - auto enc_key = key_span.subspan(0, enc_key_size); - auto auth_key = key_span.subspan(enc_key_size); - - // Authenticate with truncated HMAC - auto hmac = HMAC(suite, auth_key); - hmac.write(aad); - hmac.write(inner_ct); - auto mac = hmac.digest(); - if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { - throw authentication_error(); - } - - // Decrypt with AES-CM - ctr_crypt(suite, enc_key, nonce, pt, ct.subspan(0, inner_ct_size)); - - return pt.subspan(0, inner_ct_size); -} - -static output_bytes -open_aead(CipherSuite suite, - const bytes& key, - const bytes& nonce, - output_bytes pt, - input_bytes aad, - input_bytes ct) -{ - auto tag_size = openssl_tag_size(suite); - if (ct.size() < tag_size) { - throw buffer_too_small_error("Ciphertext buffer too small"); - } - - auto inner_ct_size = ct.size() - tag_size; - if (pt.size() < inner_ct_size) { - throw buffer_too_small_error("Plaintext buffer too small"); - } - - auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); - if (ctx.get() == nullptr) { - throw openssl_error(); - } - - auto cipher = openssl_cipher(suite); - if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { - throw openssl_error(); - } - - auto tag = ct.subspan(inner_ct_size, tag_size); - auto tag_ptr = const_cast(static_cast(tag.data())); - auto tag_size_downcast = static_cast(tag.size()); - if (1 != EVP_CIPHER_CTX_ctrl( - ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { - throw openssl_error(); - } - - int out_size; - auto aad_size_int = static_cast(aad.size()); - if (aad.size() > 0) { - if (1 != EVP_DecryptUpdate( - ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { - throw openssl_error(); - } - } - - auto inner_ct_size_int = static_cast(inner_ct_size); - if (1 != EVP_DecryptUpdate( - ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { - throw openssl_error(); - } - - // Providing nullptr as an argument is safe here because this - // function never writes with GCM; it only verifies the tag - if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { - throw authentication_error(); - } - - return pt.subspan(0, inner_ct_size); -} + return 4; -output_bytes -open(CipherSuite suite, - const bytes& key, - const bytes& nonce, - output_bytes pt, - input_bytes aad, - input_bytes ct) -{ - switch (suite) { - case CipherSuite::AES_CM_128_HMAC_SHA256_4: - case CipherSuite::AES_CM_128_HMAC_SHA256_8: { - return open_ctr(suite, key, nonce, pt, aad, ct); - } + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + return 8; case CipherSuite::AES_GCM_128_SHA256: - case CipherSuite::AES_GCM_256_SHA512: { - return open_aead(suite, key, nonce, pt, aad, ct); - } - } + case CipherSuite::AES_GCM_256_SHA512: + return 16; - throw unsupported_ciphersuite_error(); + default: + throw unsupported_ciphersuite_error(); + } } -} // namespace sframe +} // namespace SFRAME_NAMESPACE diff --git a/src/crypto.h b/src/crypto.h index 385b00c..f31c0be 100644 --- a/src/crypto.h +++ b/src/crypto.h @@ -1,19 +1,10 @@ #pragma once -#include #include #include -namespace sframe { - -/// -/// Scoped pointers for OpenSSL objects -/// - -using scoped_evp_ctx = - std::unique_ptr; -using scoped_hmac_ctx = std::unique_ptr; +namespace SFRAME_NAMESPACE { /// /// Information about algorithms @@ -24,22 +15,16 @@ cipher_digest_size(CipherSuite suite); size_t cipher_key_size(CipherSuite suite); size_t +cipher_enc_key_size(CipherSuite suite); +size_t cipher_nonce_size(CipherSuite suite); +size_t +cipher_overhead(CipherSuite suite); /// /// HMAC and HKDF /// -struct HMAC -{ - HMAC(CipherSuite suite, input_bytes key); - void write(input_bytes data); - input_bytes digest(); - - scoped_hmac_ctx ctx; - std::array md; -}; - bytes hkdf_extract(CipherSuite suite, const bytes& salt, const bytes& ikm); @@ -69,4 +54,4 @@ open(CipherSuite suite, input_bytes aad, input_bytes ct); -} // namespace sframe +} // namespace SFRAME_NAMESPACE diff --git a/src/crypto_boringssl.cpp b/src/crypto_boringssl.cpp new file mode 100644 index 0000000..60b44cd --- /dev/null +++ b/src/crypto_boringssl.cpp @@ -0,0 +1,414 @@ +#if defined(BORINGSSL) + +#include "crypto.h" +#include "header.h" + +#include +#include +#include +#include +#include + +namespace SFRAME_NAMESPACE { + +/// +/// Convert between native identifiers / errors and OpenSSL ones +/// + +openssl_error::openssl_error() + : std::runtime_error(ERR_error_string(ERR_get_error(), nullptr)) +{ +} + +static const EVP_MD* +openssl_digest_type(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + case CipherSuite::AES_GCM_128_SHA256: + return EVP_sha256(); + + case CipherSuite::AES_GCM_256_SHA512: + return EVP_sha512(); + + default: + throw unsupported_ciphersuite_error(); + } +} + +static const EVP_CIPHER* +openssl_cipher(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + return EVP_aes_128_ctr(); + + case CipherSuite::AES_GCM_128_SHA256: + return EVP_aes_128_gcm(); + + case CipherSuite::AES_GCM_256_SHA512: + return EVP_aes_256_gcm(); + + default: + throw unsupported_ciphersuite_error(); + } +} + +/// +/// HKDF +/// + +bytes +hkdf_extract(CipherSuite suite, const bytes& salt, const bytes& ikm) +{ + const auto* md = openssl_digest_type(suite); + auto out = bytes(EVP_MD_size(md)); + auto out_len = size_t(out.size()); + if (1 != HKDF_extract(out.data(), + &out_len, + md, + ikm.data(), + ikm.size(), + salt.data(), + salt.size())) { + throw openssl_error(); + } + + return out; +} + +bytes +hkdf_expand(CipherSuite suite, const bytes& prk, const bytes& info, size_t size) +{ + const auto* md = openssl_digest_type(suite); + auto out = bytes(size); + if (1 != HKDF_expand(out.data(), + out.size(), + md, + prk.data(), + prk.size(), + info.data(), + info.size())) { + throw openssl_error(); + } + + return out; +} + +/// +/// AEAD Algorithms +/// + +static bytes +compute_tag(CipherSuite suite, + input_bytes auth_key, + input_bytes aad, + input_bytes ct, + size_t tag_size) +{ + using scoped_hmac_ctx = std::unique_ptr; + + auto ctx = scoped_hmac_ctx(HMAC_CTX_new(), HMAC_CTX_free); + const auto md = openssl_digest_type(suite); + + // Guard against sending nullptr to HMAC_Init_ex + const auto* key_data = auth_key.data(); + auto key_size = static_cast(auth_key.size()); + const auto non_null_zero_length_key = uint8_t(0); + if (key_data == nullptr) { + key_data = &non_null_zero_length_key; + } + + if (1 != HMAC_Init_ex(ctx.get(), key_data, key_size, md, nullptr)) { + throw openssl_error(); + } + + if (1 != HMAC_Update(ctx.get(), aad.data(), aad.size())) { + throw openssl_error(); + } + + if (1 != HMAC_Update(ctx.get(), ct.data(), ct.size())) { + throw openssl_error(); + } + + auto size = static_cast(EVP_MD_size(md)); + auto tag = bytes(size); + if (1 != HMAC_Final(ctx.get(), tag.data(), &size)) { + throw openssl_error(); + } + + tag.resize(tag_size); + return tag; +} + +using scoped_evp_cipher_ctx = + std::unique_ptr; + +static void +ctr_crypt(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes out, + input_bytes in) +{ + if (out.size() != in.size()) { + throw buffer_too_small_error("CTR size mismatch"); + } + + auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto padded_nonce = bytes(nonce.begin(), nonce.end()); + padded_nonce.resize(16); + + auto cipher = openssl_cipher(suite); + if (1 != + EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + throw openssl_error(); + } + + int outlen = 0; + auto in_size_int = static_cast(in.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { + throw openssl_error(); + } + + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw openssl_error(); + } +} + +static output_bytes +seal_ctr(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + // Split the key into enc and auth subkeys + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Encrypt with AES-CM + auto inner_ct = ct.subspan(0, pt.size()); + ctr_crypt(suite, enc_key, nonce, inner_ct, pt); + + // Authenticate with truncated HMAC + auto mac = compute_tag(suite, auth_key, aad, inner_ct, tag_size); + auto tag = ct.subspan(pt.size(), tag_size); + std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); + + return ct.subspan(0, pt.size() + tag_size); +} + +static output_bytes +seal_aead(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw openssl_error(); + } + + int outlen = 0; + auto aad_size_int = static_cast(aad.size()); + if (aad.size() > 0) { + if (1 != EVP_EncryptUpdate( + ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { + throw openssl_error(); + } + } + + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + throw openssl_error(); + } + + // Providing nullptr as an argument is safe here because this + // function never writes with GCM; it only computes the tag + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw openssl_error(); + } + + auto tag = ct.subspan(pt.size(), tag_size); + auto tag_ptr = const_cast(static_cast(tag.data())); + auto tag_size_downcast = static_cast(tag.size()); + if (1 != EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { + throw openssl_error(); + } + + return ct.subspan(0, pt.size() + tag_size); +} + +output_bytes +seal(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: { + return seal_ctr(suite, key, nonce, ct, aad, pt); + } + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: { + return seal_aead(suite, key, nonce, ct, aad, pt); + } + } + + throw unsupported_ciphersuite_error(); +} + +static output_bytes +open_ctr(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + auto inner_ct = ct.subspan(0, inner_ct_size); + auto tag = ct.subspan(inner_ct_size, tag_size); + + // Split the key into enc and auth subkeys + auto enc_key_size = cipher_enc_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Authenticate with truncated HMAC + auto mac = compute_tag(suite, auth_key, aad, inner_ct, tag_size); + if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { + throw authentication_error(); + } + + // Decrypt with AES-CTR + const auto pt_out = pt.first(inner_ct_size); + ctr_crypt(suite, enc_key, nonce, pt_out, ct.first(inner_ct_size)); + + return pt_out; +} + +static output_bytes +open_aead(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + if (pt.size() < inner_ct_size) { + throw buffer_too_small_error("Plaintext buffer too small"); + } + + auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw openssl_error(); + } + + auto tag = ct.subspan(inner_ct_size, tag_size); + auto tag_ptr = const_cast(static_cast(tag.data())); + auto tag_size_downcast = static_cast(tag.size()); + if (1 != EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { + throw openssl_error(); + } + + int out_size; + auto aad_size_int = static_cast(aad.size()); + if (aad.size() > 0) { + if (1 != EVP_DecryptUpdate( + ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { + throw openssl_error(); + } + } + + auto inner_ct_size_int = static_cast(inner_ct_size); + if (1 != EVP_DecryptUpdate( + ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { + throw openssl_error(); + } + + // Providing nullptr as an argument is safe here because this + // function never writes with GCM; it only verifies the tag + if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { + throw authentication_error(); + } + + return pt.subspan(0, inner_ct_size); +} + +output_bytes +open(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: { + return open_ctr(suite, key, nonce, pt, aad, ct); + } + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: { + return open_aead(suite, key, nonce, pt, aad, ct); + } + } + + throw unsupported_ciphersuite_error(); +} + +} // namespace SFRAME_NAMESPACE + +#endif // defined(OPENSSL_3) diff --git a/src/crypto_openssl11.cpp b/src/crypto_openssl11.cpp new file mode 100644 index 0000000..6b0dd14 --- /dev/null +++ b/src/crypto_openssl11.cpp @@ -0,0 +1,473 @@ +#if defined(OPENSSL_1_1) + +#include "crypto.h" + +#include +#include +#include + +namespace SFRAME_NAMESPACE { + +/// +/// Scoped pointers for OpenSSL objects +/// + +using scoped_evp_ctx = + std::unique_ptr; +using scoped_hmac_ctx = std::unique_ptr; + +/// +/// Convert between native identifiers / errors and OpenSSL ones +/// + +openssl_error::openssl_error() + : std::runtime_error(ERR_error_string(ERR_get_error(), nullptr)) +{} + +static const EVP_MD* +openssl_digest_type(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + case CipherSuite::AES_GCM_128_SHA256: + return EVP_sha256(); + + case CipherSuite::AES_GCM_256_SHA512: + return EVP_sha512(); + + default: + throw unsupported_ciphersuite_error(); + } +} + +static const EVP_CIPHER* +openssl_cipher(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + return EVP_aes_128_ctr(); + + case CipherSuite::AES_GCM_128_SHA256: + return EVP_aes_128_gcm(); + + case CipherSuite::AES_GCM_256_SHA512: + return EVP_aes_256_gcm(); + + default: + throw unsupported_ciphersuite_error(); + } +} + +static size_t +openssl_tag_size(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + return 4; + + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + return 8; + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: + return 16; + + default: + throw unsupported_ciphersuite_error(); + } +} + +/// +/// HMAC and HKDF +/// + +struct HMAC +{ + HMAC(CipherSuite suite, input_bytes key); + void write(input_bytes data); + input_bytes digest(); + + scoped_hmac_ctx ctx; + std::array md; +}; + + +HMAC::HMAC(CipherSuite suite, input_bytes key) + : ctx(HMAC_CTX_new(), HMAC_CTX_free) +{ + auto type = openssl_digest_type(suite); + auto key_size = static_cast(key.size()); + if (1 != HMAC_Init_ex(ctx.get(), key.data(), key_size, type, nullptr)) { + throw openssl_error(); + } +} + +void +HMAC::write(input_bytes data) +{ + if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) { + throw openssl_error(); + } +} + +input_bytes +HMAC::digest() +{ + unsigned int size = 0; + if (1 != HMAC_Final(ctx.get(), md.data(), &size)) { + throw openssl_error(); + } + + return input_bytes(md.data(), size); +} + +static bytes +hmac_for_hkdf(CipherSuite suite, input_bytes key, input_bytes data) +{ + const auto type = openssl_digest_type(suite); + auto ctx = scoped_hmac_ctx(HMAC_CTX_new(), HMAC_CTX_free); + + // Some FIPS-enabled libraries are overly conservative in their interpretation + // of NIST SP 800-131A, which requires HMAC keys to be at least 112 bits long. + // That document does not impose that requirement on HKDF, so we disable FIPS + // enforcement for purposes of HKDF. + // + // https://doi.org/10.6028/NIST.SP.800-131Ar2 + static const auto fips_min_hmac_key_len = 14; + auto key_size = static_cast(key.size()); + if (FIPS_mode() != 0 && key_size < fips_min_hmac_key_len) { + HMAC_CTX_set_flags(ctx.get(), EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); + } + + // Guard against sending nullptr to HMAC_Init_ex + auto* key_data = key.data(); + static const auto dummy_key = bytes{ 0 }; + if (key_data == nullptr) { + key_data = dummy_key.data(); + } + + if (1 != HMAC_Init_ex(ctx.get(), key.data(), key_size, type, nullptr)) { + throw openssl_error(); + } + + if (1 != HMAC_Update(ctx.get(), data.data(), data.size())) { + throw openssl_error(); + } + + auto md = bytes(cipher_digest_size(suite)); + unsigned int size = 0; + if (1 != HMAC_Final(ctx.get(), md.data(), &size)) { + throw openssl_error(); + } + + return md; +} + +bytes +hkdf_extract(CipherSuite suite, const bytes& salt, const bytes& ikm) +{ + return hmac_for_hkdf(suite, salt, ikm); +} + +// For simplicity, we enforce that size <= Hash.length, so that +// HKDF-Expand(Secret, Label) reduces to: +// +// HMAC(Secret, Label || 0x01) +bytes +hkdf_expand(CipherSuite suite, + const bytes& secret, + const bytes& info, + size_t size) +{ + // Ensure that we need only one hash invocation + if (size > cipher_digest_size(suite)) { + throw invalid_parameter_error("Size too big for hkdf_expand"); + } + + auto label = info; + label.push_back(0x01); + auto mac = hmac_for_hkdf(suite, secret, label); + mac.resize(size); + return mac; +} + +/// +/// AEAD Algorithms +/// + +static void +ctr_crypt(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes out, + input_bytes in) +{ + if (out.size() != in.size()) { + throw buffer_too_small_error("CTR size mismatch"); + } + + auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + static auto padded_nonce = + std::array{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + std::copy(nonce.begin(), nonce.end(), padded_nonce.begin()); + + auto cipher = openssl_cipher(suite); + if (1 != + EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + throw openssl_error(); + } + + int outlen = 0; + auto in_size_int = static_cast(in.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { + throw openssl_error(); + } + + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw openssl_error(); + } +} + +static output_bytes +seal_ctr(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto tag_size = openssl_tag_size(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + // Split the key into enc and auth subkeys + auto key_span = input_bytes(key); + auto enc_key_size = cipher_key_size(suite); + auto enc_key = key_span.subspan(0, enc_key_size); + auto auth_key = key_span.subspan(enc_key_size); + + // Encrypt with AES-CM + auto inner_ct = ct.subspan(0, pt.size()); + ctr_crypt(suite, enc_key, nonce, inner_ct, pt); + + // Authenticate with truncated HMAC + auto hmac = HMAC(suite, auth_key); + hmac.write(aad); + hmac.write(inner_ct); + auto mac = hmac.digest(); + auto tag = ct.subspan(pt.size(), tag_size); + std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); + + return ct.subspan(0, pt.size() + tag_size); +} + +static output_bytes +seal_aead(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto tag_size = openssl_tag_size(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw openssl_error(); + } + + int outlen = 0; + auto aad_size_int = static_cast(aad.size()); + if (aad.size() > 0) { + if (1 != EVP_EncryptUpdate( + ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { + throw openssl_error(); + } + } + + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + throw openssl_error(); + } + + // Providing nullptr as an argument is safe here because this + // function never writes with GCM; it only computes the tag + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw openssl_error(); + } + + auto tag = ct.subspan(pt.size(), tag_size); + auto tag_ptr = const_cast(static_cast(tag.data())); + auto tag_size_downcast = static_cast(tag.size()); + if (1 != EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { + throw openssl_error(); + } + + return ct.subspan(0, pt.size() + tag_size); +} + +output_bytes +seal(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: { + return seal_ctr(suite, key, nonce, ct, aad, pt); + } + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: { + return seal_aead(suite, key, nonce, ct, aad, pt); + } + } + + throw unsupported_ciphersuite_error(); +} + +static output_bytes +open_ctr(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + auto tag_size = openssl_tag_size(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + auto inner_ct = ct.subspan(0, inner_ct_size); + auto tag = ct.subspan(inner_ct_size, tag_size); + + // Split the key into enc and auth subkeys + auto key_span = input_bytes(key); + auto enc_key_size = cipher_key_size(suite); + auto enc_key = key_span.subspan(0, enc_key_size); + auto auth_key = key_span.subspan(enc_key_size); + + // Authenticate with truncated HMAC + auto hmac = HMAC(suite, auth_key); + hmac.write(aad); + hmac.write(inner_ct); + auto mac = hmac.digest(); + if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { + throw authentication_error(); + } + + // Decrypt with AES-CM + ctr_crypt(suite, enc_key, nonce, pt, ct.subspan(0, inner_ct_size)); + + return pt.subspan(0, inner_ct_size); +} + +static output_bytes +open_aead(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + auto tag_size = openssl_tag_size(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + if (pt.size() < inner_ct_size) { + throw buffer_too_small_error("Plaintext buffer too small"); + } + + auto ctx = scoped_evp_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw openssl_error(); + } + + auto tag = ct.subspan(inner_ct_size, tag_size); + auto tag_ptr = const_cast(static_cast(tag.data())); + auto tag_size_downcast = static_cast(tag.size()); + if (1 != EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { + throw openssl_error(); + } + + int out_size; + auto aad_size_int = static_cast(aad.size()); + if (aad.size() > 0) { + if (1 != EVP_DecryptUpdate( + ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { + throw openssl_error(); + } + } + + auto inner_ct_size_int = static_cast(inner_ct_size); + if (1 != EVP_DecryptUpdate( + ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { + throw openssl_error(); + } + + // Providing nullptr as an argument is safe here because this + // function never writes with GCM; it only verifies the tag + if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { + throw authentication_error(); + } + + return pt.subspan(0, inner_ct_size); +} + +output_bytes +open(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_CM_128_HMAC_SHA256_8: { + return open_ctr(suite, key, nonce, pt, aad, ct); + } + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: { + return open_aead(suite, key, nonce, pt, aad, ct); + } + } + + throw unsupported_ciphersuite_error(); +} + +} // namespace SFRAME_NAMESPACE + +#endif // defined(OPENSSL_1_1) diff --git a/src/crypto_openssl3.cpp b/src/crypto_openssl3.cpp new file mode 100644 index 0000000..26de861 --- /dev/null +++ b/src/crypto_openssl3.cpp @@ -0,0 +1,453 @@ +#if defined(OPENSSL_3) + +#include "crypto.h" +#include "header.h" + +#include +#include +#include +#include +#include + +namespace SFRAME_NAMESPACE { + +/// +/// Convert between native identifiers / errors and OpenSSL ones +/// + +openssl_error::openssl_error() + : std::runtime_error(ERR_error_string(ERR_get_error(), nullptr)) +{ +} + +static const EVP_CIPHER* +openssl_cipher(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + return EVP_aes_128_ctr(); + + case CipherSuite::AES_GCM_128_SHA256: + return EVP_aes_128_gcm(); + + case CipherSuite::AES_GCM_256_SHA512: + return EVP_aes_256_gcm(); + + default: + throw unsupported_ciphersuite_error(); + } +} + +static const char* +openssl_digest_name(CipherSuite suite) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + case CipherSuite::AES_CM_128_HMAC_SHA256_4: + case CipherSuite::AES_GCM_128_SHA256: + return OSSL_DIGEST_NAME_SHA2_256; + + case CipherSuite::AES_GCM_256_SHA512: + return OSSL_DIGEST_NAME_SHA2_512; + + default: + throw unsupported_ciphersuite_error(); + } +} + +/// +/// HKDF +/// + +using scoped_evp_kdf = std::unique_ptr; +using scoped_evp_kdf_ctx = + std::unique_ptr; + +bytes +hkdf_extract(CipherSuite suite, const bytes& salt, const bytes& ikm) +{ + auto mode = EVP_KDF_HKDF_MODE_EXTRACT_ONLY; + auto digest_name = const_cast(openssl_digest_name(suite)); + auto* salt_ptr = + const_cast(reinterpret_cast(salt.data())); + auto* ikm_ptr = const_cast(reinterpret_cast(ikm.data())); + + const auto params = std::array{ + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode), + OSSL_PARAM_construct_utf8_string( + OSSL_KDF_PARAM_DIGEST, digest_name, 0), + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, ikm_ptr, ikm.size()), + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_SALT, salt_ptr, salt.size()), + OSSL_PARAM_construct_end(), + }; + + const auto kdf = + scoped_evp_kdf(EVP_KDF_fetch(NULL, "HKDF", NULL), EVP_KDF_free); + const auto ctx = + scoped_evp_kdf_ctx(EVP_KDF_CTX_new(kdf.get()), EVP_KDF_CTX_free); + if (1 != EVP_KDF_CTX_set_params(ctx.get(), params.data())) { + throw openssl_error(); + } + + const auto digest_size = EVP_KDF_CTX_get_kdf_size(ctx.get()); + auto out = bytes(digest_size); + if (1 != EVP_KDF_derive(ctx.get(), out.data(), out.size(), nullptr)) { + throw openssl_error(); + } + + return out; +} + +bytes +hkdf_expand(CipherSuite suite, const bytes& prk, const bytes& info, size_t size) +{ + auto mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY; + auto digest_name = const_cast(openssl_digest_name(suite)); + auto* prk_ptr = const_cast(reinterpret_cast(prk.data())); + auto* info_ptr = + const_cast(reinterpret_cast(info.data())); + + const auto params = std::array{ + OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode), + OSSL_PARAM_construct_utf8_string( + OSSL_KDF_PARAM_DIGEST, digest_name, 0), + OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_KEY, prk_ptr, prk.size()), + OSSL_PARAM_construct_octet_string( + OSSL_KDF_PARAM_INFO, info_ptr, info.size()), + OSSL_PARAM_construct_end(), + }; + + const auto kdf = + scoped_evp_kdf(EVP_KDF_fetch(NULL, "HKDF", NULL), EVP_KDF_free); + const auto ctx = + scoped_evp_kdf_ctx(EVP_KDF_CTX_new(kdf.get()), EVP_KDF_CTX_free); + + auto out = bytes(size); + if (1 != EVP_KDF_derive(ctx.get(), out.data(), out.size(), params.data())) { + throw openssl_error(); + } + + return out; +} + +/// +/// AEAD Algorithms +/// + +static bytes +compute_tag(CipherSuite suite, + input_bytes auth_key, + input_bytes aad, + input_bytes ct, + size_t tag_size) +{ + using scoped_evp_mac = std::unique_ptr; + using scoped_evp_mac_ctx = + std::unique_ptr; + + auto digest_name = const_cast(openssl_digest_name(suite)); + std::array params = { + OSSL_PARAM_construct_utf8_string( + OSSL_ALG_PARAM_DIGEST, digest_name, 0), + OSSL_PARAM_construct_end() + }; + + const auto mac = scoped_evp_mac( + EVP_MAC_fetch(nullptr, OSSL_MAC_NAME_HMAC, nullptr), EVP_MAC_free); + const auto ctx = + scoped_evp_mac_ctx(EVP_MAC_CTX_new(mac.get()), EVP_MAC_CTX_free); + + if (1 != EVP_MAC_init( + ctx.get(), auth_key.data(), auth_key.size(), params.data())) { + throw openssl_error(); + } + + if (1 != EVP_MAC_update(ctx.get(), aad.data(), aad.size())) { + throw openssl_error(); + } + + if (1 != EVP_MAC_update(ctx.get(), ct.data(), ct.size())) { + throw openssl_error(); + } + + size_t size = 0; + auto tag = bytes(cipher_digest_size(suite)); + if (1 != EVP_MAC_final(ctx.get(), tag.data(), &size, tag.size())) { + throw openssl_error(); + } + + tag.resize(tag_size); + return tag; +} + +using scoped_evp_cipher_ctx = + std::unique_ptr; + +static void +ctr_crypt(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes out, + input_bytes in) +{ + if (out.size() != in.size()) { + throw buffer_too_small_error("CTR size mismatch"); + } + + auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto padded_nonce = bytes(nonce.begin(), nonce.end()); + padded_nonce.resize(16); + + auto cipher = openssl_cipher(suite); + if (1 != + EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + throw openssl_error(); + } + + int outlen = 0; + auto in_size_int = static_cast(in.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), out.data(), &outlen, in.data(), in_size_int)) { + throw openssl_error(); + } + + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw openssl_error(); + } +} + +static output_bytes +seal_ctr(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + // Split the key into enc and auth subkeys + auto enc_key_size = cipher_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Encrypt with AES-CM + auto inner_ct = ct.subspan(0, pt.size()); + ctr_crypt(suite, enc_key, nonce, inner_ct, pt); + + // Authenticate with truncated HMAC + auto mac = compute_tag(suite, auth_key, aad, inner_ct, tag_size); + auto tag = ct.subspan(pt.size(), tag_size); + std::copy(mac.begin(), mac.begin() + tag_size, tag.begin()); + + return ct.subspan(0, pt.size() + tag_size); +} + +static output_bytes +seal_aead(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < pt.size() + tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw openssl_error(); + } + + int outlen = 0; + auto aad_size_int = static_cast(aad.size()); + if (aad.size() > 0) { + if (1 != EVP_EncryptUpdate( + ctx.get(), nullptr, &outlen, aad.data(), aad_size_int)) { + throw openssl_error(); + } + } + + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + throw openssl_error(); + } + + // Providing nullptr as an argument is safe here because this + // function never writes with GCM; it only computes the tag + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw openssl_error(); + } + + auto tag = ct.subspan(pt.size(), tag_size); + auto tag_ptr = const_cast(static_cast(tag.data())); + auto tag_size_downcast = static_cast(tag.size()); + if (1 != EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_GCM_GET_TAG, tag_size_downcast, tag_ptr)) { + throw openssl_error(); + } + + return ct.subspan(0, pt.size() + tag_size); +} + +output_bytes +seal(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + case CipherSuite::AES_CM_128_HMAC_SHA256_4: { + return seal_ctr(suite, key, nonce, ct, aad, pt); + } + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: { + return seal_aead(suite, key, nonce, ct, aad, pt); + } + } + + throw unsupported_ciphersuite_error(); +} + +static output_bytes +open_ctr(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + auto inner_ct = ct.subspan(0, inner_ct_size); + auto tag = ct.subspan(inner_ct_size, tag_size); + + // Split the key into enc and auth subkeys + auto enc_key_size = cipher_key_size(suite); + auto enc_key = key.first(enc_key_size); + auto auth_key = key.subspan(enc_key_size); + + // Authenticate with truncated HMAC + auto mac = compute_tag(suite, auth_key, aad, inner_ct, tag_size); + if (CRYPTO_memcmp(mac.data(), tag.data(), tag.size()) != 0) { + throw authentication_error(); + } + + // Decrypt with AES-CTR + const auto pt_out = pt.first(inner_ct_size); + ctr_crypt(suite, enc_key, nonce, pt_out, ct.first(inner_ct_size)); + + return pt_out; +} + +static output_bytes +open_aead(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + auto tag_size = cipher_overhead(suite); + if (ct.size() < tag_size) { + throw buffer_too_small_error("Ciphertext buffer too small"); + } + + auto inner_ct_size = ct.size() - tag_size; + if (pt.size() < inner_ct_size) { + throw buffer_too_small_error("Plaintext buffer too small"); + } + + auto ctx = scoped_evp_cipher_ctx(EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free); + if (ctx.get() == nullptr) { + throw openssl_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw openssl_error(); + } + + auto tag = ct.subspan(inner_ct_size, tag_size); + auto tag_ptr = const_cast(static_cast(tag.data())); + auto tag_size_downcast = static_cast(tag.size()); + if (1 != EVP_CIPHER_CTX_ctrl( + ctx.get(), EVP_CTRL_GCM_SET_TAG, tag_size_downcast, tag_ptr)) { + throw openssl_error(); + } + + int out_size; + auto aad_size_int = static_cast(aad.size()); + if (aad.size() > 0) { + if (1 != EVP_DecryptUpdate( + ctx.get(), nullptr, &out_size, aad.data(), aad_size_int)) { + throw openssl_error(); + } + } + + auto inner_ct_size_int = static_cast(inner_ct_size); + if (1 != EVP_DecryptUpdate( + ctx.get(), pt.data(), &out_size, ct.data(), inner_ct_size_int)) { + throw openssl_error(); + } + + // Providing nullptr as an argument is safe here because this + // function never writes with GCM; it only verifies the tag + if (1 != EVP_DecryptFinal(ctx.get(), nullptr, &out_size)) { + throw authentication_error(); + } + + return pt.subspan(0, inner_ct_size); +} + +output_bytes +open(CipherSuite suite, + const bytes& key, + const bytes& nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + switch (suite) { + case CipherSuite::AES_CM_128_HMAC_SHA256_8: + case CipherSuite::AES_CM_128_HMAC_SHA256_4: { + return open_ctr(suite, key, nonce, pt, aad, ct); + } + + case CipherSuite::AES_GCM_128_SHA256: + case CipherSuite::AES_GCM_256_SHA512: { + return open_aead(suite, key, nonce, pt, aad, ct); + } + } + + throw unsupported_ciphersuite_error(); +} + +} // namespace SFRAME_NAMESPACE + +#endif // defined(OPENSSL_3) diff --git a/src/header.cpp b/src/header.cpp index 1cbab0e..0609cfa 100644 --- a/src/header.cpp +++ b/src/header.cpp @@ -1,6 +1,6 @@ #include "header.h" -namespace sframe { +namespace SFRAME_NAMESPACE { static size_t uint_size(uint64_t val) @@ -109,4 +109,4 @@ Header::encode(output_bytes buffer) const return 1 + kid_size + ctr_size; } -} // namespace sframe +} // namespace SFRAME_NAMESPACE diff --git a/src/header.h b/src/header.h index b1a6968..55ac232 100644 --- a/src/header.h +++ b/src/header.h @@ -2,7 +2,7 @@ #include -namespace sframe { +namespace SFRAME_NAMESPACE { void encode_uint(uint64_t val, output_bytes buffer); @@ -21,4 +21,4 @@ struct Header static constexpr size_t max_size = 1 + 7 + 7; }; -} // namespace sframe +} // namespace SFRAME_NAMESPACE diff --git a/src/sframe.cpp b/src/sframe.cpp index 4290f6b..ed4a3c6 100644 --- a/src/sframe.cpp +++ b/src/sframe.cpp @@ -10,7 +10,7 @@ #include #include -namespace sframe { +namespace SFRAME_NAMESPACE { std::ostream& operator<<(std::ostream& str, const input_bytes data) @@ -294,4 +294,4 @@ MLSContext::get_state(KeyID key_id) return epoch->get(suite, sender_id); } -} // namespace sframe +} // namespace SFRAME_NAMESPACE diff --git a/test/sframe.cpp b/test/sframe.cpp index 2e5d818..f6f5ff8 100644 --- a/test/sframe.cpp +++ b/test/sframe.cpp @@ -8,16 +8,7 @@ #include // for invalid_argument #include // for basic_string, operator== -using namespace sframe; - -static void -ensure_fips_if_required() -{ - const auto* require = std::getenv("REQUIRE_FIPS"); - if (require && FIPS_mode() == 0) { - REQUIRE(FIPS_mode_set(1) == 1); - } -} +using namespace SFRAME_NAMESPACE; static bytes from_hex(const std::string& hex) @@ -45,8 +36,6 @@ to_bytes(const T& range) TEST_CASE("SFrame Known-Answer") { - ensure_fips_if_required(); - struct KnownAnswerTest { bytes key; @@ -143,8 +132,6 @@ TEST_CASE("SFrame Known-Answer") TEST_CASE("SFrame Round-Trip") { - ensure_fips_if_required(); - const auto rounds = 1 << 9; const auto kid = KeyID(0x42); const auto plaintext = from_hex("00010203"); @@ -183,8 +170,6 @@ TEST_CASE("SFrame Round-Trip") TEST_CASE("MLS Known-Answer") { - ensure_fips_if_required(); - struct KnownAnswerTest { using Epoch = std::vector; @@ -312,8 +297,6 @@ TEST_CASE("MLS Known-Answer") TEST_CASE("MLS Round-Trip") { - ensure_fips_if_required(); - const auto epoch_bits = 2; const auto test_epochs = 1 << (epoch_bits + 1); const auto epoch_rounds = 10; @@ -359,8 +342,6 @@ TEST_CASE("MLS Round-Trip") TEST_CASE("MLS Known-Answer with Context") { - ensure_fips_if_required(); - struct KnownAnswerTest { using ContextCases = std::vector; @@ -643,8 +624,6 @@ TEST_CASE("MLS Known-Answer with Context") TEST_CASE("MLS Round-Trip with context") { - ensure_fips_if_required(); - const auto epoch_bits = 4; const auto test_epochs = 1 << (epoch_bits + 1); const auto epoch_rounds = 10; @@ -709,8 +688,6 @@ TEST_CASE("MLS Round-Trip with context") TEST_CASE("MLS Failure after Purge") { - ensure_fips_if_required(); - const auto suite = CipherSuite::AES_GCM_128_SHA256; const auto epoch_bits = 2; const auto plaintext = from_hex("00010203");