Skip to content

Commit 8bcd6c9

Browse files
committed
libmavconn: add MAVLink v2 signing support
1 parent e2e813c commit 8bcd6c9

File tree

3 files changed

+260
-13
lines changed

3 files changed

+260
-13
lines changed

libmavconn/include/mavconn/interface.hpp

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@
2424
#define MAVCONN__INTERFACE_HPP_
2525

2626
#include <atomic>
27+
#include <array>
2728
#include <cassert>
2829
#include <chrono>
2930
#include <deque>
3031
#include <functional>
3132
#include <memory>
3233
#include <mutex>
34+
#include <optional>
3335
#include <sstream>
3436
#include <stdexcept>
3537
#include <string>
@@ -245,6 +247,31 @@ class MAVConnInterface
245247
void set_protocol_version(Protocol pver);
246248
[[nodiscard]] Protocol get_protocol_version();
247249

250+
/**
251+
* @brief Enable MAVLink v2 packet signing for this connection.
252+
*
253+
* @param[in] secret_key 32-byte signing key
254+
* @param[in] sign_outgoing enable signing for outgoing packets
255+
* @param[in] link_id link id embedded in outgoing signatures
256+
* @param[in] initial_timestamp optional initial timestamp in 10 usec units
257+
* since 2015-01-01 00:00:00 UTC.
258+
*/
259+
void setup_signing(
260+
const std::array<uint8_t, 32> & secret_key,
261+
bool sign_outgoing = true,
262+
uint8_t link_id = 0,
263+
std::optional<uint64_t> initial_timestamp = std::nullopt);
264+
265+
/**
266+
* @brief Disable MAVLink v2 packet signing.
267+
*/
268+
void disable_signing();
269+
270+
/**
271+
* @brief Configure callback for accepting unsigned packets while signing is enabled.
272+
*/
273+
void set_accept_unsigned_callback(mavlink::mavlink_accept_unsigned_t cb);
274+
248275
/**
249276
* @brief Construct connection from URL
250277
*
@@ -303,7 +330,7 @@ class MAVConnInterface
303330

304331
inline mavlink::mavlink_status_t * get_status_p()
305332
{
306-
return &m_parse_status;
333+
return &m_tx_status;
307334
}
308335

309336
inline mavlink::mavlink_message_t * get_buffer_p()
@@ -326,9 +353,18 @@ class MAVConnInterface
326353
private:
327354
friend const mavlink::mavlink_msg_entry_t * mavlink::mavlink_get_msg_entry(uint32_t msgid);
328355

329-
mavlink::mavlink_status_t m_parse_status;
356+
static uint64_t default_signing_timestamp();
357+
void apply_signing_config(bool sign_outgoing, uint8_t link_id, uint64_t timestamp);
358+
359+
mavlink::mavlink_status_t m_rx_parse_status;
330360
mavlink::mavlink_message_t m_buffer;
331-
mavlink::mavlink_status_t m_mavlink_status;
361+
mavlink::mavlink_status_t m_rx_status;
362+
mavlink::mavlink_status_t m_tx_status;
363+
364+
mavlink::mavlink_signing_t m_rx_signing;
365+
mavlink::mavlink_signing_t m_tx_signing;
366+
mavlink::mavlink_signing_streams_t m_rx_signing_streams;
367+
bool signing_enabled;
332368

333369
std::atomic<size_t> tx_total_bytes, rx_total_bytes;
334370
std::mutex iostat_mutex;

libmavconn/src/interface.cpp

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717

1818
#include <cassert>
19+
#include <cstring>
1920
#include <limits>
2021
#include <memory>
2122
#include <set>
@@ -46,9 +47,14 @@ std::atomic<size_t> MAVConnInterface::conn_id_counter {0};
4647
MAVConnInterface::MAVConnInterface(uint8_t system_id, uint8_t component_id)
4748
: sys_id(system_id),
4849
comp_id(component_id),
49-
m_parse_status{},
50+
m_rx_parse_status{},
5051
m_buffer{},
51-
m_mavlink_status{},
52+
m_rx_status{},
53+
m_tx_status{},
54+
m_rx_signing{},
55+
m_tx_signing{},
56+
m_rx_signing_streams{},
57+
signing_enabled(false),
5258
tx_total_bytes(0),
5359
rx_total_bytes(0),
5460
last_tx_total_bytes(0),
@@ -61,7 +67,7 @@ MAVConnInterface::MAVConnInterface(uint8_t system_id, uint8_t component_id)
6167

6268
mavlink_status_t MAVConnInterface::get_status()
6369
{
64-
return m_mavlink_status;
70+
return m_rx_status;
6571
}
6672

6773
MAVConnInterface::IOStat MAVConnInterface::get_iostat()
@@ -116,8 +122,8 @@ void MAVConnInterface::parse_buffer(
116122
auto c = *buf++;
117123

118124
auto msg_received = static_cast<Framing>(mavlink::mavlink_frame_char_buffer(
119-
&m_buffer, &m_parse_status, c,
120-
&message, &m_mavlink_status));
125+
&m_buffer, &m_rx_parse_status, c,
126+
&message, &m_rx_status));
121127

122128
if (msg_received != Framing::incomplete) {
123129
log_recv(pfx, message, msg_received);
@@ -191,23 +197,97 @@ void MAVConnInterface::send_message_ignore_drop(const mavlink::Message & msg, ui
191197
void MAVConnInterface::set_protocol_version(Protocol pver)
192198
{
193199
if (pver == Protocol::V10) {
194-
m_mavlink_status.flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
195-
m_parse_status.flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
200+
m_tx_status.flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
201+
m_rx_status.flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
202+
m_rx_parse_status.flags |= MAVLINK_STATUS_FLAG_OUT_MAVLINK1;
196203
} else {
197-
m_mavlink_status.flags &= ~(MAVLINK_STATUS_FLAG_OUT_MAVLINK1);
198-
m_parse_status.flags &= ~(MAVLINK_STATUS_FLAG_OUT_MAVLINK1);
204+
m_tx_status.flags &= ~(MAVLINK_STATUS_FLAG_OUT_MAVLINK1);
205+
m_rx_status.flags &= ~(MAVLINK_STATUS_FLAG_OUT_MAVLINK1);
206+
m_rx_parse_status.flags &= ~(MAVLINK_STATUS_FLAG_OUT_MAVLINK1);
199207
}
200208
}
201209

202210
Protocol MAVConnInterface::get_protocol_version()
203211
{
204-
if (m_mavlink_status.flags & MAVLINK_STATUS_FLAG_OUT_MAVLINK1) {
212+
if (m_tx_status.flags & MAVLINK_STATUS_FLAG_OUT_MAVLINK1) {
205213
return Protocol::V10;
206214
} else {
207215
return Protocol::V20;
208216
}
209217
}
210218

219+
uint64_t MAVConnInterface::default_signing_timestamp()
220+
{
221+
constexpr auto mavlink_signing_epoch = std::chrono::sys_days {std::chrono::year(2015) /
222+
std::chrono::January / 1};
223+
auto now = std::chrono::system_clock::now();
224+
if (now < mavlink_signing_epoch) {
225+
now = mavlink_signing_epoch;
226+
}
227+
228+
const auto delta_us = std::chrono::duration_cast<std::chrono::microseconds>(
229+
now - mavlink_signing_epoch).count();
230+
return static_cast<uint64_t>(delta_us / 10);
231+
}
232+
233+
void MAVConnInterface::apply_signing_config(bool sign_outgoing, uint8_t link_id, uint64_t timestamp)
234+
{
235+
m_tx_signing.flags = sign_outgoing ? MAVLINK_SIGNING_FLAG_SIGN_OUTGOING : 0;
236+
m_tx_signing.link_id = link_id;
237+
m_tx_signing.timestamp = timestamp;
238+
m_tx_signing.last_status = mavlink::MAVLINK_SIGNING_STATUS_NONE;
239+
240+
m_rx_signing.flags = sign_outgoing ? MAVLINK_SIGNING_FLAG_SIGN_OUTGOING : 0;
241+
m_rx_signing.link_id = link_id;
242+
m_rx_signing.timestamp = timestamp;
243+
m_rx_signing.last_status = mavlink::MAVLINK_SIGNING_STATUS_NONE;
244+
245+
m_rx_signing_streams = {};
246+
247+
m_tx_status.signing = &m_tx_signing;
248+
m_rx_parse_status.signing = &m_rx_signing;
249+
m_rx_parse_status.signing_streams = &m_rx_signing_streams;
250+
m_rx_status.signing = &m_rx_signing;
251+
m_rx_status.signing_streams = &m_rx_signing_streams;
252+
253+
signing_enabled = true;
254+
}
255+
256+
void MAVConnInterface::setup_signing(
257+
const std::array<uint8_t, 32> & secret_key,
258+
bool sign_outgoing,
259+
uint8_t link_id,
260+
std::optional<uint64_t> initial_timestamp)
261+
{
262+
std::memcpy(m_tx_signing.secret_key, secret_key.data(), secret_key.size());
263+
std::memcpy(m_rx_signing.secret_key, secret_key.data(), secret_key.size());
264+
265+
const auto timestamp = initial_timestamp.value_or(default_signing_timestamp());
266+
apply_signing_config(sign_outgoing, link_id, timestamp);
267+
}
268+
269+
void MAVConnInterface::disable_signing()
270+
{
271+
m_tx_signing = {};
272+
m_rx_signing = {};
273+
m_rx_signing_streams = {};
274+
275+
m_tx_status.signing = nullptr;
276+
m_tx_status.signing_streams = nullptr;
277+
m_rx_parse_status.signing = nullptr;
278+
m_rx_parse_status.signing_streams = nullptr;
279+
m_rx_status.signing = nullptr;
280+
m_rx_status.signing_streams = nullptr;
281+
282+
signing_enabled = false;
283+
}
284+
285+
void MAVConnInterface::set_accept_unsigned_callback(mavlink::mavlink_accept_unsigned_t cb)
286+
{
287+
m_tx_signing.accept_unsigned_callback = cb;
288+
m_rx_signing.accept_unsigned_callback = cb;
289+
}
290+
211291
/**
212292
* Parse host:port pairs
213293
*/

libmavconn/test/test_mavconn.cpp

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
// #include <gmock/gmock.h>
2121
#include <gtest/gtest.h>
2222

23+
#include <array>
2324
#include <chrono>
2425
#include <cmath>
2526
#include <condition_variable>
@@ -271,6 +272,13 @@ class TCP : public UDP
271272
{
272273
};
273274

275+
static bool accept_unsigned_for_test(
276+
const mavlink::mavlink_status_t * status [[maybe_unused]],
277+
uint32_t msgid [[maybe_unused]])
278+
{
279+
return true;
280+
}
281+
274282
TEST_F(TCP, bind_error)
275283
{
276284
MAVConnInterface::Ptr conns[2];
@@ -357,6 +365,129 @@ TEST(SERIAL, open_error)
357365
DeviceError);
358366
}
359367

368+
TEST(SIGNING, udp_signed_packet_is_accepted)
369+
{
370+
std::mutex mutex;
371+
std::condition_variable cond;
372+
auto got = false;
373+
auto framing = Framing::incomplete;
374+
auto message_id = std::numeric_limits<msgid_t>::max();
375+
376+
const std::array<uint8_t, 32> key {0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
377+
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
378+
0x42, 0x42, 0x42, 0x42, 0x42, 0x42};
379+
380+
auto receiver = std::make_shared<MAVConnUDP>(42, 200, "0.0.0.0", 45042);
381+
receiver->setup_signing(key, true, 1, 1U);
382+
receiver->connect(
383+
[&](const mavlink_message_t * message, const Framing msg_framing) {
384+
std::lock_guard<std::mutex> lock(mutex);
385+
message_id = message->msgid;
386+
framing = msg_framing;
387+
got = true;
388+
cond.notify_all();
389+
});
390+
391+
auto sender = std::make_shared<MAVConnUDP>(44, 200, "0.0.0.0", 45043, "localhost", 45042);
392+
sender->setup_signing(key, true, 2, 1U);
393+
sender->connect(MAVConnInterface::ReceivedCb());
394+
395+
send_heartbeat(sender.get());
396+
send_heartbeat(sender.get());
397+
398+
{
399+
std::unique_lock<std::mutex> lock(mutex);
400+
EXPECT_TRUE(cond.wait_for(lock, std::chrono::seconds(2), [&]() {return got;}));
401+
}
402+
EXPECT_EQ(framing, Framing::ok);
403+
EXPECT_EQ(message_id, mavlink::common::msg::HEARTBEAT::MSG_ID);
404+
405+
sender->close();
406+
receiver->close();
407+
}
408+
409+
TEST(SIGNING, udp_signature_mismatch_is_reported)
410+
{
411+
std::mutex mutex;
412+
std::condition_variable cond;
413+
auto got = false;
414+
auto framing = Framing::incomplete;
415+
416+
const std::array<uint8_t, 32> sender_key {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
417+
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
418+
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11};
419+
const std::array<uint8_t, 32> receiver_key {0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
420+
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
421+
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22};
422+
423+
auto receiver = std::make_shared<MAVConnUDP>(42, 200, "0.0.0.0", 45044);
424+
receiver->setup_signing(receiver_key, true, 1, 1U);
425+
receiver->connect(
426+
[&](const mavlink_message_t * message [[maybe_unused]], const Framing msg_framing) {
427+
std::lock_guard<std::mutex> lock(mutex);
428+
framing = msg_framing;
429+
got = true;
430+
cond.notify_all();
431+
});
432+
433+
auto sender = std::make_shared<MAVConnUDP>(44, 200, "0.0.0.0", 45045, "localhost", 45044);
434+
sender->setup_signing(sender_key, true, 2, 1U);
435+
sender->connect(MAVConnInterface::ReceivedCb());
436+
437+
send_heartbeat(sender.get());
438+
send_heartbeat(sender.get());
439+
440+
{
441+
std::unique_lock<std::mutex> lock(mutex);
442+
EXPECT_TRUE(cond.wait_for(lock, std::chrono::seconds(2), [&]() {return got;}));
443+
}
444+
EXPECT_EQ(framing, Framing::bad_signature);
445+
446+
sender->close();
447+
receiver->close();
448+
}
449+
450+
TEST(SIGNING, udp_accept_unsigned_callback_allows_unsigned_packets)
451+
{
452+
std::mutex mutex;
453+
std::condition_variable cond;
454+
auto got = false;
455+
auto framing = Framing::incomplete;
456+
auto message_id = std::numeric_limits<msgid_t>::max();
457+
458+
const std::array<uint8_t, 32> receiver_key {0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
459+
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
460+
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33};
461+
462+
auto receiver = std::make_shared<MAVConnUDP>(42, 200, "0.0.0.0", 45046);
463+
receiver->setup_signing(receiver_key, true, 1, 1U);
464+
receiver->set_accept_unsigned_callback(accept_unsigned_for_test);
465+
receiver->connect(
466+
[&](const mavlink_message_t * message, const Framing msg_framing) {
467+
std::lock_guard<std::mutex> lock(mutex);
468+
message_id = message->msgid;
469+
framing = msg_framing;
470+
got = true;
471+
cond.notify_all();
472+
});
473+
474+
auto sender = std::make_shared<MAVConnUDP>(44, 200, "0.0.0.0", 45047, "localhost", 45046);
475+
sender->connect(MAVConnInterface::ReceivedCb());
476+
477+
send_heartbeat(sender.get());
478+
send_heartbeat(sender.get());
479+
480+
{
481+
std::unique_lock<std::mutex> lock(mutex);
482+
EXPECT_TRUE(cond.wait_for(lock, std::chrono::seconds(2), [&]() {return got;}));
483+
}
484+
EXPECT_EQ(framing, Framing::ok);
485+
EXPECT_EQ(message_id, mavlink::common::msg::HEARTBEAT::MSG_ID);
486+
487+
sender->close();
488+
receiver->close();
489+
}
490+
360491
#if 0
361492
TEST(URL, open_url_serial)
362493
{

0 commit comments

Comments
 (0)