QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
MAVLinkSigningKeys.cc
Go to the documentation of this file.
2
3#include <QtCore/QCoreApplication>
4#include <QtCore/QCryptographicHash>
5#include <QtCore/QRandomGenerator>
6#include <QtCore/QSet>
7#include <QtCore/QSettings>
8#include <QtCore/QTimer>
9#include <QtNetwork/QPasswordDigestor>
10#include <array>
11
12#include "LinkInterface.h"
13#include "LinkManager.h"
14#include "MAVLinkSigning.h"
15#include "MultiVehicleManager.h"
16#include "QGCLoggingCategory.h"
17#include "QmlObjectListModel.h"
18#include "SecureMemory.h"
19#include "SigningController.h"
20#include "Vehicle.h"
22
23QGC_LOGGING_CATEGORY(MAVLinkSigningKeysLog, "MAVLink.SigningKeys")
24
25MAVLinkSigningKey::MAVLinkSigningKey(const QString& name, const MAVLinkSigning::SigningKey& keyBytes, QObject* parent)
26 : QObject(parent), _name(name), _keyBytes(keyBytes)
27{
28 qCDebug(MAVLinkSigningKeysLog) << "MAVLinkSigningKey ctor:" << _name;
29}
30
32{
33 QGC::secureZero(_keyBytes);
34 qCDebug(MAVLinkSigningKeysLog) << "MAVLinkSigningKey dtor:" << _name;
35}
36
37Q_APPLICATION_STATIC(MAVLinkSigningKeys, _mavlinkSigningKeysInstance);
38
40{
41 return _mavlinkSigningKeysInstance();
42}
43
44MAVLinkSigningKeys::MAVLinkSigningKeys(QObject* parent) : QObject(parent), _keys(new QmlObjectListModel(this))
45{
46 qCDebug(MAVLinkSigningKeysLog) << "MAVLinkSigningKeys ctor";
47
48 _load();
49
51 connect(mvm, &MultiVehicleManager::vehicleAdded, this, &MAVLinkSigningKeys::_connectVehicle);
52 connect(mvm, &MultiVehicleManager::vehicleRemoved, this, &MAVLinkSigningKeys::_disconnectVehicle);
53
54 // Singleton may be created after vehicles are connected.
55 for (int i = 0; i < mvm->vehicles()->count(); ++i) {
56 _connectVehicle(mvm->vehicles()->value<Vehicle*>(i));
57 }
58
59 _timestampFlushTimer = new QTimer(this);
60 _timestampFlushTimer->setInterval(kTimestampFlushIntervalMs);
61 connect(_timestampFlushTimer, &QTimer::timeout, this, &MAVLinkSigningKeys::flushAllTimestamps);
62 _timestampFlushTimer->start();
63
64 // Q_APPLICATION_STATIC destruction order is undefined relative to LinkManager — flush before either gets torn down.
65 if (auto* app = QCoreApplication::instance()) {
66 connect(app, &QCoreApplication::aboutToQuit, this, &MAVLinkSigningKeys::flushAllTimestamps);
67 }
68}
69
71{
72 qCDebug(MAVLinkSigningKeysLog) << "MAVLinkSigningKeys dtor";
73}
74
75bool MAVLinkSigningKeys::isKeyInUse(const QString& name) const
76{
78 for (int i = 0; i < mvm->vehicles()->count(); ++i) {
79 const auto* vehicle = mvm->vehicles()->value<Vehicle*>(i);
80 if (vehicle && vehicle->signingController() && vehicle->signingController()->signingStatus().keyName == name) {
81 return true;
82 }
83 }
84 return false;
85}
86
87void MAVLinkSigningKeys::_connectVehicle(Vehicle* vehicle)
88{
89 if (auto* sc = vehicle->signingController()) {
91 }
92 ++_keyUsageRevision;
93 emit keyUsageChanged();
94}
95
96void MAVLinkSigningKeys::_disconnectVehicle(Vehicle* vehicle)
97{
98 if (auto* sc = vehicle->signingController()) {
100 }
101 ++_keyUsageRevision;
102 emit keyUsageChanged();
103}
104
106{
107 if (index >= 0 && index < _keys->count()) {
108 return _keys->value<MAVLinkSigningKey*>(index);
109 }
110 return nullptr;
111}
112
113std::optional<MAVLinkSigning::SigningKey> MAVLinkSigningKeys::keyBytesByName(const QString& name) const
114{
115 const auto it = _keyIndex.constFind(name);
116 if (it == _keyIndex.constEnd()) {
117 return std::nullopt;
118 }
119 return it.value()->keyBytes();
120}
121
122MAVLinkSigningKey* MAVLinkSigningKeys::_insertKey(const QString& name, const MAVLinkSigning::SigningKey& keyBytes)
123{
124 auto* key = new MAVLinkSigningKey(name, keyBytes, _keys);
125 _keys->append(key);
126 _keyIndex.insert(name, key);
127 return key;
128}
129
130bool MAVLinkSigningKeys::_validateNewKey(const QString& name) const
131{
132 if (name.isEmpty()) {
133 qCWarning(MAVLinkSigningKeysLog) << "Key name must not be empty";
134 return false;
135 }
136 if (_keys->count() >= kMaxKeys) {
137 qCWarning(MAVLinkSigningKeysLog) << "Maximum key count reached:" << kMaxKeys;
138 return false;
139 }
140 if (_keyIndex.contains(name)) {
141 qCWarning(MAVLinkSigningKeysLog) << "Key with name already exists:" << name;
142 return false;
143 }
144 return true;
145}
146
147bool MAVLinkSigningKeys::addKey(const QString& name, const QString& passphrase)
148{
149 if (passphrase.size() < kMinPassphraseLength) {
150 qCWarning(MAVLinkSigningKeysLog) << "Passphrase must be at least" << kMinPassphraseLength << "characters";
151 return false;
152 }
153 if (!_validateNewKey(name)) {
154 return false;
155 }
156
157 // Fixed app salt → deterministic across installs: same passphrase yields same key on every GCS sharing the vehicle.
158 const QByteArray salt(kPbkdf2Salt.constData(), kPbkdf2Salt.size());
159 QByteArray utf8 = passphrase.toUtf8();
160 QByteArray derived = QPasswordDigestor::deriveKeyPbkdf2(QCryptographicHash::Sha256, utf8, salt,
161 _effectivePbkdf2Iterations(), kSigningKeySize);
162 QGC::secureZero(utf8);
163 const auto key = MAVLinkSigning::makeSigningKey(derived);
164 QGC::secureZero(derived);
165 if (!key) {
166 return false;
167 }
168 _insertKey(name, *key);
169 _save();
170 emit keysChanged();
171 return true;
172}
173
174bool MAVLinkSigningKeys::addRawKey(const QString& name, const QString& hexKey)
175{
176 if (hexKey.isEmpty()) {
177 qCWarning(MAVLinkSigningKeysLog) << "Hex key must not be empty";
178 return false;
179 }
180 if (!_validateNewKey(name)) {
181 return false;
182 }
183
184 const QByteArray keyBytes = QByteArray::fromHex(hexKey.toLatin1());
185 const auto key = MAVLinkSigning::makeSigningKey(keyBytes);
186 if (!key) {
187 qCWarning(MAVLinkSigningKeysLog) << "Raw key must be exactly 32 bytes (64 hex chars), got" << keyBytes.size();
188 return false;
189 }
190
191 _insertKey(name, *key);
192 _save();
193 emit keysChanged();
194 return true;
195}
196
198{
199 // Generate into aligned buffer then copy — QByteArray::data() may not be 4-byte aligned
200 std::array<quint32, 8> aligned{};
201 QRandomGenerator::system()->fillRange(aligned.data(), aligned.size());
202 const QByteArray bytes(reinterpret_cast<const char*>(aligned.data()), sizeof(aligned));
203 return QString::fromLatin1(bytes.toHex());
204}
205
206QString MAVLinkSigningKeys::keyHexByName(const QString& name) const
207{
208 const auto key = keyBytesByName(name);
209 if (!key) {
210 return {};
211 }
212 QByteArray bytes(reinterpret_cast<const char*>(key->data()), key->size());
213 QByteArray hex = bytes.toHex();
214 QString result = QString::fromLatin1(hex);
215 // Wipe both intermediates; toHex() output also contains the key in encoded form.
216 QGC::secureZero(bytes);
217 QGC::secureZero(hex);
218 return result;
219}
220
221void MAVLinkSigningKeys::removeKey(const QString& name)
222{
223 const auto it = _keyIndex.constFind(name);
224 if (it == _keyIndex.constEnd()) {
225 return;
226 }
227 MAVLinkSigningKey* const entry = it.value();
228 _keyIndex.erase(it);
229 if (auto* removed = _keys->removeOne(entry)) {
230 removed->deleteLater();
231 }
232 _save();
233 emit keysChanged();
234}
235
237{
238 if (_keys->count() == 0) {
239 return;
240 }
241
242 _keyIndex.clear();
243 _keys->clearAndDeleteContents();
244 _save();
245 emit keysChanged();
246}
247
248void MAVLinkSigningKeys::_save()
249{
250 // No cross-process lock: RunGuard enforces single-instance, so the keystore has no concurrent writer.
251 QSettings settings;
252 settings.beginGroup(kSettingsGroup);
253 const QStringList previousNames = settings.value(kManifestKey).toStringList();
254
255 QStringList manifestNames;
256 for (int i = 0; i < _keys->count(); ++i) {
257 const auto* key = _keys->value<MAVLinkSigningKey*>(i);
258 const auto& bytes = key->keyBytes();
259 QByteArray serialized(reinterpret_cast<const char*>(bytes.data()), static_cast<qsizetype>(bytes.size()));
260 settings.setValue(QString("%1/%2").arg(kKeySubgroup, key->name()), serialized);
261 QGC::secureZero(serialized);
262 manifestNames.append(key->name());
263 }
264
265 const QSet<QString> liveNameSet(manifestNames.constBegin(), manifestNames.constEnd());
266 for (const QString& oldName : previousNames) {
267 if (!liveNameSet.contains(oldName)) {
268 settings.remove(QString("%1/%2").arg(kKeySubgroup, oldName));
269 settings.remove(QString("%1/%2").arg(kTimestampSubgroup, oldName));
270 }
271 }
272
273 settings.setValue(kManifestKey, manifestNames);
274 settings.endGroup();
275 // Explicit sync: persist before a crash between here and QSettings destruction.
276 settings.sync();
277}
278
279uint64_t MAVLinkSigningKeys::lastTimestamp(const QString& name) const
280{
281 const auto it = _keyIndex.constFind(name);
282 return it == _keyIndex.constEnd() ? 0 : it.value()->lastTimestamp();
283}
284
285void MAVLinkSigningKeys::recordTimestamps(const QHash<QString, uint64_t>& batch)
286{
287 if (batch.isEmpty()) {
288 return;
289 }
290 QSettings settings;
291 settings.beginGroup(kSettingsGroup);
292 bool wroteAnything = false;
293 for (auto it = batch.constBegin(); it != batch.constEnd(); ++it) {
294 const auto keyIt = _keyIndex.constFind(it.key());
295 if (keyIt == _keyIndex.constEnd()) {
296 continue;
297 }
298 // Monotonic guard — protects against a stale snapshot from a slower flush path.
299 if (it.value() <= keyIt.value()->lastTimestamp()) {
300 continue;
301 }
302 keyIt.value()->setLastTimestamp(it.value());
303 settings.setValue(QString("%1/%2").arg(kTimestampSubgroup, it.key()), QVariant::fromValue<quint64>(it.value()));
304 wroteAnything = true;
305 }
306 settings.endGroup();
307 if (wroteAnything) {
308 settings.sync();
309 }
310}
311
312void MAVLinkSigningKeys::recordTimestamp(const QString& name, uint64_t ts)
313{
314 recordTimestamps({{name, ts}});
315}
316
318{
319 recordTimestamps(_snapshotAllTimestamps());
320}
321
322QHash<QString, uint64_t> MAVLinkSigningKeys::_snapshotAllTimestamps() const
323{
324 QHash<QString, uint64_t> batch;
325 const auto links = LinkManager::instance()->links();
326 for (const auto& link : links) {
327 if (!link) {
328 continue;
329 }
330 const SigningController* const ctrl = link->signing();
331 if (!ctrl) {
332 continue;
333 }
334 const auto snap = ctrl->channel().currentTimestampAndName();
335 if (snap.keyName.isEmpty() || snap.timestamp == 0) {
336 continue;
337 }
338 auto& slot = batch[snap.keyName];
339 if (snap.timestamp > slot) {
340 slot = snap.timestamp;
341 }
342 }
343 return batch;
344}
345
347{
348 if (!controller || !MAVLinkSigning::isMessageSigned(message)) {
349 return QString();
350 }
351 // If signing is already configured on this controller, the C library already verified it.
352 if (controller->isEnabled()) {
353 return QString();
354 }
355
356 // Single-lock snapshot avoids TOCTOU vs MockLink's thread.
357 const auto snap = controller->channel().detectSnapshot();
358 if (snap.autoDetectSuspended || snap.inCooldown) {
359 return QString();
360 }
361
362 // Strict matches explicit-enable; Permissive here would silently accept all unsigned and defeat enforcement.
364
365 const QString& hintName = snap.keyHint;
366 if (!hintName.isEmpty()) {
367 if (const auto hintKey = keyBytesByName(hintName);
368 hintKey && MAVLinkSigning::verifySignature(*hintKey, message)) {
369 const QByteArrayView kv(reinterpret_cast<const char*>(hintKey->data()), hintKey->size());
370 if (controller->initSigningImmediate(kv, kPolicy, hintName)) {
371 controller->clearDetectCooldown();
372 qCDebug(MAVLinkSigningKeysLog) << "Auto-detected signing key" << hintName << "(cached hint)";
373 return hintName;
374 }
375 }
376 }
377
378 const int keyCount = _keys->count();
379 for (int i = 0; i < keyCount; ++i) {
380 const auto* entry = keyAt(i);
381 if (!entry || entry->name() == hintName) {
382 continue;
383 }
384 const auto& keyBytes = entry->keyBytes();
385 if (MAVLinkSigning::verifySignature(keyBytes, message)) {
386 const QByteArrayView kv(reinterpret_cast<const char*>(keyBytes.data()), keyBytes.size());
387 if (controller->initSigningImmediate(kv, kPolicy, entry->name())) {
388 controller->clearDetectCooldown();
389 qCDebug(MAVLinkSigningKeysLog) << "Auto-detected signing key" << entry->name();
390 return entry->name();
391 }
392 }
393 }
394
395 controller->recordDetectMiss();
396 return QString();
397}
398
399void MAVLinkSigningKeys::_load()
400{
401 _keys->clearAndDeleteContents();
402 _keyIndex.clear();
403
404 QSettings settings;
405 settings.beginGroup(kSettingsGroup);
406 const QStringList manifest = settings.value(kManifestKey).toStringList();
407 for (const QString& name : manifest) {
408 QByteArray keyBytes = settings.value(QString("%1/%2").arg(kKeySubgroup, name)).toByteArray();
409 if (const auto key = MAVLinkSigning::makeSigningKey(keyBytes); !name.isEmpty() && key) {
410 auto* inserted = _insertKey(name, *key);
411 const uint64_t ts = settings.value(QString("%1/%2").arg(kTimestampSubgroup, name)).toULongLong();
412 if (ts > 0) {
413 inserted->setLastTimestamp(ts);
414 }
415 } else if (!keyBytes.isEmpty()) {
416 qCWarning(MAVLinkSigningKeysLog) << "Skipping malformed key entry:" << name;
417 }
418 QGC::secureZero(keyBytes);
419 }
420 settings.endGroup();
421}
Q_APPLICATION_STATIC(MAVLinkSigningKeys, _mavlinkSigningKeysInstance)
struct __mavlink_message mavlink_message_t
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static LinkManager * instance()
QList< SharedLinkInterfacePtr > links()
A single named signing key entry.
const MAVLinkSigning::SigningKey & keyBytes() const
Bag of named MAVLink signing keys; correct key per vehicle is auto-detected from incoming signed pack...
static MAVLinkSigningKeys * instance()
std::optional< MAVLinkSigning::SigningKey > keyBytesByName(const QString &name) const
Key bytes for the key with the given name, or nullopt if not found.
void flushAllTimestamps()
Walk every signing channel and persist its current timestamp under the active key's name.
MAVLinkSigningKey * keyAt(int index) const
Key entry at the given index, or nullptr if invalid.
void recordTimestamps(const QHash< QString, uint64_t > &batch)
Batch update with single QSettings + sync; per-entry monotonic guard still applies.
Q_INVOKABLE bool addRawKey(const QString &name, const QString &hexKey)
Q_INVOKABLE void removeAllKeys()
Used by tests and full reset.
MAVLinkSigningKeys(QObject *parent=nullptr)
void recordTimestamp(const QString &name, uint64_t ts)
Update in-memory + persisted last-timestamp for name. Monotonic — older values are dropped.
Q_INVOKABLE void removeKey(const QString &name)
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...
Q_INVOKABLE bool addKey(const QString &name, const QString &passphrase)
Q_INVOKABLE bool isKeyInUse(const QString &name) const
True if any connected vehicle is using the key with the given name.
Q_INVOKABLE QString keyHexByName(const QString &name) const
Hex-encoded key bytes for export (empty if not found).
uint64_t lastTimestamp(const QString &name) const
Last persisted signing timestamp for name, or 0 if unknown / no entry.
static Q_INVOKABLE QString generateRandomHexKey()
Cryptographically random 64-char hex string (32 bytes).
static MultiVehicleManager * instance()
void vehicleAdded(Vehicle *vehicle)
void vehicleRemoved(Vehicle *vehicle)
void append(QObject *object)
Caller maintains responsibility for object ownership and deletion.
T value(int index) const
QObject * removeOne(const QObject *object) override final
int count() const override final
void clearAndDeleteContents() override final
Clears the list and calls deleteLater on each entry.
MAVLinkSigning::DetectSnapshot detectSnapshot() const
Single-lock snapshot; 3 separate reads have TOCTOU window vs MockLink's thread.
TimestampSnapshot currentTimestampAndName() const
Returns current timestamp and active key name. Returns {0, ""} when signing is not enabled.
Owns MAVLink signing state and the deferred-confirmation state machine for one LinkInterface.
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.
const SigningChannel & channel() const
VehicleSigningController * signingController()
Definition Vehicle.h:542
std::array< uint8_t, kSigningKeySize > SigningKey
std::array avoids QByteArray COW detach so secureZero() actually wipes the bytes.
bool verifySignature(QByteArrayView key, const mavlink_message_t &message)
Verify a key against a signed message's signature.
std::optional< SigningKey > makeSigningKey(QByteArrayView bytes)
Build a SigningKey from arbitrary bytes. Returns nullopt if input is the wrong size.
bool isMessageSigned(const mavlink_message_t &message)
Returns true if the message has a MAVLink2 signature.
void secureZero(void *data, size_t size)