[fix] distinguish trusted device authentication failures

This commit is contained in:
dijunkun committed 2026-11-13 10:35:24 +08:00
1 parent 9d61aacffe
commit 7d8daa27eb
4 files changed
+67 -18

No files matched your search

@@ -45,9 +45,16 @@ bool GuiRuntime::BeginControllerTrust(const std::string& id, PeerEventHandler* e
state.pending = required;
state.deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10);
try {
if (!trusted_devices_) state.invalid = true;
else state.nonce = TrustedDevices::Nonce();
} catch (...) { state.invalid = true; }
if (!trusted_devices_) {
state.invalid = true;
state.failure_reason = "identity_unavailable";
} else {
state.nonce = TrustedDevices::Nonce();
}
} catch (...) {
state.invalid = true;
state.failure_reason = "challenge_generation_failed";
}
controller_trust_[id] = std::move(state);
return !required;
}
@@ -112,8 +119,12 @@ bool GuiRuntime::HandleDeviceTrustMessage(const std::string& id, PeerEventHandle
if (state.invalid || state.verified || std::chrono::steady_clock::now() >= state.deadline)
return true;
// Accept once per fresh challenge; malformed/replayed proofs grant nothing.
state.verified = TrustedDevices::Verify(message, state.nonce);
if (!state.verified) state.invalid = true;
TrustedDevices::ProofFailure failure;
state.verified = TrustedDevices::Verify(message, state.nonce, &failure);
if (!state.verified) {
state.invalid = true;
state.failure_reason = TrustedDevices::ProofFailureReason(failure);
}
}
} catch (const std::exception&) {
// Untrusted protocol input never escapes the transport callback.
@@ -128,6 +139,7 @@ void GuiRuntime::TickControllerTrust() {
std::string nonce;
int kind;
std::string host_info;
const char* rejection_reason = nullptr;
};
std::vector<Action> actions;
const auto now = std::chrono::steady_clock::now();
@@ -140,9 +152,12 @@ void GuiRuntime::TickControllerTrust() {
const bool trusted = state.IsTrusted(trusted_devices_.get());
if (required && (state.invalid || (state.verified && !trusted) ||
(!state.verified && now >= state.deadline))) {
const char* reason = state.invalid
? (state.failure_reason ? state.failure_reason : "invalid_proof")
: (state.verified ? "public_key_not_trusted" : "proof_timeout");
state.invalid = true;
state.closing = true;
actions.push_back({id, state.events, {}, -1});
actions.push_back({id, state.events, {}, -1, {}, reason});
} else if (state.pending && (!required || trusted)) {
state.pending = false;
actions.push_back({id, state.events, {}, 1, std::exchange(state.early_host_info, {})});
@@ -160,7 +175,8 @@ void GuiRuntime::TickControllerTrust() {
auto callback = events->EnterCallback();
if (!callback) continue;
if (action.kind < 0) {
LOG_WARN("Reject controller: trusted device verification required");
LOG_WARN("Reject controller: trusted device verification required; reason={}",
action.rejection_reason);
transfers_.Cancel(events.get(), action.id);
remote_input_.Disconnect(action.id);
CloseServerController(action.id);
@@ -140,6 +140,7 @@ class GuiRuntime : protected gui_detail::GuiState {
std::string nonce;
std::optional<TrustedDevices::Device> verified;
std::string early_host_info;
const char* failure_reason = nullptr;
std::chrono::steady_clock::time_point deadline{}, next_challenge{};
bool pending = false, invalid = false, closing = false, capable = false;
bool IsTrusted(const TrustedDevices* devices) const {
@@ -202,12 +202,18 @@ std::string TrustedDevices::Sign(std::string_view text) const {
}
std::optional<TrustedDevices::Device> TrustedDevices::Verify(
const nlohmann::json& proof, const std::string& nonce) {
const nlohmann::json& proof, const std::string& nonce, ProofFailure* failure) {
if (failure) *failure = ProofFailure::None;
const auto reject = [failure](ProofFailure reason) -> std::optional<Device> {
if (failure) *failure = reason;
return std::nullopt;
};
try {
if (proof.at("version") != 1 || proof.at("nonce") != nonce || !IsHex(nonce, 32))
return std::nullopt;
if (proof.at("version") != 1) return reject(ProofFailure::UnsupportedVersion);
if (!IsHex(nonce, 32)) return reject(ProofFailure::Malformed);
if (proof.at("nonce") != nonce) return reject(ProofFailure::NonceMismatch);
Device device{proof.at("public_key"), proof.at("name"), proof.at("device_id")};
if (!Label(device.name) || !Label(device.device_id)) return std::nullopt;
if (!Label(device.name) || !Label(device.device_id)) return reject(ProofFailure::Malformed);
const auto public_key = Unhex(device.public_key, 32);
const auto signature = Unhex(proof.at("signature"), 64);
std::unique_ptr<EVP_PKEY, decltype(&EVP_PKEY_free)> key(
@@ -215,12 +221,28 @@ std::optional<TrustedDevices::Device> TrustedDevices::Verify(
EVP_PKEY_free);
Context context(EVP_MD_CTX_new(), EVP_MD_CTX_free);
const auto text = Transcript(nonce, device);
if (key && context && EVP_DigestVerifyInit(context.get(), nullptr, nullptr, nullptr, key.get()) == 1 &&
EVP_DigestVerify(context.get(), signature.data(), signature.size(),
reinterpret_cast<const uint8_t*>(text.data()), text.size()) == 1)
return device;
} catch (const std::exception&) {}
return std::nullopt;
if (!key || !context ||
EVP_DigestVerifyInit(context.get(), nullptr, nullptr, nullptr, key.get()) != 1)
return reject(ProofFailure::CryptoUnavailable);
const int verified = EVP_DigestVerify(context.get(), signature.data(), signature.size(),
reinterpret_cast<const uint8_t*>(text.data()), text.size());
if (verified == 1) return device;
return reject(verified == 0 ? ProofFailure::InvalidSignature : ProofFailure::CryptoUnavailable);
} catch (const std::exception&) {
return reject(ProofFailure::Malformed);
}
}
const char* TrustedDevices::ProofFailureReason(ProofFailure failure) {
switch (failure) {
case ProofFailure::None: return "none";
case ProofFailure::Malformed: return "malformed_proof";
case ProofFailure::UnsupportedVersion: return "unsupported_proof_version";
case ProofFailure::NonceMismatch: return "nonce_mismatch";
case ProofFailure::InvalidSignature: return "invalid_signature";
case ProofFailure::CryptoUnavailable: return "crypto_unavailable";
}
return "invalid_proof";
}
bool TrustedDevices::Contains(const std::string& public_key) const {
+11 -1
View File
@@ -27,6 +27,14 @@ class TrustedDevices {
std::string public_key, name, device_id;
bool operator==(const Device&) const = default;
};
enum class ProofFailure {
None,
Malformed,
UnsupportedVersion,
NonceMismatch,
InvalidSignature,
CryptoUnavailable
};
TrustedDevices(const std::filesystem::path& directory,
const std::filesystem::path& cache,
@@ -40,7 +48,9 @@ class TrustedDevices {
nlohmann::json Prove(const std::string& nonce,
const std::string& device_id, const std::string& name) const;
static std::optional<Device> Verify(const nlohmann::json& proof,
const std::string& nonce);
const std::string& nonce, ProofFailure* failure = nullptr);
// Stable diagnostic codes contain no peer-supplied data or key material.
static const char* ProofFailureReason(ProofFailure failure);
static constexpr size_t kMaxPairingInfoSize = 2048;
std::string PairingInfo(const std::string& device_id, const std::string& name) const;
static std::optional<Device> ParsePairingInfo(std::string_view text);