diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1eb8c60..cd0511b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,6 +18,7 @@ jobs: fail-fast: false matrix: os: [windows-latest, ubuntu-latest, macos-latest] + crypto: [OPENSSL_1_1, OPENSSL_3, BORINGSSL] include: - os: windows-latest vcpkg-cmake-file: "$env:VCPKG_INSTALLATION_ROOT\\scripts\\buildsystems\\vcpkg.cmake" @@ -37,10 +38,15 @@ jobs: steps: - uses: actions/checkout@v4 + - name: dependencies (ubuntu) + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + sudo apt-get install nasm + - name: dependencies (macos) if: ${{ matrix.os == 'macos-latest' }} run: | - brew install llvm ninja + brew install llvm ninja nasm go ln -s "/usr/local/opt/llvm/bin/clang-format" "/usr/local/bin/clang-format" ln -s "/usr/local/opt/llvm/bin/clang-tidy" "/usr/local/bin/clang-tidy" @@ -52,8 +58,8 @@ jobs: key: ${{ runner.os }}-${{ hashFiles( '**/vcpkg.json' ) }} - name: configure to use clang-tidy and sanitizers - run: | - cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DCLANG_TIDY=ON -DTESTING=ON -DSANITIZERS=ON -DCMAKE_TOOLCHAIN_FILE="${{ matrix.vcpkg-cmake-file}}" . + run: > + cmake -B "${{ env.CMAKE_BUILD_DIR }}" -DTESTING=ON -DCLANG_TIDY=ON -DSANITIZERS=ON -DCMAKE_TOOLCHAIN_FILE="${{ matrix.vcpkg-cmake-file}}" -DCRYPTO="${{ matrix.crypto }}" -DVCPKG_MANIFEST_DIR="alternatives/${{ matrix.crypto }}" - name: build run: | diff --git a/CMakeLists.txt b/CMakeLists.txt index b7c2daa..4520438 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ option(TESTING "Build tests" OFF) option(CLANG_TIDY "Perform linting with clang-tidy" OFF) option(SANITIZERS "Enable sanitizers" OFF) -# Use -DCRYPTO=(OPENSSL_1_1 | OPENSSL_3) to configure crypto +# Use -DCRYPTO=(OPENSSL_1_1 | OPENSSL_3 | BORINGSSL) to configure crypto if(NOT DEFINED CRYPTO) set(CRYPTO "OPENSSL_3") endif() @@ -64,6 +64,11 @@ elseif(${CRYPTO} STREQUAL "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() diff --git a/Makefile b/Makefile index 56dfb81..ab4def2 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,11 @@ LIB=./build/libsframe.a TEST_VECTOR_DIR=./build/test TEST_BIN=./build/test/sframe_test -.PHONY: all tidy test clean cclean format +OPENSSL_1_1_MANIFEST=alternatives/OPENSSL_1_1 +OPENSSL_3_MANIFEST=alternatives/OPENSSL_3 +BORINGSSL_MANIFEST=alternatives/BORINGSSL + +.PHONY: all dev dev1 devB tidy test clean cclean format ${LIB}: ${BUILD_DIR} src/* include/sframe/* cmake --build ${BUILD_DIR} --target sframe @@ -20,7 +24,19 @@ ${BUILD_DIR}: CMakeLists.txt test/CMakeLists.txt cmake -B${BUILD_DIR} . dev: CMakeLists.txt test/CMakeLists.txt - cmake -B${BUILD_DIR} -DCMAKE_BUILD_TYPE=Debug -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_BIN}: ${LIB} 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/src/crypto_boringssl.cpp b/src/crypto_boringssl.cpp new file mode 100644 index 0000000..e6a4181 --- /dev/null +++ b/src/crypto_boringssl.cpp @@ -0,0 +1,433 @@ +#if defined(BORINGSSL) + +#include "crypto.h" +#include "header.h" + +#include +#include +#include +#include +#include + +namespace sframe { + +/// +/// Convert between native identifiers / errors and OpenSSL ones +/// + +crypto_error::crypto_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_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: + 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_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: + 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 +/// + +owned_bytes +hkdf_extract(CipherSuite suite, input_bytes salt, input_bytes ikm) +{ + const auto* md = openssl_digest_type(suite); + auto out = owned_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 crypto_error(); + } + + return out; +} + +owned_bytes +hkdf_expand(CipherSuite suite, input_bytes prk, input_bytes info, size_t size) +{ + const auto* md = openssl_digest_type(suite); + auto out = owned_bytes(size); + if (1 != HKDF_expand(out.data(), + out.size(), + md, + prk.data(), + prk.size(), + info.data(), + info.size())) { + throw crypto_error(); + } + + return out; +} + +/// +/// AEAD Algorithms +/// + +static owned_bytes<64> +compute_tag(CipherSuite suite, + input_bytes auth_key, + input_bytes nonce, + 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 crypto_error(); + } + + auto len_block = owned_bytes<24>(); + auto len_view = output_bytes(len_block); + encode_uint(aad.size(), len_view.first(8)); + encode_uint(ct.size(), len_view.first(16).last(8)); + encode_uint(tag_size, len_view.last(8)); + if (1 != HMAC_Update(ctx.get(), len_block.data(), len_block.size())) { + throw crypto_error(); + } + + if (1 != HMAC_Update(ctx.get(), nonce.data(), nonce.size())) { + throw crypto_error(); + } + + if (1 != HMAC_Update(ctx.get(), aad.data(), aad.size())) { + throw crypto_error(); + } + + if (1 != HMAC_Update(ctx.get(), ct.data(), ct.size())) { + throw crypto_error(); + } + + auto tag = owned_bytes<64>(); + auto size = static_cast(EVP_MD_size(md)); + if (1 != HMAC_Final(ctx.get(), tag.data(), &size)) { + throw crypto_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 crypto_error(); + } + + auto padded_nonce = owned_bytes<16>(0); + padded_nonce.append(nonce); + padded_nonce.resize(16); + + auto cipher = openssl_cipher(suite); + if (1 != + EVP_EncryptInit(ctx.get(), cipher, key.data(), padded_nonce.data())) { + throw crypto_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 crypto_error(); + } + + if (1 != EVP_EncryptFinal(ctx.get(), nullptr, &outlen)) { + throw crypto_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, nonce, 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 crypto_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_EncryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw crypto_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 crypto_error(); + } + } + + auto pt_size_int = static_cast(pt.size()); + if (1 != EVP_EncryptUpdate( + ctx.get(), ct.data(), &outlen, pt.data(), pt_size_int)) { + throw crypto_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 crypto_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 crypto_error(); + } + + return ct.subspan(0, pt.size() + tag_size); +} + +output_bytes +seal(CipherSuite suite, + input_bytes key, + input_bytes nonce, + output_bytes ct, + input_bytes aad, + input_bytes pt) +{ + switch (suite) { + case CipherSuite::AES_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { + 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, nonce, 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 crypto_error(); + } + + auto cipher = openssl_cipher(suite); + if (1 != EVP_DecryptInit(ctx.get(), cipher, key.data(), nonce.data())) { + throw crypto_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 crypto_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 crypto_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 crypto_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, + input_bytes key, + input_bytes nonce, + output_bytes pt, + input_bytes aad, + input_bytes ct) +{ + switch (suite) { + case CipherSuite::AES_128_CTR_HMAC_SHA256_80: + case CipherSuite::AES_128_CTR_HMAC_SHA256_64: + case CipherSuite::AES_128_CTR_HMAC_SHA256_32: { + 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 + +#endif // defined(OPENSSL_3) diff --git a/src/crypto_openssl11.cpp b/src/crypto_openssl11.cpp index 6cd16dc..5218029 100644 --- a/src/crypto_openssl11.cpp +++ b/src/crypto_openssl11.cpp @@ -65,7 +65,7 @@ openssl_cipher(CipherSuite suite) } /// -/// HMAC and HKDF +/// HMAC /// struct HMAC diff --git a/test/sframe.cpp b/test/sframe.cpp index 4d616fc..ed59e15 100644 --- a/test/sframe.cpp +++ b/test/sframe.cpp @@ -12,21 +12,8 @@ using namespace sframe; -static void -ensure_fips_if_required() -{ -#if defined(OPENSSL_1_1) - const auto* require = std::getenv("REQUIRE_FIPS"); - if (require && FIPS_mode() == 0) { - REQUIRE(FIPS_mode_set(1) == 1); - } -#endif -} - 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"); @@ -69,8 +56,6 @@ TEST_CASE("SFrame Round-Trip") // only have round-trip tests, not known-answer tests. 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; @@ -116,8 +101,6 @@ TEST_CASE("MLS Round-Trip") 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; @@ -183,8 +166,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 metadata = from_hex("00010203"); diff --git a/test/vectors.cpp b/test/vectors.cpp index ff707e5..ac9adc1 100644 --- a/test/vectors.cpp +++ b/test/vectors.cpp @@ -158,9 +158,9 @@ struct SFrameTestVector const auto act_ct_hex = to_hex(ct_out); const auto exp_ct_hex = to_hex(ct); - CHECK(act_ct_hex == exp_ct_hex); + REQUIRE(act_ct_hex == exp_ct_hex); - CHECK(ct_out == ct); + REQUIRE(ct_out == ct); // Unprotect auto recv_ctx = Context(cipher_suite); @@ -168,7 +168,7 @@ struct SFrameTestVector auto pt_data = owned_bytes<128>(); auto pt_out = recv_ctx.unprotect(pt_data, ct, metadata); - CHECK(pt_out == pt); + REQUIRE(pt_out == pt); } }; diff --git a/vcpkg.json b/vcpkg.json deleted file mode 100644 index 8594ffe..0000000 --- a/vcpkg.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "sframe", - "version-string": "0.1", - "description": "Cisco SFrame C++ library", - "dependencies": [ - "openssl", - "doctest", - "nlohmann-json" - ], - "builtin-baseline": "7476f0d4e77d3333fbb249657df8251c28c4faae", - "overrides": [ - { - "name": "openssl", - "version-string": "3.1.2" - } - ] -}