1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
|
# 098 — 링크의 보안 검사: 경로 위의 공격자가 할 수 있는 것
2026-09-29. [설계 011](../design/011-pure-transport.md)의 4단계 가운데 보안 검사다. 011의 중단 기준은 이렇다. "보안 검사(증폭,
보낸 이 위장, 재전송, 변조)에 실패하면 인터넷용으로 켜지 않는다."
착수 기준:
- **바꿀 결정.** 링크를 인터넷 쪽에 켤 수 있는가.
- **최소 확인.** 경로 위의 공격자가 할 수 있는 일을 하나씩 검사로 만든다.
- 패킷을 바꾼다.
- 지난 패킷을 다시 보낸다. 보낸 이의 주소로도, 자기 주소로도 보낸다.
- 허용되지 않은 키나 출처로 보낸다.
- 알려진 키 id로 지어낸 패킷을 쏟는다.
- 모든 검사에서 한 키가 같은 논스로 두 번 봉인하지 않는지 본다.
- **중단 기준.** 위 가운데 하나라도 실패하면 고칠 때까지 인터넷용으로 켜지 않는다.
## 쉬운 말로
- 알아낸 것:
- 코드를 공격자의 눈으로 읽고 검사를 만들었더니 구멍이 여섯 개 나왔다.
- 가장 무거운 것은 논스 재사용이었다. 쉬던 보내는 이가 돌아오면, 받는 쪽이 같은 키로 답의 번호를 0부터 다시 썼다.
AES-GCM에서는 평문이 새고 위조까지 가능해지는 결함이다.
- 다음은 지난 패킷 하나로 링크를 영원히 멈출 수 있던 것이다.
- 모두 고쳤다. 이제 다섯 가지 공격 검사를 모두 통과한다.
- 검사를 만들다 공격과 무관한 버그도 하나 찾았다. RTT 표본이 0이면 페이싱이 NaN이 되어 보내는 쪽이 영원히 멈췄다.
- 아직 모르는 것: 전방 비밀성(에폭 바꿈)은 아직 없다. 지금은 두 노드의 정적 키가 새면 녹화한 지난 기록을 풀 수 있다.
- 사용 경험의 차이: 경로 위의 누가 패킷을 바꾸거나 되풀이하거나 쏟아부어도, 기록은 한 번씩 그대로 적용되고 링크는 이어진다.
## 한 것
링크의 두 쪽을 가상 시간의 네트워크에 놓는 단위 검사([test-link.c](../../link/test-link.c))에 공격자를 더했다. 모두 씨앗에서
결정적으로 돌고, `make check`에 들어 있다.
| 검사 | 공격자가 하는 것 | 통과 조건 |
|---|---|---|
| 논스 | (모든 검사에서) | 보낸 모든 패킷의 (키를 정하는 것, run, 번호)가 한 번씩만 나온다. 답은 받을 보내는 이의 시작마다 따로 센다 |
| 변조 | 오가는 모든 패킷을 바이트 하나 뒤집고, 자르고, 늘린 사본을 먼저 넣는다 | 어느 쪽에서도 열리지 않고, 답을 끌어내지 않는다 |
| 재전송 | 걸음의 25 %마다 지난 패킷 하나를 다시 넣는다. 받는 쪽에는 보낸 이의 주소나 공격자의 주소로 넣는다. 손실, 재정렬, 양쪽 재시작, 쉼이 함께 있다 | 모든 기록이 한 번씩 그대로 적용된다. 공격자에게 간 바이트는 받은 것의 3배 이하다 |
| 위장 | 허용되지 않은 키로 보낸다. 그 패킷에 허용된 키의 id를 단다. 허용된 키로 허용되지 않은 출처 이름을 쓴다 | 앞의 둘은 열리지 않는다. 셋 다 아무것도 전달되지 않는다 |
| 폭주 | 허용된 키의 id로, 처음 보는 run의 지어낸 패킷 1만 개를 초당 10만 개로 넣는다 | 만든 키가 300개 이하, X25519는 1번이다. 공격자에게는 0 바이트가 간다. 뒤이은 진짜 보내는 이의 기록 200건이 모두 전달된다 |
## 찾아 고친 것
1. **논스 재사용.**
- 받는 쪽은 모든 답을 자기 run 하나로 봉인했다. 답의 키는 (받는 쪽 run, 보내는 쪽 run)에서 나온다.
- 쉬는 보내는 이의 상태를 비운 뒤 그 보내는 이가 같은 run으로 돌아오면, 새로 만든 칸이 답의 번호를 0부터 다시 썼다. 같은
키, 같은 논스다.
- 이제 칸을 만들 때마다 답용 run을 새로 뽑는다. 보내는 쪽은 답의 run이 바뀌면 키를 다시 만든다(원래 그렇게 했다).
2. **지난 패킷 하나로 링크가 멈춤.**
- 들은 번호를 아직 모르는 흐름(재시작이나 쉼 뒤)은 쥔 기록 가운데 가장 앞의 것부터 넘겨, 호스트의 답으로 들은 번호를 배운다.
- 공격자가 다시 보낸 옛 패킷에 이미 저장된 기록의 일부가 들어 있으면, 그것이 맨 앞에서 영원히 온전해지지 않았다.
- 이제 첫 온전한 기록부터 넘긴다. 호스트의 답으로 옛 조각은 버려진다.
3. **답을 빼앗음.**
- 열리기만 하면 아무리 옛 패킷이라도 답이 갈 주소를 옮겼다.
- 이제 그 run에서 가장 큰 번호의 패킷만 주소를 옮긴다.
- 새 주소에는 CHALLENGE를 보내고, 보내는 쪽이 RESPONSE로 되돌려 주면 확인된 것으로 친다. 공격자는 봉인된 답을 읽지 못하니
되돌려 줄 수 없다.
- 확인 전에는 그 주소로 받은 바이트의 3배까지만 답한다(RFC 9000 §8). 경로 바뀜(NAT 재바인딩, 망 전환)도 이 규칙으로
따라간다.
4. **열리지 않은 패킷이 답을 끌어냄.** 이미 받은 번호면 열기 전에 확인을 다시 보냈다. 번호는 평문 머리에 있으니 위조할 수
있다. 이제 열고 나서 본다.
5. **옛 답이 보내는 쪽을 묶음.**
- 옛 답의 크레딧 1이 보내는 쪽을 멈출 수 있었다. 옛 답의 틈 표시는 받은 것을 모두 다시 보내게 했다.
- 이제 보내는 쪽은 들은 번호와 크레딧으로 들은 가장 큰 한도까지 보낸다. 설계 011의 "최댓값으로만 받아들인다"이다.
- 틈 표시는 저장된 번호가 뒤로 가지 않은 답에서만 받는다.
- 옛 답이 가져온 옛 챌린지에 밀리지 않게, 최근 챌린지 네 개를 모두 되돌려 준다.
6. **폭주가 키를 만들게 함.**
- 처음 보는 run의 패킷마다 HKDF를 두 번 했다.
- 이제 키를 만들었는데 열리지 않으면 토큰 하나를 쓴다. 토큰은 초당 256개로 채운다.
- 정상적인 대량 깨움(M2의 기기 1만 대)은 모두 열리니 토큰을 쓰지 않는다. 폭주 중에는 이미 아는 run의 패킷만 제 속도로 받고,
새 run은 초당 256개까지만 받는다.
- X25519는 보내는 이마다 한 번이다. 비밀을 메모에 적어 두기 때문이다(095).
변조 검사는 처음부터 통과했다. 머리를 연관 데이터로 넣은 AES-GCM 덕분이다. 위장 검사도 통과했다. (101에서 LOST를 더한 뒤로는, 받는
쪽이 모르는 run의 에폭 1 이상 패킷이 받은 것보다 작은 LOST 하나를 끌어낼 수 있다. QUIC의 stateless reset과 같은 자리이고, 초당
4,096개로 묶인다.)
## 결과
- `make check`가 통과한다. UDP 링크 검사(재시작, 손실, 쉼, 메모)도 경로 확인이 들어간 채 통과한다.
- 전송이 기록 하나에 쓰는 CPU는 61 B와 1 KiB에서 같다. 16 KiB는 5 % 높은데, 앞선 라운드들의 흔들림(19.7–22.5 µs) 안이다.
- 검사 결과(가상 시간):
| 경우 | 기록 | 가상 시간 | 결과 |
|---|---:|---:|---|
| 변조 | 1,500 | 3.9초 | 바뀐 패킷 11,583개, 열린 것도 답을 끌어낸 것도 0 |
| 재전송(두 씨앗) | 3,000 | 15.8·17.3초 | 다시 넣은 패킷 2,354·2,403개. 모든 기록이 한 번씩. 공격자가 보낸 715·892 KB에 공격자에게 간 답은 0 바이트 |
| 위장 | – | – | 전달 0 |
| 폭주 | 1만 패킷 | 0.1초 | 키 282개, X25519 1번, 뒤이은 200건 모두 전달 |
## 판정
- 011의 보안 중단 기준(증폭, 보낸 이 위장, 재전송, 변조)은 검사한 범위에서 모두 통과한다.
- 남은 약점은 설계가 알고 있던 둘이다.
- **전방 비밀성이 없다.** 키는 두 정적 키와 run에서만 나온다. 정적 키가 새면 녹화한 트래픽을 풀 수 있다. 011은 이것을
주기적인 에폭 바꿈(임시 키 교환)으로 얻겠다고 했고, 아직 구현하지 않았다. 그때까지 인터넷용으로 켜기 전에 풀어야 할 첫
과제다. ([099](099-link-epochs.md)에서 넣었다.)
- **KCI.** 받는 쪽의 개인 키를 가진 자는 어떤 보내는 이로도 행세할 수 있다. 정적-정적 X25519의 성질이다. Noise KK의 첫
메시지도 같다. (처음에는 에폭 바꿈이 이 창을 첫 에폭으로 좁힌다고 적었다. 틀렸다. 행세하는 자는 자기 run을 에폭 0부터
새로 시작하면 된다. 에폭이 막는 것은 이미 오가는 남의 run에 끼어드는 것이다. [099](099-link-epochs.md) 참고.)
## 한계와 다음
- 검사는 가상 네트워크의 결정적 흉내다. 실제 공격 도구로 쏟아부은 것은 아니다.
- 폭주 중에는 새 run이 초당 256개로 묶인다. 정상 기기의 재연결이 그 사이에 늦을 수 있다. QUIC의 Retry처럼 주소를 증명하는
토큰은 아직 없다.
- 보내는 쪽은 옛 답을 받을 때마다 그 답의 run 키를 다시 만든다(HKDF). 옛 답을 되풀이하는 공격자는 보내는 쪽의 CPU를 조금
쓰게 할 수 있다.
- 다음은 011 4단계의 나머지다. 에폭 바꿈(전방 비밀성), 경로 바뀜의 실제 측정(주소가 바뀌는 동안 끊긴 시간), 저장소 옮기기(M3).
|