[fix] persist device signing keys outside the cache directory

This commit is contained in:
dijunkun committed 2026-11-13 10:31:19 +08:00
1 parent 24f91c608a
commit b3be25ad80
4 files changed
+58 -26

No files matched your search

@@ -70,28 +70,38 @@ struct PrivateText {
void Validate(const nlohmann::json& document) {
Require(document.is_object() && document.at("version") == 1);
}
std::optional<nlohmann::json> ReadLocked(const std::filesystem::path& directory) {
const auto path = directory / "identity.json";
if (std::filesystem::symlink_status(path).type() ==
std::filesystem::file_type::not_found) return std::nullopt;
PrivateText text{private_file::Read(path)};
auto saved = nlohmann::json::parse(text.value, nullptr, false);
Require(!saved.is_discarded());
Validate(saved);
return saved;
}
} // namespace
void Update(const std::filesystem::path& cache,
const std::function<void(nlohmann::json&)>& update) {
private_file::EnsureDirectory(cache, true);
StoreLock lock(cache / "identity.json.lock");
const auto path = cache / "identity.json";
const bool exists = std::filesystem::symlink_status(path).type() !=
std::filesystem::file_type::not_found;
nlohmann::json saved = {{"version", 1}};
if (exists) {
PrivateText text{private_file::Read(path)};
saved = nlohmann::json::parse(text.value, nullptr, false);
Require(!saved.is_discarded());
}
Validate(saved);
std::optional<nlohmann::json> Read(const std::filesystem::path& directory) {
private_file::EnsureDirectory(directory, true);
StoreLock lock(directory / "identity.json.lock");
return ReadLocked(directory);
}
void Update(const std::filesystem::path& directory,
const std::function<void(nlohmann::json&, bool)>& update) {
private_file::EnsureDirectory(directory, true);
StoreLock lock(directory / "identity.json.lock");
auto previous = ReadLocked(directory);
auto saved = previous ? std::move(*previous) : nlohmann::json{{"version", 1}};
PrivateText before{saved.dump()};
update(saved);
update(saved, previous.has_value());
Validate(saved);
PrivateText after{saved.dump()};
Require(after.value.size() <= 64 * 1024);
if (!exists || before.value != after.value) private_file::Write(path, after.value);
if (!previous || before.value != after.value)
private_file::Write(directory / "identity.json", after.value);
}
} // namespace crossdesk::device_identity_store
@@ -10,14 +10,17 @@
#include <filesystem>
#include <functional>
#include <nlohmann/json.hpp>
#include <optional>
namespace crossdesk::device_identity_store {
// The shared device signing key lives in cache/identity.json. Read the latest
// document under a process lock, apply an update, then atomically persist it.
// Device signing keys live in the persistent trusted-device directory.
// Read the latest document under a process lock, then atomically persist it.
// Throw on corruption, unsafe permissions, lock contention or persistence error.
void Update(const std::filesystem::path& cache,
const std::function<void(nlohmann::json&)>& update);
// Read returns nullopt only when the identity file is absent (legacy migration).
std::optional<nlohmann::json> Read(const std::filesystem::path& directory);
void Update(const std::filesystem::path& directory,
const std::function<void(nlohmann::json&, bool exists)>& update);
} // namespace crossdesk::device_identity_store
@@ -14,8 +14,8 @@
namespace crossdesk {
namespace {
constexpr size_t kMaxTrustedDevices = 128;
void Require(bool ok) {
if (!ok) throw std::runtime_error("Invalid or unavailable trusted device identity");
void Require(bool ok, const char* error = "Invalid or unavailable trusted device identity") {
if (!ok) throw std::runtime_error(error);
}
std::string Hex(const uint8_t* data, size_t size) {
constexpr char digits[] = "0123456789abcdef";
@@ -81,7 +81,19 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
}
}
device_identity_store::Update(cache, [&](nlohmann::json& identity) {
device_identity_store::Update(directory, [&](nlohmann::json& identity, bool identity_exists) {
bool legacy_identity_exists = false;
if (!identity_exists) {
// The persistent copy is authoritative once migration has completed.
// Leave the old cache copy intact so an interrupted migration is retryable.
if (auto previous = device_identity_store::Read(cache)) {
identity = std::move(*previous);
legacy_identity_exists = true;
}
} else {
Require(identity.contains("trust_private_key"),
"Persistent device signing key is missing; restore the original identity");
}
if (!identity.contains("trust_private_key") && !legacy_key.value.empty())
identity["trust_private_key"] = legacy_key.value;
if (identity.contains("trust_private_key")) {
@@ -94,6 +106,8 @@ TrustedDevices::TrustedDevices(const std::filesystem::path& directory,
secret.data(), secret.size()));
OPENSSL_cleanse(secret.data(), secret.size());
} else {
Require(!exists && !legacy_identity_exists,
"Device signing key is missing from an existing installation; restore the original identity");
key_.reset(EVP_PKEY_Q_keygen(nullptr, nullptr, "ED25519"));
Require(bool(key_));
std::array<uint8_t, 32> secret{};
@@ -270,7 +270,9 @@ void SharedHostIdentity::Import() {
private_file::EnsureDirectory(cache_, true);
private_file::EnsureDirectory(trusted_);
Json identity{{"version", 1}};
if (std::filesystem::exists(cache_ / "identity.json"))
if (std::filesystem::exists(trusted_ / "identity.json"))
identity = PrivateDocument(trusted_ / "identity.json");
else if (std::filesystem::exists(cache_ / "identity.json"))
identity = PrivateDocument(cache_ / "identity.json");
Require(identity.is_object() && identity.value("version", 0) == 1,
"Invalid existing device identity store");
@@ -284,7 +286,7 @@ void SharedHostIdentity::Import() {
cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum));
// Validate everything before replacing any user identity data. Both stores
// stay private to this process's user; their DACLs are never relaxed.
private_file::Write(cache_ / "identity.json", identity.dump());
private_file::Write(trusted_ / "identity.json", identity.dump());
private_file::Write(trusted_ / "devices.json", data.at("trusted_devices").dump());
private_file::Write(cache_ / "secure_cache_v3.enc",
std::string(reinterpret_cast<const char*>(&cache), sizeof(cache)));
@@ -303,7 +305,10 @@ bool SharedHostIdentity::Publish(const std::string& host_config, std::string& er
std::memset(cache.base.iv, 0, sizeof(cache.base.iv));
cache.checksum = credential_cache::Checksum(&cache, offsetof(credential_cache::CacheV3, checksum));
const std::string identity_cache(reinterpret_cast<const char*>(&cache), sizeof(cache));
const auto identity = PrivateDocument(cache_ / "identity.json");
// CLI setup can publish an installation that has not run GUI migration yet.
const auto identity = PrivateDocument(
std::filesystem::exists(trusted_ / "identity.json")
? trusted_ / "identity.json" : cache_ / "identity.json");
const auto key = identity.at("trust_private_key").get<std::string>();
Require(Unhex(key).size() == 32, "Invalid device identity key");
const auto devices = PrivateDocument(trusted_ / "devices.json");