BLEはペアリングすることなく通信することができますが、その状態ではデバイスの付近にいれば誰でも通信できてしまいます。そのため、製品化する場合には、ほとんどのケースでペアリングを実装する必要があります。
例えば、スマートウォッチでは、初回起動時にスマホとのペアリングをすることで、他者からアクセスできないようにしています。
BLEの開発をしていると、ペアリングに関する部分は低レイヤーなので、アプリケーションレベルで気にすることは少ないのですが、今回は実際にどのようにペアリングが行われているのかを掘り下げてみたいと思い、Wiresharkでキャプチャーしながら学ぶことにしました。
Bluetoothのデバイスを使用するとき、ユーザはペアリングをして、その後はペアリング作業なしに接続できるようにしたいと思います。このようにするためには、Bluetoothの仕様上、「ボンディング」が必要になります。
ペアリングとは、デバイスとの暗号化通信を行うための鍵を交換すること自体を言います。そして、ボンディングとは、ペアリングを実施し、生成された鍵を保存することで、以降の暗号化通信ができるようにしておくことを言います。
今回は、ボンディングも行うケースとなります。
BLEでは、暗号化はキャラクタリスティックの属性として設定します。そのため、1つのサービス内で、公開されたキャラクタリスティックと、暗号化されたキャラクタリスティックを混在させることができます。
ペアリングしていない状態で暗号化されたキャラクタリスティックにアクセスすると、 Error Code: Insufficient Encryption (0x0f)
というエラーがレスポンスされます。
実際にWiresharkで見てみるとこのようになります。
ちなみに、今回は次のような構成になっています。
デバイス | ペアリング時の役割 | BLEの役割 | Wireshark上の表示 | IO Capability |
iPhone | イニシエータ | セントラル | MACアドレス(ぼかしています) | DisplayKeyboard |
Raspberry Pi | レスポンダ | ペリフェラル | localhost() | DisplayOnly |
Raspberry Pi側は、次のような構成になっています。
encrypt-read
もしくは encrypt-write
を指定します。BLEでは、暗号化通信に使用する暗号鍵をペアリングで交換し、以降の通信で使います。この暗号鍵のことをLTK(Long Term Key)と呼び、ここからはLTKを交換するためのペアリングの流れについて説明します。
ペアリングは次の図の流れで行われます。
ペアリングリクエストを送った方がイニシエータとなり、その相手がレスポンダとなります。
イニシエータが暗号化されたキャラクタリスティックにアクセスすると、先ほどの通りエラーがレスポンスされますが、その際にセキュリティリクエストもレスポンスされます。
それを受けて、イニシエータ側はペアリングれリクエストを行います。
BLEペアリングには、次の2種類があります。
この2つの違いは、LTK(LongTerm Key)を無線通信上で交換するか否かになります。Legacy Pairingでは最初にSTK(Short Term Key)を生成し、STKによる暗号化通信でLTKを無線通信で交換します。
このため、legacy pairingではLTKが盗聴される可能性があります。しかし、Secure Connectionsであれば、LTKはPasskey認証などによって各デバイス上で生成されるため、無線通信上で交換する必要がなく、安全です。
今回は、Secure Connectionsの流れを見ていきたいと思います。
一番初めにレスポンダからセキュリティの要求が行われます。この通信は任意ですが、ペアリングリクエストと似たようなパラメータ構成となっています。
イニシエータからレスポンダにペアリングリクエストが送られることで、ペアリングが開始します。ここで、IO Capabilityをリクエスト時に送信し、レスポンダ側も自身のIO Capabilityをレスポンスします。
IO Capabilityとは、デバイスのもつ入手出力の特性のことで、主に次のように設定します。
今回、イニシエータのiPhoneはKeyboard Display、レスポンダのRaspberry PiはDisplay Onlyとしています。この場合、iPhone側には次のようなPasskey入力画面が表示され、Raspberry Pi側に出力された6桁のPasskeyを入力します。
Raspberry Pi側では、bt-agentを立ち上げている状態にしておくと、ターミナルにPasskeyが出力されるので、これをiPhoneで入力します。
bt-agentは先ほど説明した通り、デーモンで動いているBlueZと対話し、IO Capabilityの指定と、生成されたPasskeyの表示を行います。
Passkeyの生成や、ペアリング自体はBlueZ側が行うので、あくまでbt-agentはDBusを経由してBlueZと対話するのみとなります。
$ bt-agent -c DisplayOnly Agent registered Default agent requested Device: iPhone (00:00:00:00:00:00) Passkey: 186754, entered: 0
Wiresharkで見てみると、次のようになります。
まずは、ペアリングリクエストです。0x1
(Bonding)にすると、この後生成するLTK(Long Term Key)をお互いに保存しておき、次回以降ペアリングが不要となります。
レスポンスは次のようになります。
お互いのIO Capabilityの組み合わせによって、認証時のユーザによる作業が決定します。
正しくPasskeyが入力されたかを検証します。検証用の公開鍵の交換、Passkeyの検証、DH Key検証の3ステップで行われます。
先ほどの通り、PasskeyからLTKを生成できるので、Passkeyの検証を通過すれば、LTKが有効であることが確認され、ペアリングが完了します。
生成されたLTKは、Raspberry PiではBlueZに保存されます。以下のコマンドを使うと、実際にペアリングされたデバイスを確認することができます。
$ bluetoothctl Agent registered [bluetooth]# devices Device 00:00:00:00:00:00 My iPhone
iPhoneでは、設定画面のBluetoothでペアリング済みデバイスとして表示されます。
ペアリングが完了すると、暗号化されたキャラクタリスティックを読み込むことができました。
Raspberry PiでWiresharkを使うと、Bluetoothのパケットをキャプチャーすることができます。導入は非常に簡単です。
$ sudo apt install wireshark $ sudo wireshark
これでWiresharkが起動するので、 bluetooth0
を選択すると、Bluetoothの通信をキャプチャすることができます。
BLEはプロトコルスタックで、実際には様々なプロトコルの通信が行われているため、プロトコルによるフィルタリングは便利でした。
例えば、SMPプロトコルのみ表示するには、 btsmp
で絞り込みます。また、ATTプロトコルを絞り込むには btatt
を指定します。
その他にも、Bluetoothのフィルター条件がたくさんあるので、右側の「書式…」ボタンを押せば、他のフィルターも探すことができます。
次回はBlueZやbluetoothctlの仕組みについて深堀していきたいと思います!