OpenSSL C++でMAVLink Proxyのツールを自作

MAVLinkをネットワーク経由でリモートGCSと通信には、[MAVProxy](https://ardupilot.org/mavproxy/)のpythonツールがありますが、SSL／TLSを対応していません。

[MAVSec](https://arxiv.org/pdf/1905.00265.pdf)の論文がありますが、実現が難しい（Flight ControllerでMAVLinkプロトコルのPAYLOADを暗号化する処理を追加）、また実現ソースコードにはRC4アルゴリズムの暗号化バグがあります。

OpenSSL APIでMAVLink Proxyの簡単なツールを自作します。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/0c974b2a-1847-f12d-b692-e7ca777d6ad9.png)


# 環境
- Ubuntu 18.04.5
- cmake version 3.10.2
- OpenSSL 1.1.1
- QGroundControl 4.0.10
- Pixhawk PX4FMU 2.4.6
- PX4 Firmware 1.9.0dev
- WireShark 2.6.10


# OpenSSL 証明書と秘密鍵の作成

テスト用self-signed certificate SSL証明書と秘密鍵を作成します。

```console
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365
```

# SSL／TLSでMAVLinkの通信の暗号化

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/ce6196ee-d113-a009-a297-e06d260041a1.png)

# GCS (QGroundControl) でMAVLink受信UDPポートの設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/e5c01178-9c21-4ddc-0b92-a4cea3a07961.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/1d894dfc-afe2-f29d-ed16-9abfae320ca3.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/6bf60fc9-87f2-a43b-5903-fe1e1e778470.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/386259/de01af64-14dc-dd67-8cdc-be67b4fa3267.png)


# OpenSSL MAVLink Proxyの簡単なサンプルコード

```c++:mavproxy_ssl_client.cpp
#include <fcntl.h>
#include <termios.h>
#include <netdb.h>
#include <openssl/ssl.h>
#include <sys/ioctl.h>

#define DEV_NAME       "/dev/ttyACM0"       // デバイスファイル名
#define BAUD_RATE      115200               // 通信ボーレート
#define BUFF_SIZE      16*1024              // 適当
#define PROXY_IP       "192.168.11.18"      // PROXY IP Address
#define PROXY_TCP_PORT 4433                 // PROXY TCP Port

int open_serial(const char *dev_name)
{
    int fd;
    fd = open(dev_name, O_RDWR);
    if(fd < 0){
        perror(dev_name);
        exit(EXIT_FAILURE);
    }

    return fd;
}

void init_serial(int fd, unsigned int baud_rate)
{
    struct termios tio;
    memset(&tio, 0, sizeof(tio));
    /*
     * CS8     : 8n1 (8 ビット，ノンパリティ，ストップビット 1)
     * CLOCAL  : ローカル接続，モデム制御なし
     * CREAD   : 受信文字(receiving characters)を有効
     */
    tio.c_cflag = CS8 | CLOCAL | CREAD;
    // キャラクタ間タイマは未使用
    tio.c_cc[VTIME] = 100;
    // ボーレートの設定
    cfsetispeed(&tio, baud_rate);
    cfsetospeed(&tio, baud_rate);
    // デバイスに設定
    tcsetattr(fd,TCSANOW,&tio);
}


int create_socket(int type, int port, const char *ip)
{
    int sd;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);

    sd = socket(AF_INET, type, 0);
    if (sd < 0) {
        perror("Unable to create socket");
        exit(EXIT_FAILURE);
    }
    if (connect(sd, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable connect to server socket");
        exit(EXIT_FAILURE);
    }
    // set non-blocking socket
    int val = 1;
    ioctl(sd, FIONBIO, &val);

    return sd;
}

void init_openssl()
{
    SSL_load_error_strings();
    OpenSSL_add_ssl_algorithms();
}

void cleanup_openssl()
{
    EVP_cleanup();
}

SSL_CTX *create_ssl_context()
{
    const SSL_METHOD *method;
    SSL_CTX *ctx;

    method = SSLv23_client_method();
    ctx = SSL_CTX_new(method);
    if (!ctx) {
        perror("Unable to create SSL context");
        ERR_print_errors_fp(stderr);
        exit(EXIT_FAILURE);
    }

    return ctx;
}

SSL *connect_ssl(SSL_CTX *ctx, int socket)
{
    SSL *ssl;

    // create SSL stack endpoint
    ssl = SSL_new(ctx);
    // attach SSL stack to socket
    SSL_set_fd(ssl, socket);
    // initiate SSL handshake
    if (SSL_connect(ssl) < 0) {
        perror("Unable initiate SSL handshake");
//        exit(EXIT_FAILURE);
    }

    return ssl;
}

int main(int argc, char **argv)
{
    int fd_dev;
    // Pixhawk シリアルポートの開く
    fd_dev = open_serial(DEV_NAME);
    // Pixhawk シリアルポートの初期化
    init_serial(fd_dev, BAUD_RATE);
    printf("Serial connection to '%s', baud_rate %d\n", DEV_NAME, BAUD_RATE);

    int fd_proxy;
    SSL_CTX *ctx;
    SSL *ssl;
    // OpenSSLの初期化
    init_openssl();
    // SSL_CTXオブジェクトの生成
    ctx = create_ssl_context();
    printf("SSL context initialized\n");
    // PROXY TCPソケットの作成
    fd_proxy = create_socket(SOCK_STREAM, PROXY_TCP_PORT, PROXY_IP);
    printf("TCP connection to '%s', port %d\n", PROXY_IP, PROXY_TCP_PORT);
    // PROXY TCPソケットでSSLハンドシェイクの接続
    ssl = connect_ssl(ctx, fd_proxy);
    printf("SSL endpoint created & handshake completed\n");

    int len = 0;
    unsigned char buffer[BUFF_SIZE];
    while(1) {
        // PROXY SSLからMAVLinkを受信
        memset(&buffer, 0, sizeof(buffer));
        len = SSL_read(ssl, buffer, sizeof(buffer));
        if (len > 0) {
            // PixhawkにMAVLinkを送信
            write(fd_dev, buffer, len);
            // For debug
            for(int i = 0; i < len; i++){
                printf("%02X ",buffer[i]);
            }
            printf("\n\n");
        }
        // PixhawkからMAVLinkを受信
        memset(&buffer, 0, sizeof(buffer));
        len = read(fd_dev, buffer, sizeof(buffer));
        if(len == 0){
            continue;
        }
        if(len < 0){
            perror("Unable read serial data");
            exit(EXIT_FAILURE);
        }
        // PROXY SSLにMAVLinkを送信
        SSL_write(ssl, buffer, len);
    }

    SSL_shutdown(ssl);
    SSL_free (ssl);
    SSL_CTX_free (ctx);
    cleanup_openssl();
    close(fd_proxy);
    close(fd_dev);

    return EXIT_SUCCESS;
}
```

```c++:mavproxy_ssl_server.cpp
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <sys/ioctl.h>

#define BUFF_SIZE      16*1024              // 適当
#define PROXY_TCP_PORT 4433                 // GCS TCP Port
#define GCS_IP         "127.0.0.1"          // GCS IP Address
#define GCS_UDP_PORT   14540                // GCS UDP Port

int create_listen_socket(int port)
{
    int s;
    struct sockaddr_in addr;

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    s = socket(AF_INET, SOCK_STREAM, 0);
    if (s < 0) {
        perror("Unable to create listen socket");
        exit(EXIT_FAILURE);
    }

    if (bind(s, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
        perror("Unable to bind listen socket");
        exit(EXIT_FAILURE);
    }

    if (listen(s, 1) < 0) {
        perror("Unable to listen socket");
        exit(EXIT_FAILURE);
    }

    return s;
}

int create_udp_socket(const char *ip_address, int port)
{
    int s;
    struct sockaddr_in addr;

    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip_address);
    addr.sin_port = htons(port);

    s = socket(AF_INET, SOCK_DGRAM, 0);
    if (s < 0) {
        perror("Unable to create udp socket");
        exit(EXIT_FAILURE);
    }

    if (connect(s, (struct sockaddr *)&addr, sizeof(struct sockaddr)) < 0) {
        printf("Unable to connect %s : %d", ip_address, port);
        perror("");
        exit(EXIT_FAILURE);
    }
    // set non blocking socket
    int val = 1;
    ioctl(s, FIONBIO, &val);

    return s;
}

void init_openssl()
{
    SSL_load_error_strings();
    OpenSSL_add_ssl_algorithms();
}

void cleanup_openssl()
{
    EVP_cleanup();
}

SSL_CTX *create_context()
{
    const SSL_METHOD *method;
    SSL_CTX *ctx;

    method = SSLv23_server_method();

    ctx = SSL_CTX_new(method);
    if (!ctx) {
        perror("Unable to create SSL context");
        exit(EXIT_FAILURE);
    }

    return ctx;
}

void configure_context(SSL_CTX *ctx)
{
    SSL_CTX_set_ecdh_auto(ctx, 1);

    /* Set the key and cert */
    if (SSL_CTX_use_certificate_file(ctx, "cert.pem", SSL_FILETYPE_PEM) <= 0) {
        exit(EXIT_FAILURE);
    }

    if (SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0 ) {
        exit(EXIT_FAILURE);
    }
}

SSL *accept_ssl(SSL_CTX *ctx, int socket)
{
    SSL *ssl;

    // create SSL stack endpoint
    ssl = SSL_new(ctx);
    // attach SSL stack to socket
    SSL_set_fd(ssl, socket);
    // initiate SSL handshake
    if (SSL_accept(ssl) <= 0) {
        perror("Unable initiate SSL handshake");
        exit(EXIT_FAILURE);
    }

    return ssl;
}

int main(int argc, char **argv)
{
    int fd_gcs;
    // GCS UDPソケットの作成
    fd_gcs = create_udp_socket(GCS_IP, GCS_UDP_PORT);
    printf("UDP connection to '%s', port %d\n", GCS_IP, GCS_UDP_PORT);

    int sock;
    int client;
    SSL_CTX *ctx;
    SSL *ssl;
    // OpenSSLの初期化
    init_openssl();
    // SSL_CTXオブジェクトの生成
    ctx = create_context();
    printf("SSL context initialized\n");
    // SSL証明書と秘密鍵の登録
    configure_context(ctx);
    printf("SSL context configured\n");
    // TCPソケットの作成
    sock = create_listen_socket(PROXY_TCP_PORT);
    printf("proxy socket created.\n");
    // クライアントから接続待機
    struct sockaddr_in addr;
    uint len_addr = sizeof(addr);
    client = accept(sock, (struct sockaddr*)&addr, &len_addr);
    if (client < 0) {
        perror("Unable to accept socket");
        exit(EXIT_FAILURE);
    }
    printf("client socket accepted.\n");
    // SSLハンドシェイクの接続
    ssl = accept_ssl(ctx, client);
    printf("SSL endpoint created & handshake completed\n");

    int len = 0;
    unsigned char buffer[BUFF_SIZE];
    while(1) {
        // SSL からMAVLinkを受信
        memset(&buffer, 0, sizeof(buffer));
        len = SSL_read(ssl, buffer, sizeof(buffer));
        printf("read from ssl %d byte\n", len);
        if (len == 0) {
            sleep(1);
            continue;
        }
        if (len < 0) {
            perror("Unable read from ssl");
            break;
        }

        // GCS にMAVLinkを送信
        send(fd_gcs, buffer, len, 0);
        printf("send to gcs %d byte\n", len);

        // GCS からMAVLinkを受信
        memset(&buffer, 0, sizeof(buffer));
        len = recv(fd_gcs, buffer, sizeof(buffer), 0);
        printf("recv from gcs %d byte\n", len);

        // SSL にMAVLinkを送信
        if (len > 0) {
            SSL_write(ssl, buffer, len);
            printf("write to ssl %d byte\n", len);
            // For debug
            for (int i=0; i < len; i++) {
                printf("%02X ", buffer[i]);
            }
            printf("\n\n");
        }
    }

    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(client);

    close(sock);
    SSL_CTX_free(ctx);
    cleanup_openssl();
}
```