C++とOpenSSLでX25519による鍵共有をする

WireGuardというVPNでは通信相手を特定するのにX25519という鍵共有アルゴリズムが使われている。WireGuard用に作成した鍵を自分のプログラムに流用したい。

2021年9月15日 嶋田大貴

WireGuardは 2020年1月に Linuxに標準で取り込まれ、また他にも Windows, Mac, iOS, Androidにそれぞれ無償で提供されている VPN実装で、余計な機能を持たせずに接続性の高さと攻撃平面の小ささを追求した設計になっている。

IPアドレスの自動割当とかそういった「多くの人が欲しいもの」もさしあたり「余計な機能」扱いなので1、WireGuardでVPN接続をせんとする両端点はあらかじめ設定ファイルで静的に設定されている必要がある。したがって、ハブ型のVPNサーバを WireGuardで運用するにあたってはIPアドレスの割当やクライアント向け設定ファイルの生成や引き渡しといった周辺サービスを別に行う必要があり、これについてはまだ定まった標準がないので多分あちこちで自作されている。

そういった周辺サービスを自分のとこでも設計・実装するとなると、VPNサーバ側がクライアントに対して付与してやりたい設定ファイルなどといった個別データをどういう経路で渡すかというところが若干悩ましい。そこで、サーバは各クライアントからもらったVPN接続用の公開鍵を流用して自分の秘密鍵との間で共通鍵を導出し、各クライアント向けに生成した設定ファイルを暗号化して自分の公開鍵と一緒にしたものを好きな経路で送りつける(あるいは簡単にアクセスできる所に晒す)という方法がある。(もちろんクライアント側にそれを復号するための専用ソフトウェアが必要にはなるが、そういうものはあらかじめ配布できるものとする)

アリスとボブのクソ雑暗号鍵共有講座

WireGuardでは X25519 という鍵共有アルゴリズムが使用されており、設定ファイルには X25519の鍵を Base64エンコードしたものを記載するようになっている。この形式で表記された鍵(相手の公開鍵+自分の秘密鍵)を使って共通鍵を導出するC++プログラムのサンプルが下記となる。もちろん計算部分は暗号の専門知識を有する者でなければ自作できないため OpenSSLの関数を使用している。

#include <unistd.h>
#include <memory.h>

#include <fstream>
#include <iostream>
#include <filesystem>

#include <openssl/evp.h>

static const size_t WG_KEY_LEN = 32; // WireGuardで鍵共有に使用される鍵(X25519)のオクテット長

std::string base64_encode(const uint8_t* bytes, size_t len)
{
    char encoded[4*((len+2)/3)];
    if (!EVP_EncodeBlock((unsigned char*)encoded, bytes, len))
        throw std::runtime_error("EVP_EncodeBlock() failed");
    //else
    return encoded;
}

std::pair<std::shared_ptr<uint8_t[]>,size_t> base64_decode(const std::string& base64)
{
    std::shared_ptr<uint8_t[]> decoded(new uint8_t[3*base64.length()/4]);
    int rst = EVP_DecodeBlock(
        decoded.get(), (const unsigned char*)base64.c_str(), base64.length());
    if (rst < 0) throw std::runtime_error("EVP_DecodeBlock() failed");
    //else
    return {decoded, (size_t)rst};
}

// おまけ: X25519鍵ペアの新規作成方法
std::pair<std::shared_ptr<uint8_t[]>,std::shared_ptr<uint8_t[]>> create_new_keypair()
{
    std::shared_ptr<uint8_t[]> privkey_bytes(new uint8_t[WG_KEY_LEN]);
    if (getentropy(privkey_bytes.get(), WG_KEY_LEN) != 0)
        throw std::runtime_error("getentropy() failed");
    // https://github.com/torvalds/linux/blob/master/include/crypto/curve25519.h#L61
    privkey_bytes[0] &= 248;
    privkey_bytes[31] = (privkey_bytes[31] & 127) | 64;
    std::shared_ptr<EVP_PKEY> privkey(
        EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, privkey_bytes.get(), WG_KEY_LEN), 
        EVP_PKEY_free);

    std::shared_ptr<uint8_t[]> pubkey_bytes(new uint8_t[WG_KEY_LEN]);
    size_t pubkey_len = WG_KEY_LEN;
    if (!EVP_PKEY_get_raw_public_key(privkey.get(), pubkey_bytes.get(), &pubkey_len)) {
        throw std::runtime_error("EVP_PKEY_get_raw_public_key() failed");
    }

    return {privkey_bytes, pubkey_bytes};
}

int main(int argc, char* argv[])
{
    if (argc < 3) {
        std::cout << "Derive shared key from base64-encoded WireGuard keys(X25519)" << std::endl;
        std::cout << "Usage:" << std::endl;
        std::cout << "  " << argv[0] << " <your private key> <peer's public key>" << std::endl;
        return -1;
    }

    auto privkey_bytes = base64_decode(argv[1]);
    auto pubkey_bytes = base64_decode(argv[2]);

    if (privkey_bytes.second < WG_KEY_LEN) throw std::runtime_error("Incomplete private key");
    if (pubkey_bytes.second < WG_KEY_LEN) throw std::runtime_error("Incomplete public key");

    std::shared_ptr<EVP_PKEY> privkey(
        EVP_PKEY_new_raw_private_key(EVP_PKEY_X25519, NULL, privkey_bytes.first.get(), WG_KEY_LEN), 
        EVP_PKEY_free);
    if (!privkey) throw std::runtime_error("EVP_PKEY_new_raw_private_key() failed");

    std::shared_ptr<EVP_PKEY> pubkey(
        EVP_PKEY_new_raw_public_key(EVP_PKEY_X25519, NULL, pubkey_bytes.first.get(), WG_KEY_LEN), 
        EVP_PKEY_free);
    if (!pubkey) throw std::runtime_error("EVP_PKEY_new_raw_public_key() failed");

    std::shared_ptr<EVP_PKEY_CTX> pkey_ctx(
        EVP_PKEY_CTX_new(privkey.get(), EVP_PKEY_get0_engine(privkey.get())), 
        EVP_PKEY_CTX_free);
    EVP_PKEY_derive_init(pkey_ctx.get());
    EVP_PKEY_derive_set_peer(pkey_ctx.get(), pubkey.get());
    size_t skeylen;
    EVP_PKEY_derive(pkey_ctx.get(), NULL, &skeylen);
    uint8_t shared_key_bytes[skeylen];
    EVP_PKEY_derive(pkey_ctx.get(), shared_key_bytes, &skeylen);

    std::cout << "Shared key: " << base64_encode(shared_key_bytes, skeylen) << std::endl;
    return 0;
}

// g++ -std=c++20 -o derive-wg-shared-key derive-wg-shared-key.cpp -lcrypto -lssl

  1. アイディア自体は2019年に提案されているようだが進展があったかどうかは知らない。 

2021年9月15日 嶋田大貴

記事一覧へ戻る