Skip to content

fix(signing): defer local signing until vehicle confirms via signature verification#14268

Open
HTRamsey wants to merge 1 commit intomavlink:masterfrom
HTRamsey:fix/signing-deferred-confirmation
Open

fix(signing): defer local signing until vehicle confirms via signature verification#14268
HTRamsey wants to merge 1 commit intomavlink:masterfrom
HTRamsey:fix/signing-deferred-confirmation

Conversation

@HTRamsey
Copy link
Copy Markdown
Collaborator

@HTRamsey HTRamsey commented Apr 8, 2026

Summary

  • Replaces fire-and-forget SETUP_SIGNING with cryptographic confirmation: enable waits for a signed message verifiable against the pending key, disable waits for an unsigned heartbeat
  • Adds 3-second timeout with user notification on rejection/no-response
  • Temporarily broadens accept-unsigned callback during pending disable so unsigned heartbeats reach QGC
  • Fixes pre-existing bug in MAVLinkSigning::verifySignature() that read CRC from wrong location in parsed messages
  • Makes MockLink actually sign outgoing messages when signing is enabled (needed for confirmation flow)

Context

Alternative to #14251. That PR used "any heartbeat = accepted" which false-positives because heartbeats prove the vehicle is alive, not that it accepted the key. This implementation uses actual signature verification as proof.

Fixes #14250

Test plan

  • Enable signing with accepted key: QGC confirms via signed heartbeat, enables local signing
  • Disable signing on accepting vehicle: QGC confirms via unsigned heartbeat, disables local signing
  • Enable signing with unreachable vehicle: 3-second timeout fires, signing stays off, user notified
  • Manual test with real vehicle

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 8, 2026 14:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates QGroundControl’s MAVLink signing enable/disable flow to avoid communication loss by deferring local signing state changes until the vehicle provides cryptographic proof (or unsigned proof for disable), and fixes signature verification CRC handling. It also expands MockLink/test coverage to exercise the new confirmation flow, and includes additional unrelated onboard log UI/controller refactors.

Changes:

  • Defer local signing enable/disable until confirmation is observed (signed message verifies against pending key; disable confirmed by unsigned heartbeat), with a 3s timeout + user notification.
  • Fix MAVLinkSigning::verifySignature() to hash the parsed-message CRC from message.ck[] instead of reading past payload.
  • Update MockLink to actually sign outgoing messages when signing is enabled; expand signing tests (including a timeout test).

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/MAVLink/MockLinkSigningTest.h Adds a new signing enable timeout test slot.
test/MAVLink/MockLinkSigningTest.cc Updates signing tests for deferred confirmation; adds timeout scenario.
src/Vehicle/Vehicle.h Adds pending signing operation state + confirmation timer members.
src/Vehicle/Vehicle.cc Implements deferred signing confirmation logic, timeout handling, and temporary accept-unsigned behavior during disable.
src/MAVLink/MAVLinkSigning.h Adds pending-disable accept-unsigned callback and callback setter API.
src/MAVLink/MAVLinkSigning.cc Implements pending-disable accept-unsigned set and fixes signature verification CRC source.
src/Comms/MockLink/MockLink.cc Enables/disables real MAVLink channel signing in MockLink when SETUP_SIGNING is received.
src/AnalyzeView/OnboardLogs/OnboardLogPage.qml Avoids refresh on completion when no active vehicle; adds “select all” checkbox behavior.
src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc Re-enables debug logging in ctors/dtors.
src/AnalyzeView/OnboardLogs/OnboardLogController.h Removes unused compression API; adds retry constants and helper declaration.
src/AnalyzeView/OnboardLogs/OnboardLogController.cc Refactors download error handling/retries and factors MAVLink send boilerplate into a helper.
Comments suppressed due to low confidence (1)

src/AnalyzeView/OnboardLogs/OnboardLogController.cc:66

  • The PR description and title focus on signing confirmation, but this PR also makes substantial onboard log UI/controller changes. This increases review/rollback risk and makes it harder to reason about the signing fix. Consider splitting the onboard log work into a separate PR or explicitly documenting the additional scope in the PR description.
void OnboardLogController::_downloadToDirectory(const QString &dir)
{
    _receivedAllEntries();

    _downloadData.reset();

    _downloadPath = dir;
    if (_downloadPath.isEmpty()) {
        return;
    }

    if (!_downloadPath.endsWith(QDir::separator())) {
        _downloadPath += QDir::separator();
    }

    if (!QDir().mkpath(_downloadPath)) {
        qCWarning(OnboardLogControllerLog) << "Failed to create download directory:" << _downloadPath;
        return;
    }

    _setDownloading(true);
    _receivedAllData();
}

Comment thread src/Vehicle/Vehicle.cc Outdated
Comment thread src/Vehicle/Vehicle.cc Outdated
Comment thread src/Vehicle/Vehicle.cc
Comment on lines +4719 to +4727
if (_pendingSigningOp == PendingSigningOp::Enable) {
if (!MAVLinkSigning::initSigning(_pendingSigningChannel, _pendingSigningKeyBytes, MAVLinkSigning::insecureConnectionAcceptUnsignedCallback)) {
qCCritical(VehicleLog) << "Failed to initialize signing on channel" << _pendingSigningChannel;
} else {
_mavlinkSigning = true;
_mavlinkSigningKeyName = _pendingSigningKeyName;
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing confirmed by vehicle, key:" << _mavlinkSigningKeyName;
}
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

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

If MAVLinkSigning::initSigning fails during enable confirmation, the code only logs and then clears the pending op without notifying the user. This leaves the UI thinking enable is still possible but provides no feedback about why confirmation failed. Consider treating this as a failure path: cancel the pending op, restore any temporary state, and show an app message.

Copilot uses AI. Check for mistakes.
Comment thread test/MAVLink/MockLinkSigningTest.cc Outdated
Comment thread src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc
Comment thread src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc
Comment thread src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc
Comment thread src/AnalyzeView/OnboardLogs/OnboardLogEntry.cc
@mrpollo
Copy link
Copy Markdown
Member

mrpollo commented Apr 8, 2026

@HTRamsey let me know when this is ready for testing, and I can run a few tests with hardware

@HTRamsey
Copy link
Copy Markdown
Collaborator Author

HTRamsey commented Apr 8, 2026

Okay will do. FYI this is based on yours but just made a separate PR with some changes to make things quick and simple

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

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: 4 passed, 36 failed, 7 skipped.

Test Results

linux-coverage: 78 passed, 0 skipped
linux-sanitizers: 78 passed, 0 skipped
Total: 156 passed, 0 skipped

Code Coverage

Coverage: 58.2%

No baseline available for comparison

Artifact Sizes

Artifact Size
QGroundControl 247.89 MB
QGroundControl-aarch64 177.14 MB
QGroundControl-installer-AMD64 135.98 MB
QGroundControl-installer-AMD64-ARM64 78.58 MB
QGroundControl-installer-ARM64 107.33 MB
QGroundControl-linux 338.51 MB
QGroundControl-mac 188.92 MB
QGroundControl-windows 188.93 MB
QGroundControl-x86_64 163.39 MB
No baseline available for comparison

Updated: 2026-04-10 15:20:50 UTC • Triggered by: Android

@HTRamsey HTRamsey force-pushed the fix/signing-deferred-confirmation branch from 14781f6 to f5992cc Compare April 9, 2026 15:51
@mrpollo
Copy link
Copy Markdown
Member

mrpollo commented Apr 9, 2026

Cool, yeah, the biggest problem is that QGC could get out of sync with PX4. Once we figure that out, we should be good!

Additionally, at some point, it would be nice to add the option of loading a custom private key instead of generating one, and give users both options.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 43.08943% with 280 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (master@77a19a4). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/Vehicle/Vehicle.cc 39.63% 51 Missing and 48 partials ⚠️
src/MAVLink/MAVLinkSigningKeys.cc 40.29% 28 Missing and 52 partials ⚠️
src/MAVLink/MAVLinkSigning.cc 55.96% 34 Missing and 14 partials ⚠️
src/Comms/MAVLinkProtocol.cc 23.33% 16 Missing and 7 partials ⚠️
src/Utilities/Platform/QGCKeychain.cc 43.58% 0 Missing and 22 partials ⚠️
src/Comms/MockLink/MockLink.cc 33.33% 0 Missing and 4 partials ⚠️
src/QmlControls/QGroundControlQmlGlobal.cc 0.00% 3 Missing ⚠️
src/MAVLink/MAVLinkSigningKeys.h 75.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff            @@
##             master   #14268   +/-   ##
=========================================
  Coverage          ?   25.61%           
=========================================
  Files             ?      753           
  Lines             ?    68221           
  Branches          ?    31638           
=========================================
  Hits              ?    17478           
  Misses            ?    38044           
  Partials          ?    12699           
Flag Coverage Δ
unittests 25.61% <43.08%> (?)

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

Files with missing lines Coverage Δ
src/Comms/MAVLinkProtocol.h 25.00% <ø> (ø)
src/FirmwarePlugin/FirmwarePlugin.cc 19.33% <ø> (ø)
src/QmlControls/QGroundControlQmlGlobal.h 14.81% <ø> (ø)
src/Vehicle/Vehicle.h 36.69% <100.00%> (ø)
src/MAVLink/MAVLinkSigningKeys.h 75.00% <75.00%> (ø)
src/QmlControls/QGroundControlQmlGlobal.cc 8.23% <0.00%> (ø)
src/Comms/MockLink/MockLink.cc 49.66% <33.33%> (ø)
src/Utilities/Platform/QGCKeychain.cc 43.58% <43.58%> (ø)
src/Comms/MAVLinkProtocol.cc 28.93% <23.33%> (ø)
src/MAVLink/MAVLinkSigning.cc 59.23% <55.96%> (ø)
... and 2 more

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 77a19a4...64c7a66. 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.

@HTRamsey
Copy link
Copy Markdown
Collaborator Author

@mrpollo should be good to go

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.

Comment thread src/MAVLink/MAVLinkSigning.cc Outdated
Comment thread src/MAVLink/MAVLinkSigningKeys.cc Outdated
Comment thread src/UI/AppSettings/SigningKeyManager.qml Outdated
Comment thread src/Vehicle/Vehicle.cc Outdated
Comment thread src/Vehicle/Vehicle.cc
Comment on lines +4790 to +4801
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing confirmed, key:" << _mavlinkSigningKeyName;
}
} else if (_pendingSigningOp == PendingSigningOp::Disable) {
MAVLinkSigning::initSigning(_pendingSigningChannel, QByteArrayView(), nullptr);
_mavlinkSigning = false;
_mavlinkSigningKeyName.clear();
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing disable confirmed";
}

_clearPendingSigningState();
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

_confirmSigning() emits mavlinkSigningChanged() before _clearPendingSigningState(). Since mavlinkSigningPending is derived from _pendingSigningOp, listeners (QML/UI/tests) may observe the signal while the object is still “pending” and never get another notification when pending clears. Clear the pending state before emitting, or emit again after clearing so the UI reliably transitions out of the pending state on confirmation.

Suggested change
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing confirmed, key:" << _mavlinkSigningKeyName;
}
} else if (_pendingSigningOp == PendingSigningOp::Disable) {
MAVLinkSigning::initSigning(_pendingSigningChannel, QByteArrayView(), nullptr);
_mavlinkSigning = false;
_mavlinkSigningKeyName.clear();
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing disable confirmed";
}
_clearPendingSigningState();
qCDebug(VehicleLog) << "Signing confirmed, key:" << _mavlinkSigningKeyName;
}
} else if (_pendingSigningOp == PendingSigningOp::Disable) {
MAVLinkSigning::initSigning(_pendingSigningChannel, QByteArrayView(), nullptr);
_mavlinkSigning = false;
_mavlinkSigningKeyName.clear();
qCDebug(VehicleLog) << "Signing disable confirmed";
}
_clearPendingSigningState();
emit mavlinkSigningChanged();

Copilot uses AI. Check for mistakes.
Comment thread src/Vehicle/Vehicle.cc
Comment on lines +4781 to +4798
// Channel was captured at send time; link failover handled by tryDetectKey
if (_pendingSigningOp == PendingSigningOp::Enable) {
if (!MAVLinkSigning::initSigning(_pendingSigningChannel, _pendingSigningKeyBytes,
MAVLinkSigning::secureConnectionAcceptUnsignedCallback)) {
qCCritical(VehicleLog) << "Failed to init signing on channel" << _pendingSigningChannel;
qgcApp()->showAppMessage(tr("Signing confirmation received but local initialization failed"));
} else {
_mavlinkSigning = true;
_mavlinkSigningKeyName = _pendingSigningKeyName;
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing confirmed, key:" << _mavlinkSigningKeyName;
}
} else if (_pendingSigningOp == PendingSigningOp::Disable) {
MAVLinkSigning::initSigning(_pendingSigningChannel, QByteArrayView(), nullptr);
_mavlinkSigning = false;
_mavlinkSigningKeyName.clear();
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing disable confirmed";
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Signing is initialized/deinitialized on _pendingSigningChannel, which is captured when the request is sent. If the primary link changes (failover) before confirmation arrives, confirmation can be received on a different channel but local signing will still be enabled/disabled on the old channel, potentially breaking comms. Consider updating the target channel at confirmation time (e.g., use the current primary link’s channel, or the channel the confirming message arrived on) and/or cancel pending ops on primary-link changes.

Suggested change
// Channel was captured at send time; link failover handled by tryDetectKey
if (_pendingSigningOp == PendingSigningOp::Enable) {
if (!MAVLinkSigning::initSigning(_pendingSigningChannel, _pendingSigningKeyBytes,
MAVLinkSigning::secureConnectionAcceptUnsignedCallback)) {
qCCritical(VehicleLog) << "Failed to init signing on channel" << _pendingSigningChannel;
qgcApp()->showAppMessage(tr("Signing confirmation received but local initialization failed"));
} else {
_mavlinkSigning = true;
_mavlinkSigningKeyName = _pendingSigningKeyName;
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing confirmed, key:" << _mavlinkSigningKeyName;
}
} else if (_pendingSigningOp == PendingSigningOp::Disable) {
MAVLinkSigning::initSigning(_pendingSigningChannel, QByteArrayView(), nullptr);
_mavlinkSigning = false;
_mavlinkSigningKeyName.clear();
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing disable confirmed";
// Resolve the channel at confirmation time so signing follows the current primary link after failover.
const mavlink_channel_t signingChannel = _priorityLink ? _priorityLink->mavlinkChannel() : _pendingSigningChannel;
if (_pendingSigningOp == PendingSigningOp::Enable) {
if (!MAVLinkSigning::initSigning(signingChannel, _pendingSigningKeyBytes,
MAVLinkSigning::secureConnectionAcceptUnsignedCallback)) {
qCCritical(VehicleLog) << "Failed to init signing on channel" << signingChannel;
qgcApp()->showAppMessage(tr("Signing confirmation received but local initialization failed"));
} else {
_mavlinkSigning = true;
_mavlinkSigningKeyName = _pendingSigningKeyName;
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing confirmed, key:" << _mavlinkSigningKeyName << "channel:" << signingChannel;
}
} else if (_pendingSigningOp == PendingSigningOp::Disable) {
MAVLinkSigning::initSigning(signingChannel, QByteArrayView(), nullptr);
_mavlinkSigning = false;
_mavlinkSigningKeyName.clear();
emit mavlinkSigningChanged();
qCDebug(VehicleLog) << "Signing disable confirmed on channel" << signingChannel;

Copilot uses AI. Check for mistakes.
@HTRamsey HTRamsey force-pushed the fix/signing-deferred-confirmation branch 2 times, most recently from 1a04a1c to a77e52e Compare April 10, 2026 13:07
@HTRamsey HTRamsey force-pushed the fix/signing-deferred-confirmation branch from a77e52e to 64c7a66 Compare April 10, 2026 14:21
@mrpollo mrpollo self-assigned this Apr 15, 2026
@mrpollo mrpollo self-requested a review April 15, 2026 17:20
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.

fix(signing): SETUP_SIGNING rejection causes communication loss

3 participants