링크 계층 암호화: 세션 키와 AES-CCM
1권 8장은 페어링으로 LTK를 만드는 데서 멈췄다. 이 장은 그 키가 실제로 패킷을 잠그는 과정을 끝까지 따라간다. 연결마다 새로 만드는 세션 키, 패킷마다 바뀌는 13바이트 nonce, 4바이트 MIC와 키스트림까지. 사양에 실린 암호화 샘플 패킷 네 개를 바이트 하나 틀리지 않게 다시 만들어 본다.
- 암호화 시작 절차에서 오가는 PDU와 HCI 명령·이벤트, 세션 키 SK = e(LTK, SKD)를 설명할 수 있다.
- CCM nonce(packetCounter·directionBit·IV)와 B·A 블록을 직접 만들고 MIC와 암호문을 계산할 수 있다.
- 헤더의 어느 비트가 MIC에서 빠지는지, 재전송이 왜 같은 nonce를 쓰는지, MIC 실패가 무엇을 끊는지 안다.
- 암호화 일시 정지로 키를 바꾸는 법과 CIS·BIS가 IV를 만드는 법을 안다.
세션 키: LTK는 한 번도 공중에 나가지 않는다
페어링이 끝나면 두 기기는 같은 LTK(Long Term Key)를 저장해 둔다. 하지만 링크 계층은 LTK로 패킷을 직접 잠그지 않는다. 연결할 때마다 양쪽이 무작위 값을 반씩 내놓아 새 세션 키(Session Key, SK)를 만들고 그것으로 잠근다. 같은 LTK로 백 번 연결해도 세션 키는 백 번 다르다.
금고 열쇠(LTK)는 금고에만 두고 들고 다니지 않는다. 매일 아침 두 사람이 각자 주사위를 던져 나온 숫자(SKD_C, SKD_P)를 금고 열쇠로 찍어 그날의 출입증(SK)을 만든다. 출입증을 잃어버려도 그날 하루뿐이고, 한 사람이 주사위를 조작해도 다른 사람의 숫자가 섞이니 혼자서는 출입증을 정할 수 없다.
절차는 센트럴이 시작한다. 센트럴의 호스트가 HCI_LE_Enable_Encryption으로 LTK와 함께 그 LTK를 찾을 단서(EDIV, Rand)를 건네면, 링크 계층이 자기 몫의 무작위 값 SKD_C(8바이트)와 IV_C(4바이트)를 만들어 LL_ENC_REQ로 보낸다. 페리페럴은 자기 몫 SKD_P·IV_P를 LL_ENC_RSP로 돌려주고, 호스트에 HCI_LE_Long_Term_Key_Request 이벤트로 EDIV와 Rand를 넘겨 맞는 LTK를 달라고 한다. LTK가 있으면 양쪽 모두
를 계산한다. e는 AES-128 한 블록 암호화다(1권 8장). 그다음 세 번 악수한다. 페리페럴이 암호화하지 않은 LL_START_ENC_REQ를 보내고, 센트럴이 암호화한 LL_START_ENC_RSP로 답하고, 페리페럴도 암호화한 LL_START_ENC_RSP로 답하면 연결이 암호화된다. 마지막 두 PDU는 내용이 아니라 “같은 키로 잠그고 풀 수 있다”를 확인하는 용도다. 키가 틀렸다면 MIC가 맞지 않아 여기서 끊긴다.
페리페럴의 호스트가 LTK를 주지 못하면(EDIV·Rand에 맞는 키를 잃었거나 본딩을 지웠을 때) 페리페럴은 LL_REJECT_EXT_IND에 오류 코드 PIN or Key Missing (0x06)을 실어 거절한다. 휴대폰에서 “기기를 다시 페어링하세요”가 뜨는 흔한 원인이다. 절차 도중 예상하지 않은 데이터 PDU가 오면 양쪽 모두 즉시 연결을 끊고 0x3D(MIC Failure)로 알린다.
공격자가 LL_ENC_REQ와 LL_ENC_RSP를 모두 엿들었다. SKD_C, SKD_P, IV_C, IV_P를 전부 안다. 세션 키를 계산할 수 있을까?
SK = e(LTK, SKD). 공중에 나온 값과 나오지 않은 값을 나눠 보자.
암호화 시작 절차·SKD와 IV 조합·SK = e(LTK, SKD)·0x06과 0x3D: [Vol 6] Part B §5.1.3.1 · e 함수: [Vol 3] Part H §2.2.1 · 샘플(HCI와 PDU 바이트 전부): [Vol 6] Part C §1
13바이트 nonce: 같은 키, 다른 패킷
세션 키 하나로 연결이 끝날 때까지 수만 개의 패킷을 잠근다. 블록 암호를 그대로 쓰면 같은 평문이 같은 암호문이 되어 패턴이 드러나므로, 패킷마다 한 번만 쓰는 값 nonce를 섞는다. LE의 nonce는 13바이트로, 세 부분이다.
| 옥텟 | 내용 | 설명 |
|---|---|---|
| 0~3 | packetCounter [31:0] | 방향마다 따로 세는 39비트 카운터의 아래 32비트(작은 바이트 먼저) |
| 4 | 비트 0~6: packetCounter [38:32] · 비트 7: directionBit | 센트럴이 보내면 1, 페리페럴이 보내면 0 |
| 5~12 | IV [63:0] | IV_P ‖ IV_C를 작은 바이트부터 |
packetCounter는 암호화 시작 절차에서 처음 암호화한 PDU(LL_START_ENC_RSP)를 0으로 하고, 새로 암호화한 PDU마다 하나씩 는다. 재전송은 늘리지 않는다. 같은 패킷을 다시 보내는 것이니 nonce도 같아야 받는 쪽이 같은 평문을 얻는다. 길이 0인 빈 PDU는 암호화하지 않으므로 카운터도 쓰지 않는다. 방향 비트가 있어서 센트럴과 페리페럴이 각자 0부터 세어도 nonce가 겹치지 않는다.
CCM nonce 형식(Table 2.1)·packetCounter와 directionBit 규칙: [Vol 6] Part E §2.1 · CCM 파라미터 M = 4, L = 2: Part E §1
AES-CCM: MIC를 만들고, 키스트림을 덧씌운다
LE는 AES-CCM(Counter with CBC-MAC)을 쓴다. 한 번의 계산으로 두 가지를 한다. ① CBC-MAC으로 헤더 일부와 페이로드를 요약해 4바이트 MIC(Message Integrity Check)를 만들고, ② 카운터 모드로 키스트림을 만들어 페이로드와 MIC에 XOR한다.
- B₀ = 0x49 ‖ nonce ‖ 페이로드 길이(2바이트). 0x49는 “MIC 4바이트, 길이 칸 2바이트, 추가 인증 데이터 있음”을 뜻하는 CCM 플래그다.
- B₁ = 00 01 ‖ 헤더 첫 바이트(NESN·SN·MD를 0으로) ‖ 0 × 13. 추가 인증 데이터(AAD)는 이 1바이트뿐이다.
- B₂, B₃, … = 페이로드를 16바이트씩(마지막은 0으로 채움).
- X₁ = e(SK, B₀), Xᵢ₊₁ = e(SK, Xᵢ ⊕ Bᵢ). 마지막 X의 앞 4바이트가 MIC다.
- Aᵢ = 0x01 ‖ nonce ‖ i(2바이트), Sᵢ = e(SK, Aᵢ). S₀의 앞 4바이트로 MIC를, S₁, S₂, …로 페이로드를 XOR한다.
암호화는 페이로드에만 걸리고 헤더(2바이트)는 평문으로 나간다. 받는 쪽이 길이와 LLID를 먼저 읽어야 하기 때문이다. 대신 헤더 첫 바이트 중 LLID와 CP 비트는 MIC로 보호한다.
패킷 암호화 작업대: 사양 샘플 네 개를 바이트까지
CCM 블록 B₀·B₁·Aᵢ(Table 2.2~2.4)과 헤더 마스크: [Vol 6] Part E §2.2·§2.3 · 블록별 중간값(B, X, A, S)과 결과 패킷: [Vol 6] Part C §1.2 · CCM 자체: IETF RFC 3610
위·변조와 재전송: MIC가 막는 것과 막지 않는 것
CCM은 스트림 암호처럼 동작한다. 암호문 비트 하나를 뒤집으면 복호한 평문의 같은 비트가 뒤집힌다. 공격자가 내용을 몰라도 “이 자리의 값을 바꾸기”는 쉽다는 뜻이다. 그래서 MIC가 필수다. 바뀐 평문으로 다시 계산한 MIC가 받은 MIC와 맞지 않으면 링크 계층은 그 패킷을 버리는 데서 그치지 않고 연결을 끊는다(0x3D). 4바이트 MIC를 맞힐 확률은 한 번에 2⁻³²이고, 한 번 틀리면 연결이 사라지니 시도를 거듭할 수도 없다.
MIC가 보지 않는 비트도 있다. 헤더의 NESN·SN·MD는 마스크되어 MIC에 들어가지 않는다. 이 비트들은 재전송과 흐름 제어(1권 6장)에 쓰여서, 같은 패킷을 다시 보낼 때나 확인 응답이 바뀔 때 값이 달라져도 다시 암호화하지 않도록 뺀 것이다. 길이 바이트도 B₀에 길이로 들어가므로 바꾸면 MIC가 맞지 않는다.
암호문을 건드려 보기
구현 버그로 페리페럴이 패킷 두 개를 같은 packetCounter로 암호화해 보냈다. 엿듣는 사람은 무엇을 알게 될까?
같은 nonce면 S₁, S₂, …(키스트림)도 같다.
MIC 계산 대상·암호화 대상·MIC 실패 시 연결 상실(0x3D): [Vol 6] Part E §1 · 재전송 때 카운터를 늘리지 않음: Part E §2.1 · NESN·SN·MD 마스크(Table 2.3): Part E §2.2
키 바꾸기, 그리고 등시성 링크의 IV
연결을 끊지 않고 새 키를 쓰려면 암호화 일시 정지 절차를 쓴다. 센트럴이 LL_PAUSE_ENC_REQ를 보내고, 페리페럴이 암호화된 LL_PAUSE_ENC_RSP로 답하고, 센트럴이 평문 LL_PAUSE_ENC_RSP를 돌려주면 암호화가 꺼진다. 그 사이에는 데이터 PDU를 보내지 않는다. 바로 이어서 위의 시작 절차를 다시 해 새 SKD와 새 IV로 새 세션 키를 만든다. 일시 정지 뒤에 페리페럴의 호스트가 LTK를 주지 못하면 거절이 아니라 연결 종료(0x06)다.
등시성 채널(14장)에도 같은 CCM을 쓴다. CIS는 연결된 ACL의 세션 키를 그대로 쓰고, IV는 ACL의 IV에서 아래 32비트만 그 CIS의 접근 주소와 XOR해 만든다. CIS마다 접근 주소가 다르므로 같은 키로도 nonce가 겹치지 않는다. packetCounter 자리에는 CIS의 페이로드 카운터를 쓴다. BIS는 연결이 없으니 방송 코드(Broadcast_Code)에서 유도한 그룹 세션 키(GSK)를 쓰고, IV 기준값은 BIGInfo에 실린 GIV다(9장).
암호화 일시 정지: [Vol 6] Part B §5.1.3.2 · CIS·BIS의 IV(그림 2.1)와 packetCounter: [Vol 6] Part E §2.1 · CIS·BIS의 세션 키: Part E §2.4 · GSK 유도(h8): Part E §1.1
핵심 정리
- LTK는 공중에 나가지 않는다. 연결마다 SK = e(LTK, SKD_P ‖ SKD_C)로 새 세션 키를 만들고, LL_START_ENC_REQ/RSP 세 번의 교환으로 양쪽이 같은 키인지 확인한다.
- CCM nonce 13바이트 = packetCounter(39비트) + directionBit + IV(IV_P ‖ IV_C). 카운터는 새 PDU마다 늘고 재전송에는 그대로다.
- AES-CCM(M = 4, L = 2): B₀·B₁(헤더 첫 바이트, NESN·SN·MD 마스크)·페이로드 블록으로 MIC, A₀으로 MIC 암호화, A₁…로 페이로드 암호화.
- MIC가 틀리면 패킷이 아니라 연결이 끊긴다(0x3D). 같은 nonce를 두 번 쓰면 평문의 XOR이 드러난다.
- 키 교체는 일시 정지 + 재시작. CIS는 ACL의 키, BIS는 GSK를 쓰고, 둘 다 IV 아래 32비트를 접근 주소와 XOR한다.
확인 퀴즈
SKD = SKD_P ‖ SKD_C에서 SKD의 가장 작은 바이트는?
LL_START_ENC_REQ는 어떻게 보내는가?
MIC 계산에서 0으로 가리는 헤더 비트는?
같은 패킷을 재전송할 때 packetCounter는?