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>
This commit is contained in:
glook9001andglook9001 authored and GitHub committed 2026-09-10 18:11:02 +03:00
1 parent 5d475e03d8
commit ffe3fba8af
13 files changed
+239 -366

No files matched your search

+1 -9
View File
@@ -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
+1 -1
View File
@@ -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)
+2
View File
@@ -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_
-45
View File
@@ -1,45 +0,0 @@
#ifndef MOCKTAIL_MOCKTAIL_SHA256_H_
#define MOCKTAIL_MOCKTAIL_SHA256_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
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<std::uint32_t, 8> state_ = {
0x6a09e667U, 0xbb67ae85U, 0x3c6ef372U, 0xa54ff53aU,
0x510e527fU, 0x9b05688cU, 0x1f83d9abU, 0x5be0cd19U,
};
std::array<unsigned char, 64> 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_
+23
View File
@@ -5,9 +5,32 @@
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
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;
+64 -11
View File
@@ -10,6 +10,7 @@
#define JSON_NOEXCEPTION 1
#include <algorithm>
#include <array>
#include <atomic>
#include <cerrno>
#include <charconv>
#include <cstddef>
@@ -28,14 +29,47 @@
#include <utility>
#include <vector>
#include <openssl/evp.h>
#include "compat/elf_build_id.h"
#include "mocktail/sha256.h"
namespace mocktail::compat {
namespace {
using Json = nlohmann::json;
std::atomic<bool> 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<EVP_MD_CTX, decltype(&EVP_MD_CTX_free)> 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<unsigned char, 128 * 1024> buffer{};
while (input) {
input.read(reinterpret_cast<char*>(buffer.data()), buffer.size());
const std::streamsize read = input.gcount();
if (read > 0) {
sha256.Update(buffer.data(), static_cast<std::size_t>(read));
if (EVP_DigestUpdate(ctx.get(), buffer.data(),
static_cast<std::size_t>(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<const unsigned char*>(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
-169
View File
@@ -1,169 +0,0 @@
#include "mocktail/sha256.h"
#include <algorithm>
#include <array>
#include <cstring>
namespace mocktail {
namespace foundation {
namespace {
constexpr std::array<std::uint32_t, 64> 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<const unsigned char*>(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<unsigned char>(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<unsigned int>(byte_index * 8);
const unsigned char byte = static_cast<unsigned char>(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<std::uint32_t, 64> words{};
for (std::size_t index = 0; index < 16; ++index) {
words[index] = static_cast<std::uint32_t>(block[index * 4]) << 24U |
static_cast<std::uint32_t>(block[index * 4 + 1]) << 16U |
static_cast<std::uint32_t>(block[index * 4 + 2]) << 8U |
static_cast<std::uint32_t>(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
+18 -15
View File
@@ -19,7 +19,6 @@
#include <string_view>
#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;
+27 -27
View File
@@ -15,7 +15,7 @@
#include <mutex>
#include <string>
#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<std::size_t>(result);
}
if (writer->sha256 != nullptr) {
writer->sha256->Update(reinterpret_cast<unsigned char*>(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<char, 256U * 1024U> 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<unsigned char*>(buffer.data()),
static_cast<std::size_t>(bytes));
}
close(descriptor);
return sha256.FinalHex();
}
bool Configure(CURL* handle, const HttpTransferRequest& request,
std::string_view url, curl_slist* headers,
std::array<char, CURL_ERROR_SIZE>* 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<std::size_t>(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;
}
+81 -55
View File
@@ -18,8 +18,6 @@
#include <thread>
#include <vector>
#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(
-8
View File
@@ -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)
+22
View File
@@ -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();
-26
View File
@@ -1,26 +0,0 @@
#include "mocktail/sha256.h"
#include <gtest/gtest.h>
#include <string>
#include <string_view>
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