From ffe3fba8afb1047e08f206792fc1eb215fcf28a6 Mon Sep 17 00:00:00 2001 From: glook9001 Date: Thu, 10 Sep 2026 18:11:02 +0300 Subject: [PATCH] refactor(crypto): remove standalone sha256 and migrate all callers to OpenSSL EVP (#122) * refactor(crypto): deprecate custom sha256 in favor of OpenSSL libcrypto - Remove standalone src/foundation/sha256.cc, include/mocktail/sha256.h, and tests/sha256_test.cc - Remove mocktail_crypto_foundation library and sha256_test targets from CMake - Export clean FileDigest and HashText helpers via include/update/payload_integrity.h - Deduplicate byte-to-hex formatting across host ABI loader and updater modules - Support direct generation hash sizing to avoid repetitive string slicing - Validate all EVP return codes across streaming downloads and asset tree hashing * fix(compat): abort candidate canary registration when profile hashing fails - Return Failure before profile validation and registration if ComputeSha256Hex fails - Propagate canary attestation hashing failures in ValidateCanaryAttestation - Add SetHostAbiProfileHashingFaultForTesting hook and regression test in host_abi_profile_test --------- Co-authored-by: glook9001 <310372091+glook9001@users.noreply.github.com> --- CMakeLists.txt | 10 +- cmake/MocktailElfCompatibility.cmake | 2 +- include/compat/host_abi_profile_loader.h | 2 + include/mocktail/sha256.h | 45 ------ include/update/payload_integrity.h | 23 +++ src/compat/host_abi_profile_loader.cc | 75 ++++++++-- src/foundation/sha256.cc | 169 ----------------------- src/update/candidate_approval.cc | 33 +++-- src/update/http_download.cc | 54 ++++---- src/update/payload_integrity.cc | 136 ++++++++++-------- tests/CMakeLists.txt | 8 -- tests/host_abi_profile_test.cc | 22 +++ tests/sha256_test.cc | 26 ---- 13 files changed, 239 insertions(+), 366 deletions(-) delete mode 100644 include/mocktail/sha256.h delete mode 100644 src/foundation/sha256.cc delete mode 100644 tests/sha256_test.cc diff --git a/CMakeLists.txt b/CMakeLists.txt index 47877a2..a0fdeeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -138,12 +138,7 @@ if(BUILD_TESTING) FetchContent_MakeAvailable(googletest) endif() -add_library(mocktail_crypto_foundation STATIC - src/foundation/sha256.cc -) -add_library(Mocktail::CryptoFoundation ALIAS mocktail_crypto_foundation) -target_include_directories(mocktail_crypto_foundation PUBLIC include) -mocktail_apply_compile_options(mocktail_crypto_foundation) +find_package(OpenSSL REQUIRED) include(${CMAKE_SOURCE_DIR}/cmake/MocktailPlatformGraphics.cmake) foreach(foundation_target IN ITEMS @@ -201,7 +196,6 @@ target_include_directories(mocktail_bionic_stdio_runtime PUBLIC include) mocktail_apply_compile_options(mocktail_bionic_stdio_runtime) find_package(CURL REQUIRED) -find_package(OpenSSL REQUIRED) find_package(nlohmann_json CONFIG REQUIRED) find_package(PkgConfig REQUIRED) pkg_check_modules(LIBYAML REQUIRED IMPORTED_TARGET yaml-0.1) @@ -234,7 +228,6 @@ add_library(Mocktail::Update ALIAS mocktail_update) target_include_directories(mocktail_update PUBLIC include) target_link_libraries(mocktail_update PUBLIC Mocktail::Compat - Mocktail::CryptoFoundation CURL::libcurl OpenSSL::Crypto PkgConfig::LIBYAML @@ -334,7 +327,6 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND endif() target_link_libraries(mocktail_runtime PUBLIC Threads::Threads) target_link_libraries(mocktail_runtime PRIVATE - Mocktail::CryptoFoundation CURL::libcurl nlohmann_json::nlohmann_json PkgConfig::GIO diff --git a/cmake/MocktailElfCompatibility.cmake b/cmake/MocktailElfCompatibility.cmake index 9c3621f..1f8a9e3 100644 --- a/cmake/MocktailElfCompatibility.cmake +++ b/cmake/MocktailElfCompatibility.cmake @@ -44,7 +44,7 @@ target_link_libraries(mocktail_compat PUBLIC nlohmann_json::nlohmann_json Threads::Threads ) -target_link_libraries(mocktail_compat PRIVATE Mocktail::CryptoFoundation) +target_link_libraries(mocktail_compat PRIVATE OpenSSL::Crypto) target_compile_features(mocktail_compat PUBLIC cxx_std_17) if(COMMAND mocktail_apply_compile_options) diff --git a/include/compat/host_abi_profile_loader.h b/include/compat/host_abi_profile_loader.h index 34211fa..1c7cf39 100644 --- a/include/compat/host_abi_profile_loader.h +++ b/include/compat/host_abi_profile_loader.h @@ -54,6 +54,8 @@ ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment( const HostAbiProfile* FindLoadedExternalHostAbiProfile( std::string_view build_id) noexcept; +void SetHostAbiProfileHashingFaultForTesting(bool enabled) noexcept; + } // namespace mocktail::compat #endif // MOCKTAIL_COMPAT_HOST_ABI_PROFILE_LOADER_H_ diff --git a/include/mocktail/sha256.h b/include/mocktail/sha256.h deleted file mode 100644 index a68d4a1..0000000 --- a/include/mocktail/sha256.h +++ /dev/null @@ -1,45 +0,0 @@ -#ifndef MOCKTAIL_MOCKTAIL_SHA256_H_ -#define MOCKTAIL_MOCKTAIL_SHA256_H_ - -#include -#include -#include -#include -#include - -namespace mocktail { -namespace foundation { - -class Sha256 final { - public: - Sha256() = default; - ~Sha256(); - - Sha256(const Sha256&) = delete; - Sha256& operator=(const Sha256&) = delete; - - void Update(const unsigned char* bytes, std::size_t size); - void Update(std::string_view bytes); - std::string FinalHex(); - - private: - void Transform(const unsigned char* block); - - std::array state_ = { - 0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU, - 0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U, - }; - std::array block_{}; - std::size_t block_bytes_ = 0; - std::uint64_t total_bytes_ = 0; - bool finalized_ = false; -}; - -// Returns the lowercase SHA-256 digest of the exact byte sequence, including -// embedded NUL bytes. -std::string ComputeSha256Hex(std::string_view bytes); - -} // namespace foundation -} // namespace mocktail - -#endif // MOCKTAIL_MOCKTAIL_SHA256_H_ diff --git a/include/update/payload_integrity.h b/include/update/payload_integrity.h index 6da2fb5..8686ccf 100644 --- a/include/update/payload_integrity.h +++ b/include/update/payload_integrity.h @@ -5,9 +5,32 @@ #include #include #include +#include + +struct evp_md_ctx_st; namespace mocktail::update { +class FileDigest final { + public: + FileDigest(); + ~FileDigest(); + FileDigest(const FileDigest&) = delete; + FileDigest& operator=(const FileDigest&) = delete; + + bool valid() const noexcept { return context_ != nullptr; } + bool Update(const void* data, std::size_t size); + bool Update(std::string_view bytes); + std::string FinalHex(std::size_t max_chars = 0); + + private: + struct evp_md_ctx_st* context_ = nullptr; +}; + +std::string HashText(std::string_view text, std::string* error = nullptr); +std::string HashText(std::string_view text, std::size_t max_hex_chars, + std::string* error = nullptr); + struct PayloadMetadata { std::string package_name; std::string version_name; diff --git a/src/compat/host_abi_profile_loader.cc b/src/compat/host_abi_profile_loader.cc index 0a506a8..2c3c1a2 100644 --- a/src/compat/host_abi_profile_loader.cc +++ b/src/compat/host_abi_profile_loader.cc @@ -10,6 +10,7 @@ #define JSON_NOEXCEPTION 1 #include #include +#include #include #include #include @@ -28,14 +29,47 @@ #include #include +#include + #include "compat/elf_build_id.h" -#include "mocktail/sha256.h" namespace mocktail::compat { namespace { using Json = nlohmann::json; +std::atomic g_hashing_fault_for_testing{false}; + +std::string FormatHex(const unsigned char* data, std::size_t size, + std::size_t max_chars = 0) { + static constexpr char kHex[] = "0123456789abcdef"; + const std::size_t limit = + (max_chars > 0 && max_chars < size * 2U) ? max_chars : size * 2U; + std::string hex; + hex.reserve(limit); + for (std::size_t i = 0; i < size && hex.size() < limit; ++i) { + hex.push_back(kHex[(data[i] >> 4) & 0x0f]); + if (hex.size() < limit) { + hex.push_back(kHex[data[i] & 0x0f]); + } + } + return hex; +} + +std::string ComputeSha256Hex(std::string_view bytes, + std::size_t max_chars = 0) { + if (g_hashing_fault_for_testing.load(std::memory_order_acquire)) { + return {}; + } + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int len = 0; + if (EVP_Digest(bytes.data(), bytes.size(), digest, &len, EVP_sha256(), + nullptr) != 1) { + return {}; + } + return FormatHex(digest, len, max_chars); +} + constexpr std::size_t kMaximumProfileBytes = 1024 * 1024; constexpr std::size_t kMaximumReceiptBytes = 64 * 1024; constexpr std::size_t kMaximumInitArrayEntries = 1024 * 1024; @@ -862,7 +896,11 @@ bool ValidateCanaryAttestation(const std::string& attestation_path, return false; } - evidence->sha256 = foundation::ComputeSha256Hex(attestation_bytes); + evidence->sha256 = ComputeSha256Hex(attestation_bytes); + if (evidence->sha256.empty()) { + *error = "cannot compute canary attestation SHA-256"; + return false; + } evidence->run_id = std::move(run_id); return true; } @@ -1013,7 +1051,7 @@ bool ValidateReceipt(const std::string& receipt_path, .append(canary_evidence[1].sha256) .append("\n"); const std::string expected_generation = - foundation::ComputeSha256Hex(generation_evidence).substr(0, 40); + ComputeSha256Hex(generation_evidence, 40); if (generation != expected_generation) { *error = "approval receipt evidence generation does not match"; return false; @@ -1066,19 +1104,31 @@ FileSha256Result ComputeFileSha256(const std::string& path) { if (!input) { return {{}, "cannot open file for SHA-256: " + path}; } - foundation::Sha256 sha256; + std::unique_ptr ctx( + EVP_MD_CTX_new(), &EVP_MD_CTX_free); + if (!ctx || EVP_DigestInit_ex(ctx.get(), EVP_sha256(), nullptr) != 1) { + return {{}, "cannot initialize SHA-256 digest context for: " + path}; + } std::array buffer{}; while (input) { input.read(reinterpret_cast(buffer.data()), buffer.size()); const std::streamsize read = input.gcount(); if (read > 0) { - sha256.Update(buffer.data(), static_cast(read)); + if (EVP_DigestUpdate(ctx.get(), buffer.data(), + static_cast(read)) != 1) { + return {{}, "cannot update SHA-256 digest for: " + path}; + } } } if (!input.eof()) { return {{}, "cannot read file for SHA-256: " + path}; } - return {sha256.FinalHex(), {}}; + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int len = 0; + if (EVP_DigestFinal_ex(ctx.get(), digest, &len) != 1) { + return {{}, "cannot finalize SHA-256 digest for: " + path}; + } + return {FormatHex(digest, len), {}}; } const HostAbiProfile* FindLoadedExternalHostAbiProfile( @@ -1161,11 +1211,10 @@ ExternalHostAbiProfileResult LoadExternalHostAbiProfile( return Failure( "external host ABI profile SHA-256 does not match payload bytes"); } - foundation::Sha256 profile_hasher; - profile_hasher.Update( - reinterpret_cast(profile_bytes.data()), - profile_bytes.size()); - const std::string profile_sha256 = profile_hasher.FinalHex(); + const std::string profile_sha256 = ComputeSha256Hex(profile_bytes); + if (profile_sha256.empty()) { + return Failure("cannot compute external host ABI profile SHA-256"); + } if (!ValidateProfileAgainstElf(*canonical_payload, profile->profile, profile->derivation_code_rvas, &path_error)) { return Failure(path_error); @@ -1243,4 +1292,8 @@ ExternalHostAbiProfileResult InitializeHostAbiProfileFromEnvironment( return LoadExternalHostAbiProfile(request); } +void SetHostAbiProfileHashingFaultForTesting(bool enabled) noexcept { + g_hashing_fault_for_testing.store(enabled, std::memory_order_release); +} + } // namespace mocktail::compat diff --git a/src/foundation/sha256.cc b/src/foundation/sha256.cc deleted file mode 100644 index 707b674..0000000 --- a/src/foundation/sha256.cc +++ /dev/null @@ -1,169 +0,0 @@ -#include "mocktail/sha256.h" - -#include -#include -#include - -namespace mocktail { -namespace foundation { -namespace { - -constexpr std::array kRoundConstants = { - 0x428a2f98U, 0x71374491U, 0xb5c0fbcfU, 0xe9b5dba5U, 0x3956c25bU, - 0x59f111f1U, 0x923f82a4U, 0xab1c5ed5U, 0xd807aa98U, 0x12835b01U, - 0x243185beU, 0x550c7dc3U, 0x72be5d74U, 0x80deb1feU, 0x9bdc06a7U, - 0xc19bf174U, 0xe49b69c1U, 0xefbe4786U, 0x0fc19dc6U, 0x240ca1ccU, - 0x2de92c6fU, 0x4a7484aaU, 0x5cb0a9dcU, 0x76f988daU, 0x983e5152U, - 0xa831c66dU, 0xb00327c8U, 0xbf597fc7U, 0xc6e00bf3U, 0xd5a79147U, - 0x06ca6351U, 0x14292967U, 0x27b70a85U, 0x2e1b2138U, 0x4d2c6dfcU, - 0x53380d13U, 0x650a7354U, 0x766a0abbU, 0x81c2c92eU, 0x92722c85U, - 0xa2bfe8a1U, 0xa81a664bU, 0xc24b8b70U, 0xc76c51a3U, 0xd192e819U, - 0xd6990624U, 0xf40e3585U, 0x106aa070U, 0x19a4c116U, 0x1e376c08U, - 0x2748774cU, 0x34b0bcb5U, 0x391c0cb3U, 0x4ed8aa4aU, 0x5b9cca4fU, - 0x682e6ff3U, 0x748f82eeU, 0x78a5636fU, 0x84c87814U, 0x8cc70208U, - 0x90befffaU, 0xa4506cebU, 0xbef9a3f7U, 0xc67178f2U, -}; - -std::uint32_t RotateRight(std::uint32_t value, unsigned int count) { - return (value >> count) | (value << (32U - count)); -} - -} // namespace - -Sha256::~Sha256() { - volatile unsigned char* block = block_.data(); - for (std::size_t index = 0; index < block_.size(); ++index) { - block[index] = 0; - } - volatile std::uint32_t* state = state_.data(); - for (std::size_t index = 0; index < state_.size(); ++index) { - state[index] = 0; - } - block_bytes_ = 0; - total_bytes_ = 0; -} - -void Sha256::Update(const unsigned char* bytes, std::size_t size) { - if (finalized_ || (bytes == nullptr && size != 0)) { - return; - } - total_bytes_ += size; - while (size > 0) { - const std::size_t copied = std::min(size, block_.size() - block_bytes_); - std::memcpy(block_.data() + block_bytes_, bytes, copied); - block_bytes_ += copied; - bytes += copied; - size -= copied; - if (block_bytes_ == block_.size()) { - Transform(block_.data()); - block_bytes_ = 0; - } - } -} - -void Sha256::Update(std::string_view bytes) { - Update(reinterpret_cast(bytes.data()), bytes.size()); -} - -std::string Sha256::FinalHex() { - if (finalized_) { - return {}; - } - finalized_ = true; - const std::uint64_t bit_count = total_bytes_ * 8U; - block_[block_bytes_++] = 0x80U; - if (block_bytes_ > 56) { - std::fill(block_.begin() + block_bytes_, block_.end(), 0); - Transform(block_.data()); - block_bytes_ = 0; - } - std::fill(block_.begin() + block_bytes_, block_.begin() + 56, 0); - for (std::size_t index = 0; index < 8; ++index) { - block_[63 - index] = static_cast(bit_count >> (index * 8U)); - } - Transform(block_.data()); - - constexpr char kHex[] = "0123456789abcdef"; - std::string digest(64, '0'); - for (std::size_t index = 0; index < state_.size(); ++index) { - const std::uint32_t word = state_[index]; - for (std::size_t byte_index = 0; byte_index < 4; ++byte_index) { - const unsigned int shift = - 24U - static_cast(byte_index * 8); - const unsigned char byte = static_cast(word >> shift); - const std::size_t output = index * 8 + byte_index * 2; - digest[output] = kHex[byte >> 4U]; - digest[output + 1] = kHex[byte & 0x0fU]; - } - } - return digest; -} - -void Sha256::Transform(const unsigned char* block) { - std::array words{}; - for (std::size_t index = 0; index < 16; ++index) { - words[index] = static_cast(block[index * 4]) << 24U | - static_cast(block[index * 4 + 1]) << 16U | - static_cast(block[index * 4 + 2]) << 8U | - static_cast(block[index * 4 + 3]); - } - for (std::size_t index = 16; index < words.size(); ++index) { - const std::uint32_t first = words[index - 15]; - const std::uint32_t second = words[index - 2]; - const std::uint32_t sigma0 = - RotateRight(first, 7) ^ RotateRight(first, 18) ^ (first >> 3U); - const std::uint32_t sigma1 = - RotateRight(second, 17) ^ RotateRight(second, 19) ^ (second >> 10U); - words[index] = words[index - 16] + sigma0 + words[index - 7] + sigma1; - } - - std::uint32_t a = state_[0]; - std::uint32_t b = state_[1]; - std::uint32_t c = state_[2]; - std::uint32_t d = state_[3]; - std::uint32_t e = state_[4]; - std::uint32_t f = state_[5]; - std::uint32_t g = state_[6]; - std::uint32_t h = state_[7]; - for (std::size_t index = 0; index < words.size(); ++index) { - const std::uint32_t sum1 = - RotateRight(e, 6) ^ RotateRight(e, 11) ^ RotateRight(e, 25); - const std::uint32_t choose = (e & f) ^ (~e & g); - const std::uint32_t first = - h + sum1 + choose + kRoundConstants[index] + words[index]; - const std::uint32_t sum0 = - RotateRight(a, 2) ^ RotateRight(a, 13) ^ RotateRight(a, 22); - const std::uint32_t majority = (a & b) ^ (a & c) ^ (b & c); - const std::uint32_t second = sum0 + majority; - h = g; - g = f; - f = e; - e = d + first; - d = c; - c = b; - b = a; - a = first + second; - } - state_[0] += a; - state_[1] += b; - state_[2] += c; - state_[3] += d; - state_[4] += e; - state_[5] += f; - state_[6] += g; - state_[7] += h; - - volatile std::uint32_t* sensitive_words = words.data(); - for (std::size_t index = 0; index < words.size(); ++index) { - sensitive_words[index] = 0; - } -} - -std::string ComputeSha256Hex(std::string_view bytes) { - Sha256 sha256; - sha256.Update(bytes); - return sha256.FinalHex(); -} - -} // namespace foundation -} // namespace mocktail diff --git a/src/update/candidate_approval.cc b/src/update/candidate_approval.cc index 62647ab..49c32d8 100644 --- a/src/update/candidate_approval.cc +++ b/src/update/candidate_approval.cc @@ -19,7 +19,6 @@ #include #include "compat/elf_build_id.h" -#include "mocktail/sha256.h" #include "update/payload_integrity.h" namespace mocktail::update { @@ -144,10 +143,6 @@ bool CopyImmutable(const std::filesystem::path& source, return error->empty() && WriteExclusive(destination, contents, 0444, error); } -std::string HashText(std::string_view contents) { - return foundation::ComputeSha256Hex(contents); -} - std::string UtcTimestamp() { const std::time_t now = std::time(nullptr); std::tm utc{}; @@ -271,7 +266,7 @@ std::string PayloadRuntimeFingerprint( evidence.append("assets=") .append(payload.metadata.asset_tree_sha256) .append("\n"); - return HashText(evidence); + return HashText(evidence, error); } CandidateApprovalResult CreateCandidateApproval( @@ -301,8 +296,10 @@ CandidateApprovalResult CreateCandidateApproval( &result.error)) { return result; } - const std::string profile_sha256 = HashText(profile_bytes); - const std::string compatibility_sha256 = HashText(compatibility_bytes); + const std::string profile_sha256 = HashText(profile_bytes, &result.error); + const std::string compatibility_sha256 = + HashText(compatibility_bytes, &result.error); + if (!result.error.empty()) return result; const std::string runtime_sha256 = HashRegularFile(options.runtime_binary, &result.error); const compat::BuildIdResult runtime_build_id = @@ -342,7 +339,8 @@ CandidateApprovalResult CreateCandidateApproval( {"readiness_log_sha256", log_sha256}, }; attestations[index] = attestation.dump(2) + "\n"; - attestation_hashes[index] = HashText(attestations[index]); + attestation_hashes[index] = HashText(attestations[index], &result.error); + if (!result.error.empty()) return result; } std::string generation_evidence; generation_evidence.append("runtime_build_id=") @@ -359,7 +357,8 @@ CandidateApprovalResult CreateCandidateApproval( generation_evidence.append("canary_2=") .append(attestation_hashes[1]) .append("\n"); - result.generation = HashText(generation_evidence).substr(0, 40); + result.generation = HashText(generation_evidence, 40, &result.error); + if (!result.error.empty()) return result; const std::string stem = payload.payload_id + "-" + result.generation; result.profile = options.store_root / "host_abi_profiles" / (stem + ".json"); result.compatibility_manifest = @@ -462,8 +461,9 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root, } const std::string fingerprint = PayloadRuntimeFingerprint( store_root / "payloads" / payload.payload_id, payload, error); - const std::string profile_sha256 = HashText(profile_bytes); - const std::string compatibility_sha256 = HashText(compatibility_bytes); + const std::string profile_sha256 = HashText(profile_bytes, error); + const std::string compatibility_sha256 = + HashText(compatibility_bytes, error); if (!error->empty()) return false; const std::string canary_runtime_sha256 = receipt->value("canary_runtime_sha256", ""); @@ -510,7 +510,8 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root, const std::string bytes = ReadRegular(attestation_path, true, error); const auto attestation = ParseJson(bytes, error); if (!error->empty() || !attestation.has_value()) return false; - attestation_hashes[index] = HashText(bytes); + attestation_hashes[index] = HashText(bytes, error); + if (!error->empty()) return false; run_ids[index] = attestation->value("run_id", ""); if (attestation->value("schema_version", 0) != 1 || attestation->value("status", "") != "passed" || @@ -551,8 +552,10 @@ bool ValidateCandidateApproval(const std::filesystem::path& store_root, generation_evidence.append("canary_2=") .append(attestation_hashes[1]) .append("\n"); - if (HashText(generation_evidence).substr(0, 40) != *generation) { - *error = "approval evidence generation does not match"; + const std::string computed_generation = + HashText(generation_evidence, 40, error); + if (!error->empty() || computed_generation != *generation) { + if (error->empty()) *error = "approval evidence generation does not match"; return false; } return true; diff --git a/src/update/http_download.cc b/src/update/http_download.cc index 2ee19bd..b239f81 100644 --- a/src/update/http_download.cc +++ b/src/update/http_download.cc @@ -15,7 +15,7 @@ #include #include -#include "mocktail/sha256.h" +#include "update/payload_integrity.h" namespace mocktail::update { namespace { @@ -184,9 +184,10 @@ struct FileWriter { // Includes any resumed prefix, so the limit covers the whole file. std::size_t written = 0; // Null while resuming; the digest is then taken in one pass at the end. - foundation::Sha256* sha256 = nullptr; + FileDigest* sha256 = nullptr; bool exceeded = false; bool write_failed = false; + bool hash_failed = false; }; std::size_t WriteFileBytes(char* data, std::size_t size, std::size_t count, @@ -213,32 +214,15 @@ std::size_t WriteFileBytes(char* data, std::size_t size, std::size_t count, offset += static_cast(result); } if (writer->sha256 != nullptr) { - writer->sha256->Update(reinterpret_cast(data), bytes); + if (!writer->sha256->Update(data, bytes)) { + writer->hash_failed = true; + return 0; + } } writer->written += bytes; return bytes; } -std::string HashCompletedFile(const std::filesystem::path& path) { - const int descriptor = open(path.c_str(), O_RDONLY | O_CLOEXEC | O_NOFOLLOW); - if (descriptor < 0) return {}; - foundation::Sha256 sha256; - std::array buffer{}; - while (true) { - const ssize_t bytes = read(descriptor, buffer.data(), buffer.size()); - if (bytes == 0) break; - if (bytes < 0) { - if (errno == EINTR) continue; - close(descriptor); - return {}; - } - sha256.Update(reinterpret_cast(buffer.data()), - static_cast(bytes)); - } - close(descriptor); - return sha256.FinalHex(); -} - bool Configure(CURL* handle, const HttpTransferRequest& request, std::string_view url, curl_slist* headers, std::array* error_buffer) { @@ -427,7 +411,13 @@ HttpDownloadResult DownloadFileAttempt(const HttpTransferRequest& request, result.error = "cannot configure HTTPS download"; return result; } - foundation::Sha256 sha256; + FileDigest sha256; + if (!resuming && !sha256.valid()) { + close(descriptor); + std::filesystem::remove(temporary, filesystem_error); + result.error = "cannot initialize sha256 digest"; + return result; + } FileWriter writer{descriptor, request.maximum_bytes, static_cast(existing), resuming ? nullptr : &sha256, false, false}; @@ -447,8 +437,10 @@ HttpDownloadResult DownloadFileAttempt(const HttpTransferRequest& request, curl_easy_getinfo(handle.get(), CURLINFO_RESPONSE_CODE, &http_status); const bool synced = fsync(descriptor) == 0; close(descriptor); - if (status != CURLE_OK || writer.write_failed || !synced) { - if (writer.exceeded) { + if (status != CURLE_OK || writer.write_failed || writer.hash_failed || !synced) { + if (writer.hash_failed) { + result.error = "cannot compute SHA-256 digest during download"; + } else if (writer.exceeded) { result.error = "HTTPS download exceeds its size limit"; } else if (writer.write_failed || !synced) { result.error = "cannot persist HTTPS download"; @@ -494,7 +486,15 @@ HttpDownloadResult DownloadFileAttempt(const HttpTransferRequest& request, return result; } result.bytes_written = writer.written; - result.sha256 = resuming ? HashCompletedFile(temporary) : sha256.FinalHex(); + result.sha256 = resuming ? HashRegularFile(temporary, &result.error) + : sha256.FinalHex(); + if (result.sha256.empty()) { + std::filesystem::remove(temporary, filesystem_error); + if (result.error.empty()) { + result.error = "cannot finalize SHA-256 digest of downloaded file"; + } + return result; + } result.final_url = current; return result; } diff --git a/src/update/payload_integrity.cc b/src/update/payload_integrity.cc index 82a51da..1c12c6a 100644 --- a/src/update/payload_integrity.cc +++ b/src/update/payload_integrity.cc @@ -18,8 +18,6 @@ #include #include -#include "mocktail/sha256.h" - namespace mocktail::update { namespace { @@ -63,58 +61,75 @@ std::string ReadMetadata(const std::filesystem::path& path, return contents; } -// foundation::Sha256 is a portable implementation and runs at roughly 180 MB/s -// here; OpenSSL reaches the CPU's SHA extensions and measures 8x faster on the -// same bytes. A payload is several hundred MiB, so file hashing uses OpenSSL. -// The digests are identical, and foundation::Sha256 stays in use for the small -// inputs elsewhere in the updater. -class FileDigest final { - public: - FileDigest() : context_(EVP_MD_CTX_new()) { - if (context_ != nullptr && - EVP_DigestInit_ex(context_, EVP_sha256(), nullptr) != 1) { - EVP_MD_CTX_free(context_); - context_ = nullptr; +std::string FormatHex(const unsigned char* data, std::size_t size, + std::size_t max_chars = 0) { + static constexpr char kHex[] = "0123456789abcdef"; + const std::size_t limit = + (max_chars > 0 && max_chars < size * 2U) ? max_chars : size * 2U; + std::string hex; + hex.reserve(limit); + for (std::size_t i = 0; i < size && hex.size() < limit; ++i) { + hex.push_back(kHex[(data[i] >> 4) & 0x0f]); + if (hex.size() < limit) { + hex.push_back(kHex[data[i] & 0x0f]); } } - - ~FileDigest() { - if (context_ != nullptr) EVP_MD_CTX_free(context_); - } - - FileDigest(const FileDigest&) = delete; - FileDigest& operator=(const FileDigest&) = delete; - - bool valid() const { return context_ != nullptr; } - - bool Update(const unsigned char* bytes, std::size_t size) { - return context_ != nullptr && - EVP_DigestUpdate(context_, bytes, size) == 1; - } - - std::string FinalHex() { - unsigned char digest[EVP_MAX_MD_SIZE] = {}; - unsigned int size = 0; - if (context_ == nullptr || - EVP_DigestFinal_ex(context_, digest, &size) != 1 || size != 32U) { - return {}; - } - static constexpr char kHex[] = "0123456789abcdef"; - std::string hex; - hex.reserve(size * 2U); - for (unsigned int index = 0; index < size; ++index) { - hex.push_back(kHex[digest[index] >> 4]); - hex.push_back(kHex[digest[index] & 0x0FU]); - } - return hex; - } - - private: - EVP_MD_CTX* context_ = nullptr; -}; + return hex; +} } // namespace +FileDigest::FileDigest() : context_(EVP_MD_CTX_new()) { + if (context_ != nullptr && + EVP_DigestInit_ex(context_, EVP_sha256(), nullptr) != 1) { + EVP_MD_CTX_free(context_); + context_ = nullptr; + } +} + +FileDigest::~FileDigest() { + if (context_ != nullptr) EVP_MD_CTX_free(context_); +} + +bool FileDigest::Update(const void* data, std::size_t size) { + if (size == 0) return context_ != nullptr; + return context_ != nullptr && data != nullptr && + EVP_DigestUpdate(context_, data, size) == 1; +} + +bool FileDigest::Update(std::string_view bytes) { + return Update(bytes.data(), bytes.size()); +} + +std::string FileDigest::FinalHex(std::size_t max_chars) { + unsigned char digest[EVP_MAX_MD_SIZE] = {}; + unsigned int size = 0; + if (context_ == nullptr || + EVP_DigestFinal_ex(context_, digest, &size) != 1 || size != 32U) { + return {}; + } + return FormatHex(digest, size, max_chars); +} + +std::string HashText(std::string_view text, std::size_t max_hex_chars, + std::string* error) { + unsigned char digest[EVP_MAX_MD_SIZE]; + unsigned int len = 0; + if (EVP_Digest(text.data(), text.size(), digest, &len, EVP_sha256(), + nullptr) != 1 || + len != 32U) { + if (error != nullptr && error->empty()) { + *error = "cannot compute SHA-256 text digest"; + } + return {}; + } + return FormatHex(digest, len, max_hex_chars); +} + +std::string HashText(std::string_view text, std::string* error) { + return HashText(text, 64, error); +} + std::string HashRegularFile(const std::filesystem::path& path, std::string* output_error) { std::string local_error; @@ -239,16 +254,27 @@ std::string HashAssetTree(const std::filesystem::path& root, *error = first_error; return {}; } - foundation::Sha256 tree; + FileDigest tree; + if (!tree.valid()) { + *error = "cannot initialise asset tree digest"; + return {}; + } for (std::size_t index = 0; index < relative_files.size(); ++index) { - tree.Update(digests[index]); - tree.Update(" "); - tree.Update(relative_files[index].generic_string()); const unsigned char terminator = 0; - tree.Update(&terminator, 1); + if (!tree.Update(digests[index]) || !tree.Update(" ") || + !tree.Update(relative_files[index].generic_string()) || + !tree.Update(&terminator, 1)) { + *error = "cannot update asset tree digest"; + return {}; + } } if (file_count != nullptr) *file_count = relative_files.size(); - return tree.FinalHex(); + const std::string tree_hex = tree.FinalHex(); + if (tree_hex.empty()) { + *error = "cannot finalise asset tree digest"; + return {}; + } + return tree_hex; } PayloadIntegrityResult InspectPreparedPayload( diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3cc3e15..ceb6ee2 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -595,13 +595,6 @@ target_link_libraries(auth_service_test PRIVATE GTest::gtest_main ) -add_executable(sha256_test sha256_test.cc) -target_link_libraries(sha256_test PRIVATE - Mocktail::CryptoFoundation - GTest::gtest_main -) -mocktail_apply_compile_options(sha256_test) - add_executable(auth_runtime_composition_test auth_runtime_composition_test.cc ) @@ -937,7 +930,6 @@ gtest_discover_tests(window_pointer_capture_owner_test) gtest_discover_tests(roblox_text_input_jni_bridge_test) gtest_discover_tests(auth_service_test) gtest_discover_tests(auth_runtime_composition_test) -gtest_discover_tests(sha256_test) gtest_discover_tests(client_settings_service_test) gtest_discover_tests(elf_build_id_test) gtest_discover_tests(build_profile_test) diff --git a/tests/host_abi_profile_test.cc b/tests/host_abi_profile_test.cc index fa7c1fa..e132de6 100644 --- a/tests/host_abi_profile_test.cc +++ b/tests/host_abi_profile_test.cc @@ -639,6 +639,28 @@ TEST_F(ExternalHostAbiProfileLoaderTest, EXPECT_NE(result.error.find("canary authorization"), std::string::npos); } +TEST_F(ExternalHostAbiProfileLoaderTest, + RejectsCandidateCanaryWhenProfileHashingFails) { + ExternalHostAbiProfileRequest request = BaseRequest(); + request.candidate_canary = true; + request.candidate_process_authorization = true; + request.explicit_unverified_authorization = true; + + struct ScopedFault { + ScopedFault() { SetHostAbiProfileHashingFaultForTesting(true); } + ~ScopedFault() { SetHostAbiProfileHashingFaultForTesting(false); } + } fault; + + const ExternalHostAbiProfileResult result = + LoadExternalHostAbiProfile(request); + + EXPECT_FALSE(result); + EXPECT_NE( + result.error.find("cannot compute external host ABI profile SHA-256"), + std::string::npos); + EXPECT_EQ(FindHostAbiProfile(build_id_), nullptr); +} + TEST_F(ExternalHostAbiProfileLoaderTest, LoadsExactCandidateIntoLookupRegistry) { ExternalHostAbiProfileRequest request = BaseRequest(); diff --git a/tests/sha256_test.cc b/tests/sha256_test.cc deleted file mode 100644 index 6141287..0000000 --- a/tests/sha256_test.cc +++ /dev/null @@ -1,26 +0,0 @@ -#include "mocktail/sha256.h" - -#include - -#include -#include - -namespace mocktail { -namespace foundation { -namespace { - -TEST(Sha256Test, ComputesStandardVector) { - EXPECT_EQ(ComputeSha256Hex("abc"), - "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"); -} - -TEST(Sha256Test, IncludesEmbeddedNullBytes) { - const std::string bytes("a\0b", 3); - EXPECT_EQ(ComputeSha256Hex(bytes), - "59b271ae1bbcb1d31d41929817f4b16fb439eb4f31520b5ad1d5ce98920a7138"); - EXPECT_NE(ComputeSha256Hex(bytes), ComputeSha256Hex(std::string_view("a"))); -} - -} // namespace -} // namespace foundation -} // namespace mocktail