diff options
| author | eunhhu <[email protected]> | 2026-09-27 19:17:02 +0100 |
|---|---|---|
| committer | eunhhu <[email protected]> | 2026-09-27 19:17:02 +0100 |
| commit | 6d276be51ba89b04a60bb16b5179c015ad202c7c (patch) | |
| tree | 1d30ae7bbb2d1db9e9a1b627d0fb76dc26eaf074 /docs | |
| parent | 313c4fa1720a0019c87083d3523b6643606c5aa6 (diff) | |
| download | pure-6d276be51ba89b04a60bb16b5179c015ad202c7c.tar.gz pure-6d276be51ba89b04a60bb16b5179c015ad202c7c.zip | |
Seal in one session a wire, authenticated by the sender's key (088)
A sealed request cost three X25519 operations (about 0.5 ms), so sealed
sends drained at half the plain rate. A wire now keeps one HPKE context,
encapsulated at its first seal, and seals each request as the context's
next number (a failed seal or a restart starts a new session); the
header carries the session's encapsulated key and the number, so a
receiver opens a session once per connection thread (16 remembered, the
least used replaced) and each seal by its number, also after it restarts,
unasked. A sender with a key of its own seals in HPKE auth mode and names
that key in the header; a receiver started with --sender NAME=KEY takes
deliveries only from the senders it names, each sealed with its key
(403 sender otherwise). The body is now the ciphertext and its tag.
Sealed sends, sender authenticated, drain at 461-464k a second against
462-470k plain and 207-234k sealed a request at a time (Raspberry Pi 5).
The check adds: a wire's requests share one session numbered from 0; a
changed number or sender key does not open; a receiver restarted naming
its sender opens the same session again; another node claiming that
sender's name with its own key is refused and applies nothing.
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/research/088-sealed-sessions.md | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/docs/research/088-sealed-sessions.md b/docs/research/088-sealed-sessions.md new file mode 100644 index 0000000..4f9bda2 --- /dev/null +++ b/docs/research/088-sealed-sessions.md @@ -0,0 +1,83 @@ +# 088 — 봉인한 세션과 보낸 이: 키 합의는 세션에 한 번, 출처는 키에 묶어 + +2026-09-27. [086](086-sealed-sends.md)의 "다음" 중 둘. 봉인한 전달이 봉인하지 않은 전달의 절반 속도였던 것(요청마다 X25519 +세 번)과, base 모드라서 `X-Pure-Source`가 인증되지 않던 것. + +착수 기준: + +- 바꿀 결정: 요청마다 캡슐화할지, 전달선마다 세션을 하나 두고 그 안에서 번호로 봉인할지(RFC 9180의 여러 번 봉인). 보낸 이는 + HPKE auth 모드로 인증할지(보내는 노드의 키가 봉인에 들어간다), 서명을 따로 붙일지. +- 최소 확인: 받는 쪽이 다시 시작해도 따로 묻지 않고 세션을 여는지, 번호나 보낸 이 키를 바꾸면 열리지 않는지, 이름 붙은 보낸 이의 + 이름을 다른 키로 쓰는 노드가 거절되는지 검사로 지킨다. 전달 속도를 요청마다 캡슐화하던 빌드와 번갈아 잰다. +- 중단 기준: 봉인하지 않은 전달이 느려지거나, 한 번 봉인한 것이 다른 번호로 열리면 되돌린다. + +## 한 것 + +**세션.** 보내는 쪽은 전달선마다 HPKE 문맥을 하나 두고, 첫 봉인 때 한 번 캡슐화한다. 그 뒤 요청은 문맥의 번호로 봉인한다(넌스는 +기본 넌스와 번호의 XOR). 번호는 OpenSSL의 보내는 쪽 문맥이 스스로 올리므로 같은 넌스가 두 번 쓰이지 않는다. 봉인이 실패하면 +다음 시도는 새 세션을 만든다. 프로세스가 다시 시작하면 새 세션이다. 헤더가 세션과 번호를 싣는다. + +``` +X-Pure-Seal: x25519-sha256-aes128gcm <캡슐화한 키 64 hex> <번호> [<보낸 이의 공개키 64 hex>] +``` + +본문은 이제 암호문과 태그 16 B뿐이다(캡슐화한 키는 헤더로 갔다). 받는 쪽의 연결 스레드는 연 세션을 16개까지 기억하고, 가장 덜 +쓴 것부터 바꾼다. 세션마다 캡슐화를 한 번 풀고(X25519 한두 번), 요청마다 번호를 정해(`OSSL_HPKE_CTX_set_seq`, 받는 쪽만 할 +수 있다) 연다. 캡슐화한 키가 요청마다 오므로 받는 쪽이 다시 시작하거나 요청이 다른 스레드로 가도 묻지 않고 연다. 봉인은 여전히 +요청의 출처·첫 번호·개수에 묶인다(AAD). + +**보낸 이.** 보내는 노드에 `--key`가 있으면 auth 모드로 봉인한다. 그 노드의 키가 키 합의에 들어가므로, 헤더에 적은 보낸 이의 +공개키에 맞는 개인키를 가진 쪽만 받는 쪽이 여는 봉인을 만들 수 있다. 받는 노드는 `--sender NAME=KEY`로 보낸 이를 이름과 키로 +적을 수 있다. 적으면 그 노드는 적힌 보낸 이의 전달만 받는다. 출처가 적힌 이름이고, auth 모드이며, 헤더의 키가 적힌 키와 같아야 +한다. 아니면 403 `sender`다. 헤더의 키가 거짓이면 봉인이 열리지 않는다(400 `seal`). + +| 요청이 | 답 | +|---|---| +| 봉인이 열린다 | 200, 번호마다 한 번 적용 | +| 한 바이트, 번호, 보낸 이의 키, 출처·번호·개수 중 무엇이든 바뀌었다 | 400 `seal`, 적용 없음 | +| 봉인이 없다(`--sealed-only`나 `--sender`인 노드) | 403 `unsealed` | +| 적힌 보낸 이가 아니거나 그 키가 아니다(`--sender`인 노드) | 403 `sender` | + +**검사(`server_sealed`).** 086의 것에 더해 이것들을 본다. + +- 보내는 쪽이 제 키로 봉인한다. 한 전달선의 요청(53–57개)이 모두 한 세션이고, 번호가 0부터 하나씩 오른다. +- 번호를 바꾼 요청과 보낸 이의 키를 바꾼 요청은 400 `seal`이다. +- 키 없이(base 모드) 다른 노드의 키로 봉인한 요청은 400 `seal`이다. +- 받는 노드를 `--sender 출처=보낸이키`로 다시 띄우면, 보내는 쪽의 다음 주문이 같은 세션으로 가서 열리고 적용된다. +- 같은 모듈이라 같은 출처 이름을 쓰는 다른 노드가 제 키로 봉인하면 403 `sender`이고 아무것도 적용되지 않는다. + +여섯 번 연달아, 그리고 `make check` 전체에서 통과한다. + +## 장치가 말한 것 + +086과 같은 방법이다. 받는 서버를 멈춘 채 보내기 20,000개를 쌓고 푼다. 세 라운드이고 변형의 순서는 돌린다. 봉인은 보내는 쪽도 +제 키로 한다(auth 모드). 기준은 요청마다 캡슐화하던 빌드(`bdb5f10`)의 봉인이다 +([원시 결과](../../research/experiments/014-sealed-sends/2026-09-27-sessions-linux-aarch64.jsonl)). + +| | 보내기/s | +|---|---:| +| 봉인 없음 | 462.2–470.2k | +| 봉인, 세션과 보낸 이 인증(지금) | **461.2–463.7k** | +| 봉인, 요청마다 캡슐화(전) | 207.3–233.7k | + +봉인의 비용이 절반에서 1% 안쪽이 됐다. 요청마다 들던 X25519 세 번(약 0.5 ms)이 세션에 한 번(auth 모드는 보내는 쪽 셋, 받는 +쪽 둘)이 되고, 요청마다 남은 것은 AES-GCM이다(약 10 KiB 묶음에 양쪽 합쳐 약 9 µs). + +## 성질과 한계 + +- **넌스.** 보내는 쪽 문맥의 번호는 OpenSSL이 올리고 되돌릴 수 없다. 세션은 프로세스 안에서만 살고, 실패하면 버린다. 그래서 + 한 키와 넌스로 두 평문을 봉인하는 일이 없다. +- **재전송.** 기록한 요청을 다시 보내면 열린다(같은 세션, 같은 번호). 그러나 출처와 번호로 한 번만 적용된다(083의 성질). + 다른 번호나 다른 출처로 옮기면 열리지 않는다. +- **보낸 이 인증의 한계.** auth 모드는 키 누설 사칭(KCI)에 약하다(RFC 9180 9.1.1). 받는 노드의 개인키를 훔친 쪽은 그 노드에게 + 아무 보낸 이로나 봉인할 수 있다. 받는 노드의 키는 그 노드가 받는 모든 것을 여는 키이기도 하므로, 그것을 지키는 것이 먼저다. +- **전방 비밀성.** 아직 없다. 받는 노드의 키를 훔치면 기록해 둔 세션이 열린다. 받는 키를 돌려 쓰는 것(서명한 사전 키)이나 + 메신저의 래칫이 다음이다. +- **비용 공격.** 모르는 캡슐화 키를 단 요청은 열 때마다 캡슐화를 한 번 푼다(X25519, 약 0.16 ms). 열리지 않는 세션은 기억하지 + 않는다. 출처별 속도 제한은 남은 일이다. +- **메타데이터.** 출처, 번호, 개수, 경로, 크기, 그리고 이제 보낸 이의 공개키가 보인다. 중계는 누가 누구에게 보내는지 안다. + +## 다음 + +- 받는 키 돌리기(서명한 사전 키)로 전방 비밀성을 세션 단위로. +- 메신저: 사용자마다 한 노드, 연락처는 `--sender` 목록, 화면은 Vega. |
