시작하며

NU40 보드와 Zephyr RTOS로 프로젝틀르 구성하였고, 블루투스(BLE)를 통해 보드에서 1, 2, 3, 4 버튼으로 바이브 코딩에서 선택하는 기능이 필요해졌다.

따라서 해당 기능에 대해서 정리하는 글을 작성해보았다.

페어링(Pairing)과 본딩(Bonding)의 차이

들어가기에 앞서 두 가지 개념을 확실히 짚고 넘어갈 필요가 있다.

  • 페어링 (Pairing): 두 기기가 서로 통신하기 위해 일시적으로 보안 키를 교환하고 암호화된 연결을 설정하는 과정이다. 연결이 끊어지면 키 정보가 사라진다.
  • 본딩 (Bonding): 페어링 과정에서 생성된 보안 키를 비휘발성 메모리(플래시)에 저장해 두는 것이다. 다음번에 다시 연결될 때는 번거로운 페어링 과정 없이 저장된 키를 이용해 즉시 암호화된 통신을 재개할 수 있다.

스마트폰에 블루투스 이어폰을 등록해두고 쓰는 것이 바로 ‘본딩’된 상태라고 보면 된다.

Zephyr 프로젝트 설정 (prj.conf)

Zephyr에서 BLE 보안 기능을 사용하려면 먼저 prj.conf 파일에 관련 설정(Kconfig)을 추가해야 한다.

# BLE 기본 설정
CONFIG_BT=y
CONFIG_BT_PERIPHERAL=y
CONFIG_BT_DEVICE_NAME="My_NU40"

# BLE 보안 및 페어링 활성화 (SMP - Security Manager Protocol)
CONFIG_BT_SMP=y

# 본딩(키 저장)을 위한 설정
CONFIG_BT_SETTINGS=y
CONFIG_FLASH=y
CONFIG_FLASH_PAGE_LAYOUT=y
CONFIG_FLASH_MAP=y
CONFIG_NVS=y
CONFIG_SETTINGS=y

CONFIG_BT_SMP=y가 보안 관리자(Security Manager)를 켜는 핵심 설정이다. 또한 본딩 정보를 저장하기 위해 플래시 메모리와 NVS(Non-Volatile Storage), Settings 서브시스템을 활성화해 주어야 한다.

보안 레벨 및 인증 콜백 구현

페어링 과정에서 화면에 핀 코드(Passkey)를 띄울 것인지, 아니면 그냥 수락(Just Works)할 것인지는 기기의 입출력(IO) 능력에 따라 결정된다. 화면이 없는 NU40 보드의 특성상 가장 간단한 Just Works 방식이나, 고정된 핀 코드를 사용하는 방식을 많이 쓴다.

Zephyr에서는 bt_conn_auth_cb 구조체를 통해 인증 콜백을 등록하여 페어링 이벤트를 처리한다.

#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>

/* 페어링 취소 시 호출되는 콜백 */
static void auth_cancel(struct bt_conn *conn)
{
	char addr[BT_ADDR_LE_STR_LEN];
	bt_addr_le_to_str(bt_conn_get_dst(conn), addr, sizeof(addr));
	printk("Pairing cancelled: %s\n", addr);
}

/* 페어링 완료/실패 시 호출되는 콜백 */
static void pairing_complete(struct bt_conn *conn, bool bonded)
{
	printk("Pairing Complete (Bonded: %d)\n", bonded);
}

static void pairing_failed(struct bt_conn *conn, enum bt_security_err reason)
{
	printk("Pairing Failed: %d\n", reason);
}

/* 인증 콜백 구조체 등록 */
static struct bt_conn_auth_cb auth_cb_display = {
	.cancel = auth_cancel,
};

static struct bt_conn_auth_info_cb auth_cb_info = {
	.pairing_complete = pairing_complete,
	.pairing_failed = pairing_failed,
};

void main(void)
{
	int err;

	/* 설정 로드 (본딩 데이터 복원) */
	settings_load();

	/* 블루투스 초기화 */
	err = bt_enable(NULL);
	if (err) {
		printk("Bluetooth init failed (err %d)\n", err);
		return;
	}

	/* 인증 콜백 등록 */
	bt_conn_auth_cb_register(&auth_cb_display);
	bt_conn_auth_info_cb_register(&auth_cb_info);

	printk("Bluetooth initialized\n");

	/* Advertising 시작 로직 생략... */
}

특성(Characteristic)에 보안 요구사항 추가

페어링이 실제로 일어나게 하려면, 특정 데이터를 읽거나 쓸 때 ‘보안 연결’을 요구하도록 설정해야 한다. GATT 테이블을 정의할 때 권한(Permission) 플래그를 수정하면 된다.

BT_GATT_SERVICE_DEFINE(my_service,
	BT_GATT_PRIMARY_SERVICE(BT_UUID_DECLARE_16(0x1234)),
	/* 읽거나 쓰려면 암호화된 연결(페어링)이 필요함 */
	BT_GATT_CHARACTERISTIC(BT_UUID_DECLARE_16(0x5678),
			       BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE,
			       BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT,
			       read_callback, write_callback, NULL),
);

BT_GATT_PERM_READ 대신 BT_GATT_PERM_READ_ENCRYPT를 사용했다. 이제 스마트폰이 이 특성에 접근하려고 시도하면, NU40은 에러(Insufficient Authentication)를 응답하고, 스마트폰은 이를 감지하여 자동으로 페어링 절차를 시작한다.

테스트 해보기

펌웨어를 빌드하고 NU40에 플래시한 뒤, 스마트폰에서 nRF Connect for Mobile 앱을 실행한다.

  1. 기기를 스캔하고 ‘blinky_ble’에 연결(Connect)한다.
  2. 연결은 성공하지만, 아직 암호화되지는 않은 상태다.
  3. 우리가 만든 0x5678 특성(Characteristic)의 읽기(Read) 화살표를 누른다.
  4. 순간적으로 스마트폰 하단에 “Bluetooth Pairing Request” 팝업이 뜬다.
  5. ‘Pair(페어링)’ 버튼을 누르면, 이전에 등록한 콜백 함수가 실행되며 콘솔에 Pairing Complete (Bonded: 1) 메시지가 출력된다.
  6. 이후부터는 해당 특성의 값을 정상적으로 읽고 쓸 수 있다.

보드를 재부팅하더라도 본딩 정보가 플래시에 저장되어 있기 때문에, 스마트폰의 블루투스 설정에서 기기 등록을 해제하지 않는 이상 다시 페어링 팝업이 뜨지 않고 바로 보안 통신이 가능하다.

페어링과 본딩이 완료 되었다면 이제 버튼을 누를 떄마다 1씩 증가하는 코드를 보내진다.

마무리

Zephyr 환경에서 BLE 보안을 적용하는 것은 처음엔 Kconfig 설정과 콜백 등록 때문에 복잡해 보일 수 있다. 하지만 Zephyr의 뛰어난 추상화 덕분에 몇 가지 설정과 권한 플래그만 변경해주면 손쉽게 페어링과 본딩 기능을 구현할 수 있었다.

다음 단계로는 우선 해당 보드에 디스플레이 및 스피커를 입력할 수 있는 방법을 연구해봐야겠다. 다행이도 블로그 글을 작성하는 모음에 이와 관련하여 진행을 하신분이 있어 많은 도움을 받을 수 있을 것 같다.

>> Home