測位衛星の航法メッセージ認証QZNMA
はじめに
測位衛星の信号受信を妨げる電波妨害(ジャミング)や、偽の信号によって受信機の位置や時刻の推定を誤らせる攻撃(スプーフィング)が問題になっています。GPS JAMには、GPS電波の干渉状況が世界地図上に表示されています。
測位衛星が放送する信号には、
- 衛星軌道や時刻などを伝える航法メッセージ
- 衛星と受信機の間の擬似距離を求めるための測距信号
が含まれています。準天頂衛星システム「みちびき」(QZSS: Quasi-Zenith Satellite System)は、自らの航法メッセージの真正性を検証するためのデジタル署名を、航法メッセージに含めて配信しています。この航法メッセージ認証の仕組みをQZNMA(QZSS Navigation Message Authentication)と呼びます。さらに、みちびきのL6E信号に含まれるQZNMAメッセージから署名データを取得し、公開鍵と対象の航法メッセージを用いて検証することで、米国のGPSと欧州のGalileoの航法メッセージについても同様の認証が可能です。
今回、QZNMAを処理するサンプルプログラムをビルドし、同梱のデータで認証処理を試してみました。
公開鍵とデモプログラムの入手
みちびきの信号認証サービスはSAS(Signal Authentication Service)と呼ばれます。みちびきのSASに関する技術仕様はIS-QZSS-SAS-001(2024-05-27発行)に記載されています。
IS-QZSS-SAS-001の図3-1の右側のブロック図から、受信機でみちびき自身の航法メッセージを認証するには、公開鍵(Public Key)、参照認証航法データ(RAND: Reference Authentication Navigation Data)、デジタル署名(DS: Digital Signature)が必要であることがわかります。

受信機は航法メッセージから参照航法メッセージ(RNAV)を抽出し、マスク処理を施したうえで鍵IDやSALTなどを加えてRANDを生成します。DSは、航法メッセージに含まれる署名データから復元します。一方、公開鍵の入手については仕様書の7.3.5節に次のように書かれています。
7.3.5 Distribution of Public Key
Those who wish to have public keys please confirm the “MICHIBIKI website” of Cabinet Office.
私は、この仕様書のドラフト版(IS-QZSS-SAS-Draft-002、2023-01-24発行)が公開された際、内閣府に問い合わせて、2024年4月には公開鍵を入手していました。

資料は2枚のDVDからなり、1枚目には公開鍵(DER形式で、約96バイトのファイルが108個)が、2枚目にはサンプルプログラムが、それぞれ収録されていました。
自分で認証コードの作成に挑戦しようと思っていたところ、公開鍵だけでなくサンプルプログラムもいただけて、ありがたかったです。
サンプルプログラムのビルド
しかし、このサンプルプログラムは多くのファイルからなり、何から手をつければよいのかわからず、2年以上にわたり放置してしまいました。
srcディレクトリにCMakeLists.txtというファイルがありましたので、cmakeコマンドで必要なツールの検出とMakefileの生成を行い、makeコマンドでビルドすればよいというところまではわかりました。しかし、当初は、表示されるエラーの意味を理解できなかったため、ビルドを諦めていました。今回、ビルドに再挑戦しました。
このプログラムが前提にしている環境は、Windows(MinGW)とLinux(x86_64)です(src/platform/)。プログラムはC++で書かれています。設定ファイルはJSON形式で、GoogleTestによるユニットテストやサンプルデータも含まれています。私は、KVM上の仮想マシンで動作する64ビットのDebian GNU/Linux 13.7(trixie)でこのプログラムをビルドしました。
uname -a
Linux himawari 6.12.111+deb13-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.12.111-1 (2026-09-28) x86_64 GNU/Linux
ビルドに必要なソフトウェアであるcmake、clang-17、g++-12(GCC 12)はaptコマンドで導入しました。cmake実行中に参照されるsrc/platform/x86_64-linux/env.shとsrc/platform/x86_64-linux/pre.cmakeでClangのバージョンを指定します。
当初は、Clang 17がGCC 14の標準C++ライブラリ(libstdc++)のヘッダを参照していたため、多くのコンパイルエラーに悩まされました。そこで、Clang 17がGCC 12のヘッダとライブラリを参照するように設定しました。CMakeLists.txtには、この組み合わせを使う理由について次のコメントがあります。
Clang-17 cannot parse the C++23
of GCC 13/14’s libstdc++. Pin to GCC 12’s older libstdc++ headers, which Clang-17 handles. libstdc++ is ABI-compatible, so the prebuilt libstdc++-based static libs (fmt, ftxui) link.
また、srcディレクトリ下にversionという名前のファイルを新規作成しなければなりません。このファイルの内容は、次のコードでバージョン番号として読み込まれます。今回は仮のバージョン番号として1.0.0と書いておきました。
file(READ "${CMAKE_SOURCE_DIR}/version" version)
string(REGEX MATCH "([0-9]*).([0-9]*).([0-9]*)" _ ${version})
set(PRODUCT_VERSION ${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3})
さらに、Linux環境でビルドするためには、CMakeLists.txtにある次の行をコメントアウトする必要があります。
set(CMAKE_SYSTEM_NAME Win32)
また、src/srcとsrc/src-importedのソースコード群内にある無修飾の関数呼び出しformat()は、引数によってはfmt::format()と標準ライブラリのstd::format()の両方が候補になり、コンパイルエラーの原因となります。そこで、同梱の外部ライブラリfmtを明示的に呼び出すように、fmt::format()へ修正しました。
以上の修正を行い、srcの親ディレクトリで次のコマンドを実行してビルドします。
cmake -S src -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build -j$(nproc)
ユニットテストに用いるGoogleTestは、cmake実行時にCMakeLists.txtにあるinclude(ImportGTest)から.cmake_modules/ImportGTest.cmakeに記述されたダウンロードとビルドが自動的に実行されるようになっていました。GoogleTestのプログラムが同梱されていないのに、GoogleTestが実行できることを不思議に思っていました。また、CTestはCMakeに付属するテスト実行ツールで、Valgrindはメモリーリークなどを検出する動的解析ツールです。
ビルドに成功すると、buildディレクトリ以下に、
sas_simple_evaluator(メインの実行ファイル)test_simple_evaluator(ユニットテストの実行ファイル)libfec.so(リードソロモン誤り訂正用の共有ライブラリ)
などが作成されます。
ユニットテスト
ビルドに成功したら、同梱のユニットテストで動作を確認します。以降は、cd buildでbuildディレクトリに移動して作業します。
src/test/test_data.hには、
#if UNIX
#if 1
const pair<string, string> v_serial_port1("/tmp/pts1-1", "/tmp/pts1-2");
const pair<string, string> v_serial_port2("/tmp/pts2-1", "/tmp/pts2-2");
#else
const pair<string, string> v_serial_port1("/tmp/pts3-1", "/tmp/pts3-2");
const pair<string, string> v_serial_port2("/tmp/pts4-1", "/tmp/pts4-2");
#endif
#else
const pair<string, string> v_serial_port1("COM3", "COM4");
const pair<string, string> v_serial_port2("COM5", "COM6");
const pair<string, string> v_serial_port1_path("/dev/ttyS2", "/dev/ttyS3");
const pair<string, string> v_serial_port2_path("/dev/ttyS4", "/dev/ttyS5");
#endif
と書かれており、シリアル通信を含むテストには、2対の実シリアルポートまたは仮想シリアルポートが必要です。この設定から、サンプルプログラムがシリアルポート経由で受信機と通信することを想定しているようです。ここでは、仮想シリアルポートを作成するために、socatコマンドをaptコマンドで導入して、/tmp/pts1-1と/tmp/pts1-2、/tmp/pts2-1と/tmp/pts2-2という2対の仮想シリアルポートを作成しました。
socat -d -d pty,raw,echo=0,link=/tmp/pts1-1 pty,raw,echo=0,link=/tmp/pts1-2 &
socat -d -d pty,raw,echo=0,link=/tmp/pts2-1 pty,raw,echo=0,link=/tmp/pts2-2 &
また、設定ファイルやテストデータを参照するために、srcディレクトリ下にあるresourceディレクトリにアクセスする必要があるので、buildディレクトリ内でln -s ../src/resource resourceを実行し、そのシンボリックリンクを作成します。
ユニットテストは次のコマンドで行います。
ctest -E "^FT_" --output-on-failure
当初、ctest --output-on-failureでユニットテストを実行しましたが、FT_で始まる統合機能テストに失敗しました。これは、いくつかのテストモジュールが同梱されていないことによるようですので、これらのテストを除外するために-Eオプションを追加しました。
サンプルデータの処理
次は、buildディレクトリ内でサンプルデータ処理を試します。サンプルデータは、resource/nmea/20230413_air.nmeaにありますが、標準的なNMEA 0183センテンスに加え、受信機独自のセンテンスや航法メッセージなどのバイナリーデータも含んでいます。記録された座標から、福岡県福岡市のRKB毎日放送付近で収録したデータのように見えます。データの一部を次に示します。
resource/nmea/20230413_air.nmea
$GPGGA,045334.00,3335.5499725,N,13020.9967462,E,1,12,1.8,27.5860,M,32.47,M,,*59
$GPZDA,045334.00,13,04,2023,,*66
$GPGSV,2,1,07,01,87,255,49,17,19,281,39,07,34,220,43,08,36,065,42*7C
$GPGSV,2,2,07,21,61,038,46,03,90,000,39,10,90,000,36*4E
$GLGSV,2,1,05,74,17,160,39,86,54,276,49,69,05,046,42,75,70,189,50*64
$GLGSV,2,2,05,85,52,007,50*58
$MSG,SA6,26,01,255,87,49,50,50,49,52,00,U,17,281,19,39,34,35,41,00,00,M,07,220,3
4,43,43,43,43,00,00,M,08,065,36,42,34,34,46,49,00,M,21,038,61,46,37,37,00,00,00,
U,03,000,90,39,37,37,42,43,00,E,10,000,90,36,21,21,41,41,00,E,194,146,73,45,43,5
0,51,48,47,U,195,078,84,48,44,49,50,49,48,U,199,000,90,39,00,00,00,00,44,E,50,18
6,51,40,00,00,00,00,00,-,43,000,90,43,00,00,00,00,00,E,35,000,90,41,00,00,00,00,
00,E,74,160,17,39,00,00,00,00,-07,M,86,276,54,49,00,47,00,00,-03,U,69,046,05,42,
00,37,00,00,01,M,75,189,70,50,00,49,00,00,00,U,85,007,52,50,00,47,00,00,04,U,102
,134,62,47,52,49,51,00,00,U,207,325,66,44,45,47,00,00,00,U,210,000,90,42,45,46,0
0,00,00,E,235,290,58,46,48,00,46,48,48,U,238,185,36,43,44,00,43,45,44,M,240,355,
64,46,47,00,46,49,48,U,260,000,90,43,00,00,00,00,41,E,245,194,53,46,49,00,46,50,
48,U*2B
このサンプルデータを読み込むために、設定ファイルbuild/resource/config.jsonのtestFilePathにresource/nmea/20230413_air.nmeaを指定します。そして、この設定ファイルを引数にして、sas_simple_evaluatorを実行します。
./sas_simple_evaluator resource/config.json
実行結果は、outputディレクトリ以下に、検査項目と日時ごとに出力されます。ディレクトリ名の日時は、このプログラムを実行した時のものです。
output
├── analyzed_navs
│ └── 20261003_045743
├── capture
│ └── 20261003_045743
├── debug_log
│ └── 20261003_045743
├── error_report
│ └── 20261003_045743
├── event
│ └── 20261003_045743
├── kpi
│ └── 20261003_045743
├── positioning
│ └── 20261003_045743
└── validation_result
└── 20261003_045743
例えば、このvalidation_result/20261003_045743には、検証結果-GNSS-Q194-E1b.csvのようなファイルが並びます。この中身は、
194,E1b,2026/10/03 04:58:32,130°20′59.923″,33°35′32.947″,11.716900,44,101,,1,
未
のようになっていました。ファイル名から、みちびきがL6E信号で配信するQZNMAを用いた、Galileo E1-Bの航法メッセージの認証結果と考えられます。ただし、Q194やCSV内の衛星番号の対応は、この出力だけでは確認できませんでした。L6E信号のPRN番号はL1信号のものと異なるため、194をそのままL6EのPRN番号と解釈しないよう注意が必要です。末尾の「未」の文字について、src/src/core/type.hppには次のように変数定義されていますので、これは認証未完了を表しているようです。
namespace status {
using status_t = string;
inline status_t kNone = "";
inline status_t kSuccess = "〇";
inline status_t kVerificationFailed = "✕";
inline status_t kAuthNotCompleted = "未";
inline status_t kRefNotCompleted =
"△ "; // also alert enabled (cond. auth completed)
} // namespace status
今回の例では、validation_resultのCSVファイルで、末尾が「未」以外の結果は見つけられませんでした。収録時間が短いことが原因として考えられます。ぜひ自分で収録したデータで試し、認証成功を示す「〇」の文字を見てみたいです。
この例にあるNMEAデータをどの受信機で収録したのかはわかりません。同様のデータを収録するには、GPSやみちびきのL1C/A・L1C・L5信号、GalileoのE1-B・E5a信号の航法メッセージと、みちびきのL6E信号のQZNMAメッセージを取得する必要があります。手持ちの受信機では、Septentrio mosaic-CLASが候補です。受信機にこれらのデータを出力するよう設定して、受信機の生データをこのプログラムの入力形式に合わせる変換ツールを自作すれば、このプログラムをそのまま利用できそうです。
L6E信号も同時に受信できる受信機は限定されます。しかし、L6E信号と認証対象の航法メッセージは、別々の受信機で取得することもできます。例えばAllystar HD9310オプションC受信機の出力を、衛星番号と時刻を対応付けて別の受信機の出力と結合するツールでも、この航法メッセージ認証が実現できるかもしれません。
航法メッセージの署名検証の中心となるECDSA(楕円曲線デジタル署名アルゴリズム: Elliptic Curve Digital Signature Algorithm)は、同梱されているOpenSSLのものを利用しているようです。今後は、この署名検証の実装を読み解いていきたいと思います。
まとめ
みちびきの航法メッセージ認証QZNMAのサンプルプログラムをDebian GNU/Linux上でビルドして、サンプルデータを解析してみました。
fmtやFTXUIなどの外部ライブラリを利用するプログラムをビルドする中で、ライブラリやツールのバージョンを整合させたこのプログラマの努力を実感しました。今回はサンプルデータの解析と結果の出力まで確認できましたが、認証成功までは確認できませんでした。
今後は、自らのデータでこの航法メッセージ認証を試してみたいと思います。