3#include <QtCore/QMetaEnum>
4#include <QtCore/QMetaObject>
18 return QMetaEnum::fromType<SigningFailure::Reason>().valueToKey(
static_cast<int>(r));
24 : QObject(parent), _mavlinkChannel(channel)
26 _timeout.setSingleShot(
true);
27 connect(&_timeout, &QTimer::timeout,
this, &SigningController::_onTimeout);
28 _wallClockRefresh.setInterval(kWallClockRefreshInterval);
30 qCDebug(SigningControllerLog) <<
"SigningController ctor — channel" << _mavlinkChannel;
35 qCDebug(SigningControllerLog) <<
"SigningController dtor — channel" << _mavlinkChannel;
37 _wallClockRefresh.stop();
39 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
40 _autoDetectGuard.reset();
41 if (_op.kind == OpKind::Enable) {
48 _channel.
init(_mavlinkChannel, QByteArrayView(),
nullptr);
51void SigningController::_setOpLocked(PendingOp next)
53 _op = std::move(next);
54 QMetaObject::invokeMethod(
this, [
this]() { emit
stateChanged(); }, Qt::AutoConnection);
57void SigningController::_setWallClockRefresh(
bool on)
60 QMetaObject::invokeMethod(&_wallClockRefresh, on ?
"start" :
"stop", Qt::AutoConnection);
67 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
72 return State::Enabling;
74 return State::Disabling;
78 return _channel.
isEnabled() ? State::On : State::Off;
98 return (s == State::On) || (s == State::Disabling);
109 if (s == State::Enabling) {
110 return tr(
"Configuring…");
112 if (s == State::Disabling) {
113 return tr(
"Disabling…");
120 return detail.isEmpty() ? tr(
"On") : detail;
127 if (!snap.keyName.isEmpty() && snap.timestamp > 0) {
131 const bool ok = _channel.
init(_mavlinkChannel, QByteArrayView(),
nullptr);
133 _setWallClockRefresh(
false);
138 const QString& keyNameHint)
142 _setWallClockRefresh(_channel.
isEnabled());
150 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
151 if (_isPendingLocked()) {
152 qCWarning(SigningControllerLog)
153 <<
"[ch" << _mavlinkChannel <<
"] enable rejected: operation already pending";
154 return SigningFailure{FailReason::VehicleUnreachable, tr(
"Signing operation already pending")};
159 const QByteArrayView keyView(
reinterpret_cast<const char*
>(keyBytes.data()), keyBytes.size());
161 if (!_channel.
init(_mavlinkChannel, keyView,
163 persisted, kName,
false)) {
164 qCWarning(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] enable rejected: signing init failed";
165 return SigningFailure{FailReason::InitFailed, tr(
"Failed to install signing for pending verification")};
169 _setOpLocked(PendingOp{
170 .kind = OpKind::Enable,
171 .expectedSysId = expectedSysId,
173 .keyBytes = keyBytes,
177 _timeout.setInterval(_effectiveTimeout());
179 _setWallClockRefresh(
true);
180 qCDebug(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] enable pending — key" << kName <<
"sysid"
181 << expectedSysId <<
"timeout" << kTimeout <<
"ms";
188 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
189 if (_isPendingLocked()) {
190 qCWarning(SigningControllerLog)
191 <<
"[ch" << _mavlinkChannel <<
"] disable rejected: operation already pending";
192 return SigningFailure{FailReason::VehicleUnreachable, tr(
"Signing operation already pending")};
197 qCWarning(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] disable rejected: channel not signing";
198 return SigningFailure{FailReason::VehicleUnreachable, tr(
"Channel not signing — cannot disable")};
201 _setOpLocked(PendingOp{
202 .kind = OpKind::Disable,
203 .expectedSysId = expectedSysId,
208 _timeout.setInterval(_effectiveTimeout());
210 qCDebug(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] disable pending — sysid" << expectedSysId
211 <<
"timeout" << kTimeout <<
"ms";
218 QMetaObject::invokeMethod(
this, [
this]() { emit
stateChanged(); }, Qt::AutoConnection);
223 bool burstFired =
false;
224 uint8_t burstCount = 0;
225 bool pendingEnable =
false;
227 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
228 burstFired = _badSigBurst.
record();
229 burstCount = _badSigBurst.
count();
230 pendingEnable = (_op.kind == OpKind::Enable);
234 const QString detail =
236 ? tr(
"MAVLink signing: %1 consecutive bad signatures while enabling — the chosen key likely "
237 "does not match the vehicle's stored key. Verify the key on the vehicle, then retry.")
239 : tr(
"MAVLink signing: %1 consecutive bad signatures on this link — wrong key or vehicle clock "
242 QMetaObject::invokeMethod(
this, [
this, detail]() { emit
alertRaised(detail); }, Qt::AutoConnection);
248 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
249 _badSigBurst.
reset();
252 bool autoDetected =
false;
257 if (!detected.isEmpty()) {
258 QMetaObject::invokeMethod(
this, [
this, detected]() { emit
keyAutoDetected(detected); }, Qt::AutoConnection);
264 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
265 _handleFsmFrameLocked(message);
273 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
274 _badSigBurst.
reset();
279 if (!_isPendingLocked()) {
282 if (message.sysid != _op.expectedSysId) {
287 if (message.msgid == MAVLINK_MSG_ID_STATUSTEXT) {
288 mavlink_statustext_t st;
289 mavlink_msg_statustext_decode(&message, &st);
290 const QString text = QString::fromLatin1(st.text, qstrnlen(st.text,
sizeof(st.text)));
291 const bool errorSeverity = (st.severity <= MAV_SEVERITY_ERROR);
292 const bool mentionsSigning = text.contains(QLatin1String(
"signing"), Qt::CaseInsensitive);
293 if (errorSeverity && mentionsSigning) {
294 _failLocked(FailReason::InitFailed, tr(
"Vehicle rejected signing change: %1").arg(text));
299 if (_op.kind == OpKind::Enable) {
304 qCWarning(SigningControllerLog)
305 <<
"[ch" << _mavlinkChannel
306 <<
"] pending-enable HEARTBEAT signature did not verify against committed key";
311 }
else if (_op.kind == OpKind::Disable) {
313 _op.unsignedSeen =
true;
319void SigningController::_confirmLocked()
322 QMetaObject::invokeMethod(&_timeout,
"stop", Qt::AutoConnection);
324 if (_op.kind == OpKind::Enable) {
325 const QString confirmedName = _op.keyName;
328 const QString detail = tr(
"Signing confirmation received but local activation failed");
329 qCCritical(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"]" << detail;
330 _failLocked(FailReason::InitFailed, detail);
333 qCDebug(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] enable confirmed — key" << confirmedName;
335 QMetaObject::invokeMethod(
336 this, [
this, confirmedName]() { emit
signingConfirmed(confirmedName); }, Qt::AutoConnection);
337 }
else if (_op.kind == OpKind::Disable) {
338 qCDebug(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] disable confirmed";
339 _completeDisableSuccessLocked();
343void SigningController::_completeDisableSuccessLocked()
345 _channel.
init(_mavlinkChannel, QByteArrayView(),
nullptr);
347 QMetaObject::invokeMethod(
this, [
this]() { emit
signingConfirmed(QString{}); }, Qt::AutoConnection);
350void SigningController::_failLocked(FailReason reason,
const QString& detail,
bool cancelled)
352 QMetaObject::invokeMethod(&_timeout,
"stop", Qt::AutoConnection);
355 if (_op.kind == OpKind::Disable && _op.unsignedSeen && !cancelled) {
356 _completeDisableSuccessLocked();
361 QString effectiveDetail = detail;
363 if (_op.kind == OpKind::Enable) {
364 _channel.
init(_mavlinkChannel, QByteArrayView(),
nullptr);
365 }
else if (_op.kind == OpKind::Disable) {
368 effectiveReason = FailReason::VehicleUnreachable;
370 tr(
"Signing disable not confirmed — vehicle is unreachable or still requires signed messages. Local "
371 "signing remains enabled.");
374 qCWarning(SigningControllerLog) <<
"[ch" << _mavlinkChannel
375 <<
"] signing operation failed:" << failReasonName(effectiveReason)
380 QMetaObject::invokeMethod(
this, [
this, failure]() { emit
signingFailed(failure); }, Qt::AutoConnection);
385 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
386 if (!_isPendingLocked()) {
389 const QString effectiveDetail =
391 ? tr(
"Signing operation cancelled — primary link changed before vehicle confirmation")
393 _failLocked(FailReason::VehicleUnreachable, effectiveDetail,
true);
396void SigningController::_onTimeout()
398 QMutexLocker<QRecursiveMutex> locker(&_fsmMutex);
399 if (!_isPendingLocked()) {
402 qCDebug(SigningControllerLog) <<
"[ch" << _mavlinkChannel <<
"] timeout fired —"
403 << (_op.kind == OpKind::Enable ?
"enable" :
"disable") <<
"not confirmed";
404 const QString detail = (_op.kind == OpKind::Enable) ? tr(
"Signing setup not confirmed by vehicle (timeout)")
405 : tr(
"Signing disable not confirmed by vehicle (timeout)");
406 _failLocked(FailReason::Timeout, detail);
409void SigningController::_clearLocked()
411 _autoDetectGuard.reset();
413 _setOpLocked(PendingOp{});
415 _setWallClockRefresh(_channel.
isEnabled());
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static MAVLinkSigningKeys * instance()
void recordTimestamp(const QString &name, uint64_t ts)
Update in-memory + persisted last-timestamp for name. Monotonic — older values are dropped.
QString tryDetectKey(SigningController *controller, const mavlink_message_t &message)
Try every stored key against message's signature; on match, configures channel and returns the key na...
uint64_t lastTimestamp(const QString &name) const
Last persisted signing timestamp for name, or 0 if unknown / no entry.
bool record()
Returns true on the rising-edge crossing into >= threshold.
bool refreshOutgoingTimestamp()
bool init(mavlink_channel_t channel, QByteArrayView key, mavlink_accept_unsigned_t callback, uint64_t persistedTimestamp=0, const QString &keyName={}, bool signOutgoing=true)
bool setAcceptUnsignedCallback(mavlink_accept_unsigned_t callback)
Swap the accept-unsigned callback without resetting the key. Returns false if signing isn't enabled.
QGC::AutoSuspendGuard suspendAutoDetect()
RAII handle that suspends auto-detect for the guard's lifetime; release is automatic on destruction.
void clearDetectCooldown()
bool consumeStatusTransition(mavlink_channel_t channel)
True if last_status changed since previous call; sole transition-detection source.
TimestampSnapshot currentTimestampAndName() const
Returns current timestamp and active key name. Returns {0, ""} when signing is not enabled.
void signingConfirmed(const QString &keyName)
Emitted exactly once per begin*() on success. keyName is the enabled key, or empty for disable.
SigningController(mavlink_channel_t channel, QObject *parent=nullptr)
std::optional< SigningFailure > tryBeginEnable(uint8_t expectedSysId, const QString &keyName, const MAVLinkSigning::SigningKey &keyBytes)
Begin pending-enable. Caller must send SETUP_SIGNING only on nullopt; outcome arrives via signingConf...
std::optional< SigningFailure > tryBeginDisable(uint8_t expectedSysId)
Atomic check-and-commit for disable; same contract as tryBeginEnable.
bool initSigningImmediate(QByteArrayView key, MAVLinkSigning::UnsignedAcceptancePolicy policy, const QString &keyNameHint={})
Bypasses the FSM; used by tests and auto-detect. Non-empty keyNameHint seeds the persisted timestamp.
bool processFrame(bool framingOk, const mavlink_message_t &message)
Per-frame entry point; drives burst alerts, auto-detect, and the FSM. Returns true on auto-detect.
QString statusText() const
SigningFailure::Reason FailReason
void cancelPending(const QString &detail={})
void alertRaised(const QString &detail)
~SigningController() override
SigningStatus status() const
void keyAutoDetected(const QString &keyName)
void signingFailed(SigningFailure failure)
Emitted exactly once per begin*() on failure (timeout, init error, cancel, re-entry).
Reason a signing operation failed. Used by SigningController error path and Vehicle::signingFailed.
std::array< uint8_t, kSigningKeySize > SigningKey
std::array avoids QByteArray COW detach so secureZero() actually wipes the bytes.
QString signingStatusString(mavlink_channel_t channel)
bool verifySignature(QByteArrayView key, const mavlink_message_t &message)
Verify a key against a signed message's signature.
bool isMessageSigned(const mavlink_message_t &message)
Returns true if the message has a MAVLink2 signature.
mavlink_accept_unsigned_t callbackForPolicy(UnsignedAcceptancePolicy policy)
Maps a high-level policy to the underlying libmavlink callback.
void logSigningFailure(mavlink_channel_t channel)
void secureZero(void *data, size_t size)