Skip to content

fix(MAVLink/Signing): wall-clock timestamp refresh, re-sign at send, drop qtkeychain keystore#14430

Merged
HTRamsey merged 1 commit into
mavlink:masterfrom
HTRamsey:fix/signing-timestamp-wallclock-refresh
Jun 6, 2026
Merged

fix(MAVLink/Signing): wall-clock timestamp refresh, re-sign at send, drop qtkeychain keystore#14430
HTRamsey merged 1 commit into
mavlink:masterfrom
HTRamsey:fix/signing-timestamp-wallclock-refresh

Conversation

@HTRamsey

@HTRamsey HTRamsey commented May 22, 2026

Copy link
Copy Markdown
Member

Summary

Hardens MAVLink signing so QGC reliably connects to an already-running, signing-enabled vehicle, and simplifies key storage. Fixes #14375 (GCS fails to connect when SITL is already running — OLD_TIMESTAMP rejection).

Root cause

SigningChannel::init() seeds _signing.timestamp from wall clock once. After that, libmavlink's mavlink_sign_packet only advances the value by +1 per outbound packet — it does not track real time. Two paths then drift behind wall clock and get rejected by peers that pin signing.timestamp to wall clock (ArduPilot's GCS_Signing::update_signing_timestamp):

  1. Idle outbound — QGC starts, sits idle detecting the vehicle, then sends its first command minutes later. The wire timestamp is still near the init() value (~3-minute offset in MAVLink signing: GCS fails to connect when SITL is already running #14375).
  2. Cached resendVehicle::sendMessageMultiple caches the already-signed bytes and re-emits them across retries, so the frozen signature timestamp drifts behind real time.

Fix

  • Wall-clock refreshSigningChannel::refreshOutgoingTimestamp() bumps _signing.timestamp up to current wall clock under the write lock (no-op when disabled or already ahead). SigningController drives it from a 1 Hz QTimer, well under the receiver's 6 s MAVLINK_SIGNING_TIMESTAMP_LIMIT. The timer is now gated on channel-signing state — it runs only while a channel is actually signing, not for every link's lifetime.
  • Re-sign at sendSigningChannel::signOutgoing() re-signs a cached outgoing message in place with a current, monotonic timestamp immediately before it hits the wire, fixing the cached-resend drift. The secret key never leaves the signing layer.
  • Keystore simplification — drops QGCKeychain (qtkeychain) and the QLockFile cross-process guard; keys persist via QSettings with an explicit sync(). Removes the qtkeychain dependency and its install plumbing.
  • FSM refactorVehicleSigningController splits handler wiring from transmit (_wireConfirmHandlers once per op, _sendAndStartRetransmit for the wire), so the retransmit path can't double-deliver the one-shot confirm/fail connections.

Test plan

  • SigningTest — new regressions: _testRefreshOutgoingTimestamp (3-minute stall catch-up, no-op when ahead/disabled), _testSignOutgoingRefreshesCachedTimestamp (cached-resend re-sign stays monotonic and still verifies), _testKeyStorePersistRoundTrip (QSettings persist/reload/remove).
  • SigningControllerTest — new: _testWallClockTimerRefreshesAfterEnable / _testWallClockTimerStoppedWhenIdle (timer gating). Burst-alert / auto-detect / state-machine unchanged.
  • MockLinkSigningTest — new: _testEnableDisableReEnableCycle (handlers re-wire per op; no spurious failure). Enable/disable FSM unchanged.
  • Added a test-only SigningController::setTimeoutForTesting() seam so timeout-path tests no longer pay the production 5 s wait (suite runtime 35 s → 17 s).
  • Manual: ArduPilot SITL with signing key, start SITL → wait 3 min → start QGC → confirm initialisation commands succeed.

@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 56.48148% with 47 lines in your changes missing coverage. Please review.
✅ Project coverage is 28.04%. Comparing base (f29efd3) to head (a8ace2c).
⚠️ Report is 44 commits behind head on master.

Files with missing lines Patch % Lines
src/Vehicle/VehicleSigningController.cc 34.48% 8 Missing and 11 partials ⚠️
src/MAVLink/Signing/MAVLinkSigning.cc 68.18% 1 Missing and 6 partials ⚠️
src/MAVLink/Signing/MAVLinkSigningKeys.cc 0.00% 1 Missing and 6 partials ⚠️
src/MAVLink/Signing/SigningChannel.cc 75.00% 0 Missing and 5 partials ⚠️
src/MAVLink/Signing/SigningController.cc 75.00% 0 Missing and 4 partials ⚠️
src/Comms/LinkInterface.cc 50.00% 0 Missing and 3 partials ⚠️
src/MAVLink/Signing/SigningController.h 83.33% 0 Missing and 1 partial ⚠️
src/Vehicle/MultiVehicleManager.cc 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master   #14430      +/-   ##
==========================================
+ Coverage   25.47%   28.04%   +2.57%     
==========================================
  Files         769      766       -3     
  Lines       65912    66277     +365     
  Branches    30495    30585      +90     
==========================================
+ Hits        16788    18589    +1801     
+ Misses      37285    34915    -2370     
- Partials    11839    12773     +934     
Flag Coverage Δ
unittests 28.04% <56.48%> (+2.57%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/Comms/LinkInterface.h 62.50% <ø> (ø)
src/MAVLink/Signing/MAVLinkSigning.h 60.00% <ø> (ø)
src/MAVLink/Signing/MAVLinkSigningKeys.h 77.77% <ø> (ø)
src/MAVLink/Signing/SigningChannel.h 66.66% <ø> (ø)
src/Utilities/Platform/Platform.cc 24.56% <ø> (-2.11%) ⬇️
src/Vehicle/Vehicle.cc 25.36% <100.00%> (+3.96%) ⬆️
src/Vehicle/VehicleSigningController.h 50.00% <ø> (ø)
src/MAVLink/Signing/SigningController.h 90.90% <83.33%> (-9.10%) ⬇️
src/Vehicle/MultiVehicleManager.cc 46.55% <0.00%> (-0.31%) ⬇️
src/Comms/LinkInterface.cc 48.64% <50.00%> (+0.11%) ⬆️
... and 5 more

... and 202 files with indirect coverage changes


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update c17948f...a8ace2c. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Passed View
Android Passed View

All builds passed.

Pre-commit

Check Status Details
pre-commit Failed (non-blocking) View

Pre-commit hooks: 2 passed, 45 failed, 7 skipped.

Test Results

linux-coverage: 89 passed, 0 skipped
Total: 89 passed, 0 skipped

Code Coverage

Coverage Baseline Change
61.9% 62.0% -0.1%

Artifact Sizes

Artifact Size Δ from master
QGroundControl 216.77 MB -4.61 MB (decrease)
QGroundControl-aarch64 176.60 MB -0.06 MB (decrease)
QGroundControl-installer-AMD64 133.54 MB -1.14 MB (decrease)
QGroundControl-installer-AMD64-ARM64 76.36 MB -1.13 MB (decrease)
QGroundControl-installer-ARM64 104.89 MB -1.14 MB (decrease)
QGroundControl-linux 186.78 MB -147.92 MB (decrease)
QGroundControl-mac 186.78 MB -0.08 MB (decrease)
QGroundControl-windows 186.79 MB -0.08 MB (decrease)
QGroundControl-x86_64 172.15 MB -0.17 MB (decrease)
Total size decreased by 156.32 MB

Updated: 2026-06-05 18:31:42 UTC • Triggered by: Android

@TomZanna

TomZanna commented Jun 2, 2026

Copy link
Copy Markdown

Hi, thanks for working on this! I've been testing this draft for a week, but the bug is still present.
I ran some Wireshark captures and noticed that connection works fine for the first few seconds of startup. After that, a timestamp drift appears between the SITL's HEARTBEAT and QGC's REQUEST_DATA_STREAM.
Since the fix introduces a 1 Hz timer and locks, could this be a race condition? For example, two different threads communicating with the same socket but not sharing or updating the timestamp correctly?

Tested on build ID 81416f0 timestamp 2026-05-22T16:29:33-04:00 (artifact of this PR's action)

@HTRamsey HTRamsey force-pushed the fix/signing-timestamp-wallclock-refresh branch from 9b62660 to 582906c Compare June 4, 2026 22:01
@TomZanna

TomZanna commented Jun 5, 2026

Copy link
Copy Markdown

Just tested the new commit. The bug seems fixed!

@HTRamsey HTRamsey force-pushed the fix/signing-timestamp-wallclock-refresh branch 2 times, most recently from d3b98f3 to 50d7f5a Compare June 5, 2026 16:40
@HTRamsey HTRamsey changed the title fix(MAVLink/Signing): refresh signing timestamp from wall clock fix(MAVLink/Signing): wall-clock timestamp refresh, re-sign at send, drop qtkeychain keystore Jun 5, 2026
…keychain

mavlink#14375 — outgoing signing timestamp drift
libmavlink only increments signing->timestamp by +1 per packet, and
cached/retried sends (Vehicle::sendMessageMultiple) reuse the same
already-encoded bytes, so the wire timestamp freezes and drifts behind
wall clock. Peers that pin signing.timestamp to wall clock (ArduPilot)
then reject the packet as OLD_TIMESTAMP.

Re-sign at the single send chokepoint: SigningChannel::signOutgoing()
stamps a current, monotonic timestamp and recomputes the signature just
before mavlink_msg_to_send_buffer() in
Vehicle::sendMessageOnLinkThreadSafe(). The secret key never leaves the
signing layer. signMessage() and verifySignature() share one hash helper
so the wire format cannot diverge. The 1 Hz refresh timer is kept for the
re-encode paths (command retries).

mavlink#14447 — keystore QLockFile test hang
MAVLinkSigningKeys guarded its store with a cross-process QLockFile whose
stale-lock heuristic fails on macOS after a SIGKILL'd run, blocking 5 s
per write. RunGuard already enforces single-instance, so the lock is
redundant; remove it.

mavlink#14466 — qtkeychain libgcrypt link failure
qtkeychain's Linux libsecret backend pulls in libgcrypt/libgpg-error,
whose cross-distro version skew breaks AppImage and source builds. Remove
qtkeychain and the QGCKeychain wrapper entirely; store signing key bytes
directly in the existing QSettings store (file-permission protected)
under a keys/ subgroup. Drops the libgcrypt early-init and the
libgcrypt/libsecret apt dependencies.

Fixes mavlink#14375
Fixes mavlink#14447
Fixes mavlink#14466
@HTRamsey HTRamsey force-pushed the fix/signing-timestamp-wallclock-refresh branch from 50d7f5a to a8ace2c Compare June 5, 2026 17:09
@HTRamsey HTRamsey marked this pull request as ready for review June 5, 2026 18:48
Copilot AI review requested due to automatic review settings June 5, 2026 18:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@HTRamsey HTRamsey merged commit d31c2ae into mavlink:master Jun 6, 2026
43 of 44 checks passed
@HTRamsey HTRamsey deleted the fix/signing-timestamp-wallclock-refresh branch June 6, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MAVLink signing: GCS fails to connect when SITL is already running

3 participants