Bezpieczeństwo BLE
Komunikacja bezprzewodowa, jakkolwiek wygodna by była, niesie ze sobą ryzyko podsłuchu. Każdy w zasięgu transmisji może się dowiedzieć, jakie parametry wysyłamy do płytki. Ten problem możemy rozwiązać, szyfrując transmitowane dane. Komunikację radiową wciąż będzie można przechwycić, ale zarejestrowane dane będą miały sens jedynie dla naszego urządzenia i smartfona.
Poza szyfrowaniem istotne jest także uwierzytelnianie, które potwierdza tożsamość urządzeń. Chroni ono przed atakami typu man-in-the-middle (MITM), w których ktoś próbuje podszyć się pod urządzenie, z którym się komunikujemy.
Całe szczęście standard BLE sam w sobie oferuje już mechanizmy zapewniające ochronę danych, a stos BLE w Zephyrze całkowicie je implementuje. Nasza rola ograniczy się więc jedynie do odpowiedniej konfiguracji i użycia tych mechanizmów.
Z punktu widzenia kodu projektu w BLE mamy 4 poziomy bezpieczeństwa połączenia:
- Level 1 (BT_SECURITY_L1): Brak szyfrowania i uwierzytelnienia.
- Level 2 (BT_SECURITY_L2): Jedynie szyfrowanie, brak uwierzytelnienia.
- Level 3 (BT_SECURITY_L3): Szyfrowanie i uwierzytelnienie.
- Level 4 (BT_SECURITY_L4): Zaawansowane bezpieczeństwo, w tym LE Secure Connections i bezpieczna wymiana kluczy przy użyciu algorytmu ECDH.
Można się spotkać jeszcze z poziomem 0, ale jest on używany w tradycyjnym Bluetooth, więc nas nie dotyczy.
Oto jak, w przybliżeniu, wygląda proces łączenia dwóch urządzeń:
- Po włączeniu nasza płytka rozpoczyna rozgłaszanie. Odbywa się to bez szyfrowania.
- Smartfon wykrywa płytkę podczas skanowania.
- Na życzenie użytkownika rozpoczyna się łączenie.
- W procesie parowania następuje wymiana kluczy, po uprzednim uwierzytelnieniu, jeśli jest ono wymagane.
Nasze urządzenie dotychczas zezwalało od razu na połączenie, ale w przypadku bezpieczniejszego podejścia wymagany jest proces parowania. W jego trakcie obie strony wymienią klucze szyfrujące, pozwalające na kodowanie i dekodowanie wiadomości.
Testowanie bezpieczeństwa BLE w projekcie
Dodajmy wyświetlenie poziomu bezpieczeństwa połączenia z telefonem zaraz po wysłaniu nowej wartości z aplikacji nRF Connect. Zmieniony początek funkcji przedstawia listing 1.
const struct bt_gatt_attr *attr,
const void *buf, uint16_t len,
uint16_t offset, uint8_t flags) {
LOG_HEXDUMP_INF(buf, len, „Received buffer:”);
LOG_DBG(„BT_SECURITY_L%d”, bt_conn_get_security(conn));
Listing 1. Zmiana w pliku bt_led_svc.c
Warto zauważyć, że wyświetlenie tej informacji odbywa się poprzez makro LOG_DBG a nie LOG_INF ponieważ nie jest ona dla nas jakoś specjalnie istotna. W przyszłości, aby nie zaśmiecać loga, będziemy mogli przestawić poziom logowania z LOG_LEVEL_DBG na LOG_LEVEL_INF i odfiltrować mniej istotne wiadomości. Polecamy zrobić to już w pliku main.c – zmienić ustawienia logowania w linijce 8 z LOG_MODULE_REGISTER(main, LOG_LEVEL_INF) na LOG_MODULE_REGISTER(main, LOG_LEVEL_WRN). Pozbędziemy się w ten sposób z loga niepotrzebnych już wiadomości „Tick”.
Ustalanie stopnia zabezpieczenia danych odbywa się na poziomie definicji charakterystyki. Zmieniając BT_GATT_PERM_WRITE na BT_GATT_PERM_WRITE_ENCRYPT, zablokujemy możliwość zapisu charakterystyki, jeśli połączenie BLE ma poziom bezpieczeństwa bez szyfrowania (Level 1), czyli taki, jaki mamy obecnie.
Sprawdźmy zatem, czy zabezpieczenia BLE działają. Listing 2 zawiera definicje serwisu, w którym podniesiono wymagany poziom zabezpieczeń charakterystyki odpowiedzialnej za częstotliwość migania diody.
BT_GATT_SERVICE_DEFINE(led_service,
BT_GATT_PRIMARY_SERVICE(BT_UUID_LED_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_LED_STATE_CHR,
BT_GATT_CHRC_WRITE, BT_GATT_PERM_WRITE,
NULL, write_chr_cb, NULL),
BT_GATT_CHARACTERISTIC(BT_UUID_LED_BLINK_CHR,
BT_GATT_CHRC_WRITE, BT_GATT_PERM_WRITE_ENCRYPT,
NULL, write_chr_cb, NULL)
);
Listing 2. Podniesiony poziom bezpieczeństwa drugiej charakterystyki – plik bt_led_svc.c
Po wgraniu nowego programu na płytkę kontynuujemy eksperyment. Tak jak w poprzedniej części kursu, podłączmy się do płytki za pomocą aplikacji nRF Connect, a następnie wyślijmy wartość „0” charakterystyce odpowiedzialnej za stan diody. Dioda zgaśnie, w logu zobaczymy potwierdzenie odebrania danych.
Jednak po wpisaniu jakiejkolwiek wartości do charakterystyki sterującej częstotliwością migania w logu stan diody się nie zmienia. Połączenie BLE może nawet zostać zakończone, co można zauważyć na listingu 3. Oczywiście to nasza wina. Zapis tej charakterystyki ustawiliśmy na poziom 2, a poziom połączenia to 1 (mamy o tym informację w logu: „BT_SECURITY_L1”).
[00:00:01.270,263] <dbg> bt_control: bt_ready_cb: BLE MAC: FD:96:31:72:1E:81 (random)
[00:00:01.272,033] <dbg> bt_control: bt_ready_cb: BLE advertisement started
[00:00:22.031,250] <inf> bt_control: connected_cb: Connected to: 52:D2:CE:FF:97:6B (random)
[00:00:48.432,342] <inf> bt_led_svc: write_chr_cb: Received buffer:
00 00 00 00 |....
[00:00:48.432,373] <dbg> bt_led_svc: write_chr_cb: BT_SECURITY_L1
[00:00:48.432,373] <inf> bt_led_svc: write_chr_cb: Led state update: 0
[00:00:48.432,495] <inf> led_control: led_worker: LED_STATE 0 received
[00:01:08.457,580] <inf> bt_control: disconnected_cb: Disconnected. Reason: 19
Listing 3. Log procesu zapisu do obu charakterystyk sterowania diodą
Zauważmy, że przy zapisie drugiej charakterystyki nawet nie doszło do wywołania funkcji obsługującej zapis, a jedyne, co zrobiliśmy, to zaznaczenie, że te dane mogą być wymieniane tylko w ramach szyfrowanego połączenia. Całą resztą zajął się stos BLE.
Security Manager Protocol (SMP)
Bezpieczeństwem w BLE zajmuje się Security Manager Protocol. Jest odpowiedzialny za zarządzanie szyfrowaniem i parowaniem urządzeń BLE. Umożliwia ustalanie kluczy szyfrujących oraz zapewnia mechanizmy i procedury zabezpieczające.
W Zephyrze SMP włącza się poprzez Kconfig, w pliku prj.conf, poprzez dodanie linii CONFIG_BT_SMP=y, co też uczynimy. I to w zasadzie wszystko, co jest wymagane. SMP zajmie się parowaniem. Dodajmy jednak powiadomienie o zmianie poziomu bezpieczeństwa. Zrobimy to za pomocą znanej nam już struktury conn_callbacks i nowej funkcji zwrotnej (listing 4).
{
if (!err) {
LOG_INF(„Security changed to BT_SECURITY_L%d”, level);
} else {
LOG_ERR(„Security change failed.”);
}
}
static struct bt_conn_cb conn_callbacks = {
.connected = connected_cb,
.disconnected = disconnected_cb,
.security_changed = security_changed_cb
};
Listing 4. Plik bt_control.c. Dodanie informacji o zmianie poziomu bezpieczeństwa
Po kompilacji przeprowadźmy jeszcze raz nasz test. Log nowego testu pokazuje listing 5.
[00:00:01.472,167] <dbg> bt_control: bt_ready_cb: BLE MAC: FD:96:31:72:1E:81 (random)
[00:00:01.474,060] <dbg> bt_control: bt_ready_cb: BLE advertisement started
[00:00:06.109,527] <inf> bt_control: connected_cb: Connected to: 58:F0:DE:60:60:0F (random)
[00:00:22.680,541] <inf> bt_led_svc: write_chr_cb: Received buffer:
00 00 00 00
[00:00:22.680,572] <dbg> bt_led_svc: write_chr_cb: BT_SECURITY_L1
[00:00:22.680,572] <inf> bt_led_svc: write_chr_cb: Led state update: 0
[00:00:22.680,694] <inf> led_control: led_worker: LED_STATE 0 received
[00:00:41.543,212] <inf> bt_control: security_changed_cb: Security changed to BT_SECURITY_L2
[00:00:42.143,280] <inf> bt_led_svc: write_chr_cb: Received buffer:
f4 01 00 00
[00:00:42.143,310] <dbg> bt_led_svc: write_chr_cb: BT_SECURITY_L2
[00:00:42.143,341] <inf> bt_led_svc: write_chr_cb: Led blink update: 500
[00:00:42.143,432] <inf> led_control: led_worker: LED_BLINK 500ms received
Listing 5. Log procesu zapisu do obu charakterystyk sterowania diodą
Teraz podczas zapisywania drugiej charakterystyki SMP przeprowadzi proces parowania. Na telefonie powinno pojawić się okienko pytające o naszą zgodę (podobne jak na rysunku 1). Potem poziom bezpieczeństwa zostanie podniesiony do BT_SECURITY_L2 i odbędzie się zapis charakterystyki.
Wyższy poziom bezpieczeństwa
Aby osiągnąć jeszcze wyższy poziom bezpieczeństwa, musimy aktywnie uczestniczyć w procesie parowania. Ponieważ płytka deweloperska nie ma wbudowanej klawiatury, nie możemy na niej wprowadzić kodu PIN (Passkey). Zamiast tego możemy wyświetlić kod na konsoli płytki i ręcznie przepisać go na telefonie.
Zaczynamy od ustawienia wyższego poziomu zabezpieczeń dla pierwszej charakterystyki LED. Ustawimy poziom wymagający uwierzytelnienia (BT_GATT_PERM_WRITE_AUTHEN) tak, jak pokazano na listingu 6. Jeśli oba urządzenia obsługują co najmniej Bluetooth 4.2, proces parowania automatycznie osiągnie poziom 4 zabezpieczeń (LE Secure Connections), co zapewni najwyższy poziom ochrony.
BT_GATT_SERVICE_DEFINE(led_service,
BT_GATT_PRIMARY_SERVICE(BT_UUID_LED_SERVICE),
BT_GATT_CHARACTERISTIC(BT_UUID_LED_STATE_CHR,
BT_GATT_CHRC_WRITE, BT_GATT_PERM_WRITE_AUTHEN,
NULL, write_chr_cb, NULL),
BT_GATT_CHARACTERISTIC(BT_UUID_LED_BLINK_CHR,
BT_GATT_CHRC_WRITE, BT_GATT_PERM_WRITE_ENCRYPT,
NULL, write_chr_cb, NULL)
);
Listing 6. Podniesiony poziom bezpieczeństwa pierwszej charakterystyki – plik bt_led_svc.c
Zephyr umożliwia sprawne przeprowadzenie parowania, wymagając jedynie dodania kilku funkcji do obsługi tego procesu. Konfiguracja opiera się na rejestrowaniu odpowiednich callbacków, bardzo podobnie jak w przypadku zdarzeń połączenia i rozłączenia, które omawialiśmy w poprzednim odcinku (listing 7).
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF(„Passkey for %s: %06u”, addr, passkey);
}
static void auth_cancel_cb(struct bt_conn *conn) {
LOG_INF(„Pairing cancelled.”);
}
static struct bt_conn_auth_cb conn_auth_callbacks = {
.passkey_display = auth_passkey_display_cb,
.passkey_entry = NULL,
.cancel = auth_cancel_cb
};
Listing 7. Konfiguracja zdarzeń uwierzytelniania. Plik bt_control.c
Rejestracja callbacków też następuje w funkcji bt_ready_cb (listing 8).
bt_conn_cb_register(&conn_callbacks);
//Register pairing callbacks
err = bt_conn_auth_cb_register(&conn_auth_callbacks);
if (err) {
LOG_ERR(„Failed to register authorization callbacks.”);
}
Listing 8. Rejestracja zdarzeń uwierzytelniania. Część funkcji bt_ready_cb() w pliku bt_control.c
Sposób przeprowadzenia uwierzytelnienia zależy od zawartości struktury conn_auth_callbacks. Przykładowo – gdyby to do pola .passkey_entry przypisany był odpowiedni callback, to na telefonie wyświetliłby się kod PIN, a na płytce musielibyśmy go wprowadzić ręcznie.
W ten sam sposób dodajmy jeszcze callbacki informujące o zakończeniu parowania wraz z ich rejestracją (listing 9 i listing 10).
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_INF(„Pairing completed: %s, bonded: %d”, addr, bonded);
}
static void pairing_failed_cb(struct bt_conn *conn, enum bt_security_err reason) {
char addr[BT_ADDR_LE_STR_LEN];
bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
LOG_ERR(„Pairing failed conn: %s, reason %d”, addr, reason);
}
static struct bt_conn_auth_info_cb conn_auth_info_callbacks = {
.pairing_complete = pairing_complete_cb,
.pairing_failed = pairing_failed_cb,
};
Listing 9. Konfiguracja zdarzeń parowania. Plik bt_control.c
bt_conn_cb_register(&conn_callbacks);
//Register pairing callbacks
err = bt_conn_auth_cb_register(&conn_auth_callbacks);
if (err) {
LOG_ERR(„Failed to register authorization callbacks.”);
}
//Register pairing info callbacks
err = bt_conn_auth_info_cb_register(&conn_auth_info_callbacks);
if (err) {
LOG_ERR(„Failed to register authorization info callbacks.”);
}
Listing 10. Rejestracja zdarzeń parowania. Część funkcji bt_ready_cb() w pliku bt_control.c
Została nam jeszcze jedna sztuczka. Zamiast przepisywać losowy kod PIN z konsoli do telefonu, przy każdym parowaniu ustawimy go sobie na stałe. Ale uwaga! Takie rozwiązanie nie jest do końca bezpieczne. Ponieważ wpisywanie pinu odbywa się, zanim nawiążemy bezpieczne połączenie, nic nie chroni pinu przed pasywnym podsłuchem. Ustawianie Passkey dodamy do funkcji inicjalizującej Bluetooth (listing 11).
LOG_DBG(„BLE init...”);
#ifdef CONFIG_BT_FIXED_PASSKEY
int err = bt_passkey_set(777777);
if (err) {
LOG_ERR(„Unable to set passkey (err: %d)”, err);
}
#endif
Listing 11. Ustawianie stałego PIN
Ponadto należy włączyć tę opcję w konfiguracji projektu (prj.conf) poprzez dodanie linii CONFIG_BT_FIXED_PASSKEY=y. Jeśli tę opcję usuniemy lub zakomentujemy, PIN znowu będzie losowy.
Po skompilowaniu i wgraniu nowego firmware'u możemy łatwo sprawdzić proces parowania, używając standardowego menu Bluetooth smartfona. Po wybraniu naszego urządzenia Led control pojawi się nowe okienko parowania (rysunek 2).
Log nowego procesu parowania pokazano na listingu 12. Od tego momentu możemy bezpiecznie sterować naszą diodą LED z aplikacji nRF Connect.
[00:00:02.398,071] <dbg> bt_control: bt_ready_cb: BLE MAC: FD:96:31:72:1E:81 (random)
[00:00:02.399,963] <dbg> bt_control: bt_ready_cb: BLE advertisement started
[00:00:13.197,326] <inf> bt_control: connected_cb: Connected to: 55:D6:15:20:F5:D6 (random)
[00:00:13.714,904] <inf> bt_control: auth_passkey_display_cb: Passkey for 55:D6:15:20:F5:D6 (random): 777777
[00:00:26.885,406] <inf> bt_control: security_changed_cb: Security changed to BT_SECURITY_L4
[00:00:26.961,486] <inf> bt_control: pairing_complete_cb: Pairing completed: D0:1B:49:08:8B:21 (public), bonded: 1
uart:~$
Listing 12. Log parowania
Podsumowanie
W tej części kursu wprowadziliśmy mechanizmy zabezpieczeń w Bluetooth LE, skonfigurowaliśmy protokół SMP do parowania i szyfrowania oraz przetestowaliśmy różne poziomy bezpieczeństwa.
W następnej części będziemy kontynuować pracę z BLE, ale tym razem nasza płytka wystąpi w roli centralnego urządzenia (central), które będzie zarządzać połączeniami z innymi urządzeniami BLE. Do testów może być potrzebne peryferyjne urządzenie obsługujące Bluetooth 4.0 lub nowszy, np. inna płytka deweloperska, pilot Bluetooth (często nazywany „przyciskiem Bluetooth” na popularnych portalach aukcyjnych) albo klawiatura Bluetooth.
Krzysztof Kierys
Paweł Jachimowski