11.13. ペアリングとボンディング¶
ここまでに説明してきた内容では、すべてのバイトが平文のまま無線で送受信されます。BLE対応のノートPCを持った人が同じ部屋にいれば、誰でもアドバタイズチャネルを盗聴し、オープンな接続のホッピングシーケンスを追跡して、やり取りされるすべての読み取り、書き込み、通知を読み出すことができます。公開されている多くのセンサーデータ(バッテリー残量、周囲温度など)であればそれで構いません。しかし、2つのエンドポイント同士が秘密にしておきたいもの -- リレーを起動する制御レジスタ、パスワード、広く公開すべきでない測定値など -- については、リンクを暗号化する必要があり、理想的にはカメラが誰と通信しているのかを把握している必要があります。
BLEはペアリングとボンディングの両方を通じてこれを実現します。
11.13.1. ペアリング、ボンディング、暗号化¶
密接に関連する3つの概念があります。
暗号化が最終的な目標です。いったんリンクが暗号化されると、データチャネル上のすべてのパケットは2つのエンドポイントだけが解読できるようになり、盗聴者にはノイズにしか見えなくなります。
ペアリングは、暗号化で使用する鍵について両者が合意するために2つのエンドポイントが実行する手続きです。これは1回限りのやり取りで、リンク層が暗号化エンジンに供給する共有鍵素材を生成します。
ボンディングは、ペアリングが完了した後に鍵を不揮発性ストレージに永続化するという選択であり、同じ2つのデバイス間の次回の接続ではペアリングをスキップして直接暗号化に進めるようにします。
平たく言えば、ペアリングは「自己紹介をする」、ボンディングは「その紹介を覚えておく」、暗号化は「以後は内密に話す」ということです。
オープンなBLE接続上でのペアリングフロー。鍵交換が完了すると、リンク層はそれ以降のすべてのパケットを暗号化します。ボンディングは鍵をフラッシュに書き込む追加のステップです。¶
11.13.2. LE Secure Connections¶
BLEで使用される最新の鍵交換はLE Secure Connectionsで、楕円曲線ディフィー・ヘルマン鍵共有の上に構築されています。両者は一時的な鍵ペアを生成し、公開鍵側を交換し、その結果を自身の秘密鍵と組み合わせて同じ共有秘密に到達します -- これは、やり取りの全記録があっても盗聴者には計算できない秘密です。
古いLE Legacy方式は安全性が低く(やり取りの全体を取得した盗聴者は通常、鍵を復元できます)、古いペリフェラルとの後方互換性のためだけに存在します。aioble のデフォルトは最新の方式です(le_secure=True)。これは維持してください。
11.13.3. ペアリングの開始¶
centralは、すでにオープンしている接続に対して aioble.DeviceConnection.pair() を呼び出すことでペアリングを行います:
async with await device.connect() as connection:
await connection.pair(bond=True, le_secure=True, mitm=False)
# ... GATT work, now over an encrypted link ...
pair が戻った後、接続上の encrypted、authenticated、bonded、key_size の各属性は、ネゴシエートされた内容を反映します。
最も役立つ4つのキーワード引数:
bond=True-- 生成された鍵をフラッシュに保存し、同じ2つのデバイス間の次回の接続でペアリングハンドシェイクをスキップできるようにします。デフォルトはTrueです。le_secure=True-- LE Secure Connections を使用します。デフォルトはTrueです。オンのままにしてください。mitm=False-- 中間者(man-in-the-middle)保護を要求するかどうかを指定します。これには帯域外チャネル(一方に表示された数値コードをもう一方で確認する、パスキーを入力する、など)が必要で、ユーザーがペアリングハンドシェイク中の2つのデバイスが本当に思っているとおりのものかを検証できます。デフォルトはFalseです(MITM保護なし -- 受動的な盗聴者はリンクを読み取れませんが、接続を能動的にリダイレクトする攻撃者は自身をペアリングに割り込ませる可能性があります)。機密性のあるものにはTrueに設定してください。ただし、ペリフェラルが実際にIO機能をサポートしている必要があることに注意してください。io=3-- デバイスが申告するIO機能です。Bluetooth仕様では5種類が定義されています:0表示のみ、1表示 + yes/no、2キーボードのみ、3入力なし出力なし、4キーボード + 表示。UIを持たないカメラは通常3を申告します。カメラ自体にディスプレイがある場合は、アプリケーションが数値確認を表示して1を使用することもできます。両者のIO機能の組み合わせによって、本当のMITM保護が実現可能かどうかが決まります。
ペリフェラルは自分から pair を呼び出すことはありません -- centralが開始した内容に応答するだけです。特定のキャラクタリスティックに対して暗号化が必要かどうかは、それがGATTデータベースでどのように宣言されているかという性質です。暗号化要求のアクセスビットは低レベルの bluetooth API の一部であり、現在のところ aioble のキャラクタリスティックコンストラクタからは公開されていません。
11.13.4. ボンディング -- そして鍵が存在する場所¶
bond=True の場合、aioble はローカルファイルシステム上のJSONファイルに鍵を書き込みます。デフォルトのファイル名は ble_secrets.json で、カレントワーキングディレクトリからの相対パスで書き込まれます。起動直後のカメラでは _boot.py がすでにそのディレクトリを選択しています:カードがマウントされている場合は /sdcard、そうでない場合は /flash -- そのため、ファイルは /sdcard/ble_secrets.json または /flash/ble_secrets.json に配置されます。このファイルには、ボンディング済みのピアが次回再接続するときにリンクを再暗号化するために必要なエントリが、ピアのアイデンティティアドレスを含めて保持されます。
覚えておくべき非対称性が1つあります:鍵が変わると保存は自動的に行われますが、次回の起動時にファイルを読み込むことは行われません。以前ボンディングしたピアが認識されるように、起動時に(ペアリングやアドバタイズの前に)一度 aioble.security.load_secrets() を呼び出してください:
import aioble
aioble.security.load_secrets() # default path: ble_secrets.json
その後、ボンディング済みのピアが次に現れると、aioble は保存された鍵を再利用し、それ以上のハンドシェイクなしにリンクが暗号化されます。
鍵をフラッシュに保存することの実際的な2つの帰結:
デバイスの削除。 すべてのボンディング済みピアを忘れるには
ble_secrets.jsonを削除(または該当するエントリを削除)し、最初からペアリングし直してください。物理アクセスは鍵を漏洩させます。 カメラのファイルシステムにアクセスできる人は誰でもこのJSONを読み取れます。これは、ネットワーク側でTLS鍵について生じたのと同じ種類の制約です(運用:鍵、有効期限、トラブルシューティング):デバイスごとの鍵を使用し、保存されたいかなる鍵も復元可能なものとして扱い、鍵が秘密のままであることに頼るのではなく、失効させる能力(ここではcentral側でボンディングを削除すること)に頼ってください。
11.13.5. 暗号化が保証するもの -- そして保証しないもの¶
ペアリングしてから暗号化するリンクは、強度の高い順に次を提供します:
機密性。 常に提供されます。盗聴者はバイトを読み取れません。
完全性。 常に提供されます。改ざんされたパケットはリンク層の認証付き暗号化チェックに失敗し、破棄されます。
認証。
mitm=Trueと対応可能なIOがある場合に限ります。それがなければ、元のペアリングのやり取りを傍受した中間者が自身を割り込ませていた可能性があり、MITM保護がなければ両者がそれを知る術はありません。
ほとんどのカメラのユースケース -- 電話がカメラと一度ペアリングし、後で再び接続する -- では、mitm=False で通常は十分です。なぜなら、最初のペアリングは管理された環境で行われるからです(ユーザーが両方のデバイスを同じ部屋で持っています)。ペアリング済みのデバイスが最初に長距離越しに、または信頼できない中継者を介してカメラに遭遇する可能性があるアプリケーションでは、MITMが適切な設定です。
11.13.6. ペアリングが間違った答えになる場合¶
ペアリングには実際のコストがあります:初回接続時の数秒間のやり取り、ボンディングするデバイスごとの永続的なフラッシュ使用、そして何か問題が起きた場合の「ボンディングを忘れる」という復旧手順です。本当に公開してよいデータ -- ビーコンとして公開される周囲のセンサー値、自身の名前を表示する標識、読み取られたり書き込まれたりしても世界に何も影響を与えないもの -- については、正しい答えはまったく暗号化しないことであり、近くのスキャナーに値を自由に読ませることです。
それ以外のすべてについては、central側での connection.pair(bond=True) が、リンクを公開チャネルから内密のチャネルへと変える1行の追加です。