BLE 보안 연결과 페어링
BLE 연결은 기본적으로 암호화되지 않는다. 주변의 누구나 GATT 데이터를 스니핑할 수 있다. 페어링은 두 디바이스가 암호화 키를 교환하는 과정이고, 본딩은 그 키를 저장해서 재연결 시 다시 페어링하지 않는 것이다.
쉽게 말하면: BLE는 기본적으로 엽서와 같다. 누구나 내용을 읽을 수 있다. 페어링은 봉투에 넣고 봉인하는 것이고, 본딩은 다음에 또 보낼 때 같은 봉인을 재사용하는 것이다.
보안 없이 연결하면 어떻게 되는가
BLE 스니퍼(nRF Sniffer, Ubertooth 등)를 켜면 주변 BLE 디바이스의 advertising과 connection 데이터를 모두 볼 수 있다.
# Wireshark + nRF Sniffer로 캡처되는 것들:
- GATT Read/Write 데이터 (센서 값, 명령어)
- Notify/Indicate 데이터
- 디바이스 이름, 서비스 UUID
- 연결 파라미터
암호화 없이 BLE로 도어락 비밀번호, 의료 데이터, 결제 정보를 전송하면 평문으로 노출된다. 보안이 필요한 모든 BLE 어플리케이션은 페어링이 필수다.
카페에서 큰 소리로 통화하는 것과 같다. 옆 테이블에서 다 들린다. BLE 스니퍼는 10만원짜리 USB 동글이면 충분하다.
페어링 과정
페어링은 3단계로 진행된다. 처음 만난 두 사람이 암호 통신을 시작하는 과정이라고 생각하면 된다.
Phase 1: Feature Exchange (자기소개)
양쪽이 I/O 능력(키보드, 디스플레이 유무)과 보안 요구사항을 교환
→ "나는 화면이 있어" / "나는 버튼만 있어"
Phase 2: Key Generation (비밀 키 만들기)
Legacy Pairing: TK → STK 생성 (취약)
LE Secure Connections: ECDH → LTK 생성 (권장)
→ 양쪽이 수학적으로 같은 암호화 키를 만들어낸다
Phase 3: Key Distribution (키 저장)
LTK, IRK, CSRK 등 장기 키를 교환
본딩이 활성화되면 이 키들을 Flash에 저장
→ 다음에 만나면 이 키로 바로 암호화
Phase 1: I/O Capability
양쪽의 I/O 능력에 따라 페어링 방식이 자동 결정된다:
| NoInput NoOutput | DisplayOnly | DisplayYesNo | KeyboardOnly | KeyboardDisplay | |
|---|---|---|---|---|---|
| NoInput NoOutput | Just Works | Just Works | Just Works | Just Works | Just Works |
| DisplayOnly | Just Works | Just Works | Just Works | Passkey | Passkey |
| DisplayYesNo | Just Works | Just Works | Numeric Comp. | Passkey | Numeric Comp. |
| KeyboardOnly | Just Works | Passkey | Passkey | Passkey | Passkey |
| KeyboardDisplay | Just Works | Passkey | Numeric Comp. | Passkey | Numeric Comp. |
임베디드 디바이스는 대부분 NoInput/NoOutput → Just Works가 선택된다. MITM 방어가 안 된다.
표가 복잡해 보이지만 규칙은 단순하다: 양쪽 다 화면/키보드가 없으면 확인할 방법이 없으니 Just Works(무조건 연결)가 된다. 한쪽이라도 입력/확인 수단이 있으면 Passkey나 Numeric Comparison이 가능해진다.
페어링 방식별 보안
| 방식 | 동작 | MITM 방어 | 사용 상황 |
|---|---|---|---|
| Just Works | 확인 없이 키 교환 | X | 디스플레이/키보드 없는 센서, 비콘 |
| Passkey Entry | 6자리 숫자 입력 | O | 키패드가 있는 디바이스 |
| Numeric Comparison | 양쪽 6자리 확인 | O | 디스플레이가 있는 양쪽 디바이스 (BLE 4.2+) |
| OOB | NFC 등 다른 채널로 키 교환 | O | NFC 터치로 페어링 |
비유로 정리하면:
- Just Works = 모르는 사람이랑 악수만 하고 “우리 친구”. 중간에 누가 끼어도 모른다
- Passkey = 비밀번호를 아는 사람만 들어올 수 있는 문
- Numeric Comparison = 양쪽이 같은 숫자를 보고 “이거 맞지?” 확인. 중간자가 끼면 숫자가 달라진다
- OOB (NFC) = 물리적으로 터치해야 연결. 원격 공격이 불가능
Legacy Pairing vs LE Secure Connections
Legacy Pairing (BLE 4.0~4.1)
1. TK (Temporary Key) 생성
- Just Works: TK = 0 (고정!)
- Passkey: TK = 입력된 6자리 숫자 (최대 999,999)
2. TK로 STK (Short-Term Key) 생성
STK = s1(TK, Srand, Mrand)
3. STK로 연결 암호화
4. 암호화된 채널에서 LTK 배포
문제점:
- Just Works의 TK = 0. 페어링 과정을 캡처하면 STK를 역산할 수 있다
- Passkey의 TK도 20비트(0~999,999). 브루트포스로 수 초 내 해독 가능
- 페어링 과정을 스니핑하면 이후 모든 통신을 복호화할 수 있다 (passive eavesdropping)
Legacy의 근본적 문제: 비밀 키(TK)가 너무 약하다. Just Works는 TK가 0(= 비밀번호 없음)이고, Passkey도 최대 6자리 숫자(100만 가지)라서 컴퓨터로 수 초면 다 시도할 수 있다. 페어링 과정을 녹화해뒀다가 나중에 해독하는 것도 가능하다.
LE Secure Connections (LESC, BLE 4.2+)
1. ECDH 키 교환
양쪽이 P-256 공개키를 교환 → 공유 시크릿 DHKey 생성
2. DHKey로 LTK 직접 생성
LTK = f5(DHKey, N_master, N_slave, BD_ADDR_master, BD_ADDR_slave)
3. 인증
- Just Works: 확인 없음
- Passkey: 6자리 숫자로 DHKey 검증 (비트별 검증, 20라운드)
- Numeric Comparison: 양쪽 6자리 확인
4. LTK로 연결 암호화 (AES-CCM)
LESC가 나은 이유:
- ECDH라서 패시브 스니핑으로 키를 역산할 수 없다 (forward secrecy)
- STK 단계가 없다. LTK를 직접 생성
- Passkey도 비트별 20라운드 검증이라 브루트포스 불가
ECDH를 쉽게 설명하면: 두 사람이 각각 비밀 숫자를 가지고, 공개 숫자만 교환한다. 수학적 성질 덕분에 양쪽 모두 같은 공유 키를 계산할 수 있지만, 옆에서 공개 숫자만 봐서는 비밀 숫자를 역산할 수 없다. Legacy는 약한 비밀번호(TK)를 직접 기반으로 키를 만들지만, LESC는 ECDH로 강력한 키를 만들어서 도청만으로는 해독이 불가능하다.
| Legacy | LESC | |
|---|---|---|
| 키 교환 | TK → STK → LTK | ECDH → LTK |
| 패시브 스니핑 | 취약 (TK 브루트포스) | 안전 (ECDH) |
| Just Works MITM | 취약 | 취약 (여전히) |
| Passkey 강도 | 20비트 | 20라운드 검증 |
| 최소 BLE 버전 | 4.0 | 4.2 |
Just Works는 LESC에서도 MITM에 취약하다. 액티브 MITM(중간자가 양쪽과 각각 페어링)은 어떤 방법으로도 I/O 확인 없이는 막을 수 없다.
한 줄 요약: LESC는 도청은 막지만, 중간자 공격은 못 막는다. 중간자를 막으려면 사람이 직접 “이 상대가 맞는지” 확인하는 단계(Passkey, Numeric Comparison)가 반드시 필요하다.
본딩
페어링 후 교환된 키를 Flash에 저장하는 것이 본딩이다. 재연결 시 저장된 LTK로 즉시 암호화한다.
블루투스 이어폰을 한번 페어링하면 다음부터 자동 연결되는 것이 본딩이다. 처음 만든 암호화 키를 양쪽 Flash에 저장해두고, 재연결 시 그 키를 꺼내서 바로 암호화한다.
저장되는 키
| 키 | 용도 |
|---|---|
| LTK | 연결 암호화. 가장 중요 |
| IRK | RPA 주소 해석. 프라이버시 |
| CSRK | 서명 검증 (거의 사용 안 함) |
| Identity Address | 상대방의 실제 주소 (public 또는 static random) |
본딩 정보 관리
// STM32WB: 본딩 테이블 크기 설정
#define CFG_BLE_NUM_LINK 2 /* 동시 연결 수 */
#define CFG_BONDING_NUMBER 10 /* 최대 본딩 디바이스 수 */
// 본딩 삭제 (팩토리 리셋)
aci_gap_clear_security_db();
재연결 시퀀스:
1. Peripheral이 RPA로 advertising
2. Central이 IRK로 RPA resolve → 알려진 디바이스 확인
3. 연결
4. Central이 Encryption Request (저장된 LTK의 EDIV, Rand 전송)
5. Peripheral이 EDIV/Rand로 LTK 찾음
6. AES-CCM 암호화 활성화
7. 페어링 없이 암호화된 연결 완료
보안 레벨
BLE는 Security Mode 1에 4개 레벨을 정의한다:
| 레벨 | 암호화 | 인증 | 의미 |
|---|---|---|---|
| Level 1 | X | X | 암호화 없음 |
| Level 2 | O | X | 암호화, 인증 없음 (Just Works) |
| Level 3 | O | O | 암호화 + MITM 방어 (Passkey/Numeric/OOB) |
| Level 4 | O | O | Level 3 + LESC 필수 (BLE 4.2+) |
집에 비유하면:
- Level 1 = 문이 없는 집
- Level 2 = 자물쇠는 있는데, 누가 잠갔는지 확인 안 함 (도청 방어만)
- Level 3 = 자물쇠 + 신분증 확인 (도청 + 중간자 방어)
- Level 4 = 가장 튼튼한 자물쇠(LESC) + 신분증 확인
Characteristic에 보안 요구 설정
// STM32WB: Characteristic에 보안 레벨 지정
aci_gatt_add_char(
service_handle,
UUID_TYPE_128, &char_uuid,
20, /* max length */
CHAR_PROP_READ | CHAR_PROP_WRITE,
ATTR_PERMISSION_ENCRY_READ | /* 읽기: 암호화 필수 */
ATTR_PERMISSION_AUTHEN_WRITE, /* 쓰기: 인증(MITM 방어) 필수 */
GATT_NOTIFY_ATTRIBUTE_WRITE,
10,
CHAR_VALUE_LEN_VARIABLE,
&char_handle
);
| 퍼미션 | 의미 |
|---|---|
ATTR_PERMISSION_NONE | Level 1: 누구나 접근 |
ATTR_PERMISSION_ENCRY_READ | Level 2+: 암호화된 연결 필요 |
ATTR_PERMISSION_AUTHEN_READ | Level 3+: 인증된 페어링 필요 |
암호화 없는 연결에서 ENCRY_READ characteristic을 읽으려 하면 스택이 자동으로 페어링을 시작한다 (Security Request → Pairing).
STM32WB 페어링 설정
초기화
// I/O Capability 설정
aci_gap_set_io_capability(IO_CAP_DISPLAY_YES_NO);
// 인증 요구사항
aci_gap_set_authentication_requirement(
1, /* bonding_mode: 1 = bonding */
1, /* mitm_mode: 1 = MITM 보호 요구 */
1, /* sc_support: 1 = LE Secure Connections */
0, /* keypress_notification */
16, /* enc_key_size_min: 최소 암호화 키 크기 */
16, /* enc_key_size_max */
1, /* use_fixed_pin: 1 = 고정 핀 사용 */
123456, /* fixed_pin */
0x00 /* identity_address_type: public */
);
페어링 이벤트 처리
void aci_gap_pairing_complete_event(uint8_t conn_handle,
uint8_t status,
uint8_t reason)
{
if (status == 0x00) {
/* 페어링 성공 — 본딩 키가 Flash에 저장됨 */
} else if (status == 0x01) {
/* 페어링 타임아웃 */
} else if (status == 0x02) {
/* 페어링 실패 */
switch (reason) {
case 0x01: /* Passkey entry failed */ break;
case 0x02: /* OOB not available */ break;
case 0x03: /* Authentication requirements not met */ break;
case 0x04: /* Confirm value failed */ break;
case 0x05: /* Pairing not supported */ break;
case 0x06: /* Encryption key size too short */ break;
}
}
}
void aci_gap_numeric_comparison_value_event(uint8_t conn_handle,
uint32_t numeric_value)
{
/* 디스플레이에 numeric_value (6자리) 표시 */
/* 사용자가 확인하면: */
aci_gap_numeric_comparison_value_confirm_yesno(conn_handle, 0x01);
}
void aci_gap_pass_key_req_event(uint8_t conn_handle)
{
/* Passkey 입력 요청 — 사용자가 입력한 값을 전달 */
aci_gap_pass_key_resp(conn_handle, 123456);
}
보안 요청 (Peripheral → Central)
Peripheral이 먼저 암호화를 요청할 수 있다:
// 연결 후 보안 요청 전송
aci_gap_slave_security_req(conn_handle);
// → Central이 이미 본딩되어 있으면 LTK로 암호화
// → 본딩 안 되어 있으면 페어링 시작
MITM 공격 시나리오
두 가지 공격 유형을 구분해야 한다. 수비 전략이 완전히 다르다.
패시브 스니핑 (도청)
공격자가 중간에서 듣기만 한다. 통신에 개입하지 않는다.
[디바이스 A] ←── BLE ──→ [디바이스 B]
↑
[스니퍼]
(패킷 캡처만)
- Legacy Just Works: TK=0이므로 STK/LTK 역산 가능. 이후 모든 데이터 복호화
- Legacy Passkey: TK가 20비트. 오프라인 브루트포스로 해독
- LESC: ECDH라서 패시브 스니핑으로는 불가능
방어: LESC를 켜면 도청은 막을 수 있다. ECDH 덕분에 옆에서 아무리 들어도 키를 알아낼 수 없다.
액티브 MITM (중간자 공격)
공격자가 양쪽 사이에 끼어들어, 각각과 별도로 페어링한다. 양쪽 모두 정상 연결이라고 착각한다.
[디바이스 A] ←── BLE ──→ [공격자] ←── BLE ──→ [디바이스 B]
(양쪽과 각각 페어링)
- Just Works (Legacy/LESC 모두): 사용자 확인이 없으므로 MITM 성공
- Passkey: 공격자가 passkey를 모르면 실패
- Numeric Comparison: 양쪽에 다른 숫자가 표시되므로 사용자가 거부 가능
- OOB (NFC): 물리적 근접이 필요하므로 MITM 매우 어려움
방어: 사람이 직접 확인하는 수밖에 없다. LESC든 뭐든 암호화 알고리즘만으로는 “내가 진짜 상대와 연결된 건지” 확인할 수 없다. Passkey나 Numeric Comparison처럼 사람이 숫자를 보고 확인하는 단계가 있어야 중간자가 끼었을 때 알아챌 수 있다.
결론: Just Works는 편리하지만 MITM에 무방비. 보안이 중요하면 최소 Passkey, 이상적으로는 Numeric Comparison 또는 OOB를 사용한다.
실전 보안 설계
| 제품 유형 | 권장 설정 |
|---|---|
| 센서/비콘 (데이터 민감하지 않음) | Just Works + LESC, Level 2 |
| 웨어러블 (폰과 1:1 연결) | Passkey + LESC + 본딩, Level 4 |
| 도어락/결제 | OOB(NFC) 또는 Numeric Comparison + LESC, Level 4 |
| 의료기기 | Numeric Comparison + LESC + 본딩, Level 4 |
공통: LESC는 항상 켠다. Legacy Pairing을 허용할 이유가 없다 (BLE 4.2 이전 디바이스 호환이 필요한 경우 제외).
제품 설계 시 핵심 질문: “내 디바이스에 화면이나 버튼이 있는가?” 없으면 Just Works밖에 선택지가 없고, MITM은 포기하는 대신 LESC로 도청만 막는다. 보안이 정말 중요하면 하드웨어에 버튼/디스플레이를 추가하거나 NFC를 붙여야 한다.
메모
- “Just Works도 암호화는 된다”
- Just Works로 페어링해도 AES-CCM 암호화는 활성화된다. 패시브 스니핑은 LESC 사용 시 방어된다
- 문제는 MITM. 공격자가 중간에서 relay하면 양쪽 모두 정상 연결로 착각
- 보안 요구가 낮은 디바이스(온도 센서 등)에서는 Just Works + LESC로 충분할 수 있다
- 본딩 테이블 가득 찼을 때
- STM32WB 기본: 가장 오래된 본딩을 삭제하고 새 디바이스를 추가
- 일부 스택은 페어링을 거부한다.
CFG_BONDING_NUMBER를 적절히 설정
- iOS/Android 페어링 동작 차이
- iOS: Just Works 페어링 시 사용자에게 팝업을 띄우지 않는다. 자동으로 진행
- Android: Just Works도 “페어링 하시겠습니까?” 팝업을 띄운다
- iOS에서 Passkey를 사용하려면 Peripheral이
DisplayOnly이상의 I/O capability를 설정해야 한다
sc_support = 1로 설정해도 상대가 LESC를 지원하지 않으면 Legacy로 폴백sc_support = 2(SC only)로 설정하면 Legacy 폴백을 차단한다. 오래된 폰에서 연결 실패할 수 있음
- 고정 Passkey의 위험
use_fixed_pin = 1, fixed_pin = 123456은 개발용이다- 프로덕션에서 고정 passkey를 쓰면 한 번 알려지면 모든 디바이스에 적용 가능
- 디바이스마다 다른 passkey를 생성하거나 Numeric Comparison을 사용
- RPA와 보안의 관계
- 본딩 시 교환한 IRK로 RPA를 해석하여 재연결 상대를 식별한다
- RPA 없이 고정 주소를 쓰면 본딩과 무관하게 디바이스 추적이 가능하다