diff --git a/src/MAVLink/Signing/SigningChannel.cc b/src/MAVLink/Signing/SigningChannel.cc index dcfd43bf5acd..4e0a3254d35f 100644 --- a/src/MAVLink/Signing/SigningChannel.cc +++ b/src/MAVLink/Signing/SigningChannel.cc @@ -61,6 +61,20 @@ bool SigningChannel::init(mavlink_channel_t channel, QByteArrayView key, mavlink return true; } +bool SigningChannel::refreshOutgoingTimestamp() +{ + QWriteLocker locker(&_lock); + if (!_enabled) { + return false; + } + const uint64_t now = MAVLinkSigning::currentSigningTimestampTicks(); + if (now <= _signing.timestamp) { + return false; + } + _signing.timestamp = now; + return true; +} + SigningChannel::TimestampSnapshot SigningChannel::currentTimestampAndName() const { QReadLocker locker(&_lock); diff --git a/src/MAVLink/Signing/SigningChannel.h b/src/MAVLink/Signing/SigningChannel.h index 1bb0a2984963..9c61bd500274 100644 --- a/src/MAVLink/Signing/SigningChannel.h +++ b/src/MAVLink/Signing/SigningChannel.h @@ -52,6 +52,12 @@ class SigningChannel /// Returns current timestamp and active key name. Returns {0, ""} when signing is not enabled. TimestampSnapshot currentTimestampAndName() const; + /// Bump `_signing.timestamp` up to current wall clock; libmavlink only increments per-packet, so an + /// idle outbound path otherwise drifts behind wall clock and triggers OLD_TIMESTAMP rejections on + /// peers that pin signing.timestamp to wall clock (ArduPilot). No-op when not enabled or already ahead. + /// Returns true if the timestamp was advanced. + bool refreshOutgoingTimestamp(); + /// While suspended, tryDetectKey is suppressed to block stale-key installs during pending enable. bool isAutoDetectSuspended() const; diff --git a/src/MAVLink/Signing/SigningController.cc b/src/MAVLink/Signing/SigningController.cc index cc2a7f0cea9d..0e222fe32708 100644 --- a/src/MAVLink/Signing/SigningController.cc +++ b/src/MAVLink/Signing/SigningController.cc @@ -26,6 +26,9 @@ SigningController::SigningController(mavlink_channel_t channel, QObject* parent) _timeout.setSingleShot(true); _timeout.setInterval(kTimeout); connect(&_timeout, &QTimer::timeout, this, &SigningController::_onTimeout); + _wallClockRefresh.setInterval(kWallClockRefreshInterval); + connect(&_wallClockRefresh, &QTimer::timeout, this, [this]() { _channel.refreshOutgoingTimestamp(); }); + _wallClockRefresh.start(); qCDebug(SigningControllerLog) << "SigningController ctor — channel" << _mavlinkChannel; } @@ -33,6 +36,7 @@ SigningController::~SigningController() { qCDebug(SigningControllerLog) << "SigningController dtor — channel" << _mavlinkChannel; _timeout.stop(); + _wallClockRefresh.stop(); { QMutexLocker locker(&_fsmMutex); _autoDetectGuard.reset(); diff --git a/src/MAVLink/Signing/SigningController.h b/src/MAVLink/Signing/SigningController.h index b311b577caef..6968f1f37072 100644 --- a/src/MAVLink/Signing/SigningController.h +++ b/src/MAVLink/Signing/SigningController.h @@ -111,9 +111,15 @@ class SigningController : public QObject PendingOp _op; std::optional _autoDetectGuard; QTimer _timeout; + /// 1Hz catch-up of `_signing.timestamp` to wall clock; libmavlink only bumps per-packet so idle + /// outbound paths otherwise sign with a stale value and get rejected by peers that pin signing + /// timestamps to wall clock (mavlink/qgroundcontrol#14375). + QTimer _wallClockRefresh; static constexpr uint8_t kBadSignatureAlertThreshold = 3; QGC::EdgeTriggeredCounter _badSigBurst{kBadSignatureAlertThreshold}; static constexpr auto kTimeout = std::chrono::seconds(5); + /// Refresh interval well under MAVLINK_SIGNING_TIMESTAMP_LIMIT (6s default) on the receiver side. + static constexpr auto kWallClockRefreshInterval = std::chrono::seconds(1); }; diff --git a/test/MAVLink/Signing/SigningTest.cc b/test/MAVLink/Signing/SigningTest.cc index 302bf5afc272..e43f4861d107 100644 --- a/test/MAVLink/Signing/SigningTest.cc +++ b/test/MAVLink/Signing/SigningTest.cc @@ -691,4 +691,31 @@ void SigningTest::_testTryDetectKeyInstallsSecureCallback() signingKeys->removeAllKeys(); } +void SigningTest::_testRefreshOutgoingTimestamp() +{ + SigningChannel ch; + const QByteArray rawKey(32, '\x77'); + QVERIFY(ch.init(MAVLINK_COMM_0, rawKey, MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)); + + const mavlink_signing_t* const signing = mavlink_get_channel_status(MAVLINK_COMM_0)->signing; + QVERIFY(signing); + + // Simulate the issue #14375 stall: init was 3 minutes ago, no outbound packets sent since. + constexpr uint64_t kThreeMinutesTicks = 3ULL * 60 * 100'000; // 10µs ticks + const uint64_t stale = MAVLinkSigning::currentSigningTimestampTicks() - kThreeMinutesTicks; + const_cast(signing)->timestamp = stale; + + QVERIFY(ch.refreshOutgoingTimestamp()); + QVERIFY(signing->timestamp >= MAVLinkSigning::currentSigningTimestampTicks() - 100'000); // within 1s of wall clock + + // Second call with no wall-clock advance should be a no-op (already at/ahead of wall clock). + const uint64_t afterFirst = signing->timestamp; + const_cast(signing)->timestamp = afterFirst + (10ULL * 100'000); // 10s into the future + QVERIFY(!ch.refreshOutgoingTimestamp()); + QCOMPARE(signing->timestamp, afterFirst + (10ULL * 100'000)); + + QVERIFY(ch.init(MAVLINK_COMM_0, QByteArrayView(), nullptr)); + QVERIFY(!ch.refreshOutgoingTimestamp()); // disabled → no-op +} + UT_REGISTER_TEST(SigningTest, TestLabel::Unit) diff --git a/test/MAVLink/Signing/SigningTest.h b/test/MAVLink/Signing/SigningTest.h index a44879774c70..b300fe0d65d1 100644 --- a/test/MAVLink/Signing/SigningTest.h +++ b/test/MAVLink/Signing/SigningTest.h @@ -34,4 +34,5 @@ private slots: void _testInitSigningWithPersistedTimestamp(); void _testStripSignatureForRetransmitProducesValidCrc(); void _testTryDetectKeyInstallsSecureCallback(); + void _testRefreshOutgoingTimestamp(); };