mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-17 17:00:55 +02:00
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:
1 parent
5d475e03d8
commit
ffe3fba8af
13 files changed
+239
-366
No files matched your search
+1
-9
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_
|
||||
@@ -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_
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user