리눅스 엔트로피 풀, WASM 플러그인, ChaCha20, BLAKE3를 활용해 직접 엔트로피를 수집하고 네트워크로 배포하는 실험 프로젝트 이야기.
요즘 무작위성에 대해 많이 생각하고 있습니다. 왜인지는 잘 모르겠어요. 어쩌면 제 뇌 자체에 대한 메타 분석일지도 모르죠. 아무튼요.
먼저 말해둘 만한 건, 저는 암호학자가 아니고 수학 실력도 특별히 뛰어난 편은 아니라는 점입니다. 이런 주제들은 보통 다른 사람들에게 맡기는 편입니다.
아무튼! 엔트로피! 우리 주변 어디에나 있죠! 유명하게도 Silicon Graphics와 Cloudflare는 용암 램프를 향해 카메라를 설치해서 엔트로피를 수집하고, 그걸 무작위성 알고리즘에 넣었습니다. 예측 불가능한 바이트 스트림을 만들어낼 수 있다는 건 작업 부하에 따라 꽤 가치 있는 일입니다. 하지만 작업 부하가 그것을 요구하지 않을 때도 이런 건 꽤 재미있습니다. 겉보기에 쓸모없어 보이는 구현 세부 사항을 끼워 넣을 수 있을 때는 특히 더 재미있죠!
어느 날 앉아서 리눅스 엔트로피에 완전히 꽂혀 버렸습니다. /dev/{u}random에 데이터를 쓰기만 해도 리눅스 시스템의 엔트로피 풀에 엔트로피를 공급할 수 있다는 걸 알고 계셨나요? 더 나아가, 시스템이 충분한 엔트로피를 확보했다고 만족하기 전의 동작을 제외하면 urandom과 random 사이에는 사실상 큰 차이가 거의 없다는 것도요? 또, 부팅 시에는 엔트로피 풀 자체의 데이터를 이용해 아주 자주 다시 시드되고, 부팅이 끝난 뒤에는 1분마다 다시 시드된다는 것도요? 커널의 random.c 구현 현대화와 몇 가지 미래 계획의 실마리를 다루는 Wireguard 제작자 Jason Donenfeld의 이 발표를 정말 추천합니다. 그 발표를 보면 이 글 뒤에서 이야기할 몇 가지 선택에 대한 맥락도 조금 더 얻을 수 있습니다.
엔트로피 풀은 여러 소스로부터 공급받지만, 커널 수준에서 받아들일 수 있는 양에는 한계가 있습니다. 사용자 공간 수준에서 그 풀에 엔트로피를 “먹일” 수 있다는 능력은 엄청나게 많은 가능성을 열어 줍니다. 정말 꼭 필요하냐고요? 아니요, 사실 그렇진 않습니다. 하지만 저는 이런 걸 보면 웃음이 나고, 보통 그 정도면 충분합니다. 어쨌든 해가 되지는 않으니까요. 그래서 앉아서 이런 개념을 뚝딱거리기 시작했습니다. WASM 플러그인을 사용해서 제 시스템의 엔트로피 풀에 데이터를 공급할 수 있을까?
WASM을 고른 이유는 단순히 그것을 플러그인 시스템으로 구현해 보는 실험을 해보고 싶었기 때문입니다. 저는 이전에도 제 프로젝트들에서 플러그인 시스템을 실험해 본 적이 있습니다. 여러 Minecraft Bukkit 플러그인을 작성해 보기도 했고, Platypus server monitoring 프로젝트에서는 완전한 Go interpreter를 붙여 보기도 했습니다. 이론도 이해하고 실무 구현 방식도 어느 정도는 알고 있지만, 보통 빠져 있는 건 사람들이 가장 편한 언어를 쓸 수 있게 해 주는 탄탄한 런타임입니다. WASM은 많은 언어가 대상으로 삼을 수 있는 견고한 기반을 제공합니다. 그래서 이건 WASM을 좀 더 가지고 놀아보려는 “그냥” 핑계이기도 했지만, 제 머릿속은 여전히 엔트로피 풀을 공급하는 일에 집착하고 있었습니다. 그렇게 morerandom이 탄생했습니다.
조금만 찾아봐도 다른 프로그램을 위한 플러그인 시스템으로 특별히 설계된 Extism을 발견할 수 있었습니다. “호스트” 쪽 구현은 아주 간단합니다. 다만 사소하게 아쉬운 점이 있다면 플러그인 쪽에서는 그들의 라이브러리를 써야 한다는 점입니다. 예를 들어, 저는 그냥 1을 그대로 돌려주는 1.wat 플러그인을 가지고 있는데, 필요한 것보다 조금 더 복잡합니다. 초기 구현, 그리고 현재 구현은 단순히 WASM 바이너리의 random() -> FnResult<String> 함수를 호출하고 그 결과에 포함된 원시 바이트를 그대로 반환하는 방식이었습니다. 개념 증명으로는 충분했지만, 그다음부터 머리가 돌아가기 시작했죠. 마이크를 엔트로피에 쓸 수 있겠다! 카메라도! 하지만... 그건 제 모든 머신에서 접근 가능한 게 아니고... WASM 플러그인에 그런 장치를 노출하는 건 까다롭고 위험할 수도 있겠더라고요...
그래서 다음 단계는 바이너리 자체에 마이크와 카메라 지원을 추가하는 것이었습니다. 다행히 Rust용 라이브러리가 이미 있었고, 저는 그 원시 바이트를 그냥 뱉어내면 됐습니다. 쉽죠! 사실상 Rust로 lavarand를 다시 만든 셈이었습니다. 멋지네요! 그런데... 잠깐, 제 홈랩의 다른 머신들도 그 엔트로피를 자기들 풀에 넣고 싶어 할 수 있잖아요... 그건 어떻게 해야 하지? 그리고 더 나아가, 악성 클라이언트가 마이크와 카메라의 원시 바이트를 빼내서 잠재적으로 저를 엿듣는 일은 어떻게 막을 수 있을까?
그렇게 토끼굴은 더 깊어졌습니다. 네트워크를 통한 엔트로피 가져오기를 가능하게 하기 위해 gRPC를 추가했습니다. gRPC는 Google의 Remote Procedure Call이지만, 이제 더는 Google 프로젝트는 아닙니다. 처음 접하는 분들을 위해 말하자면, gRPC는 높은 수준에서 보면 어떤 코드베이스가 원격 머신 위의 함수를 호출할 수 있게 해 주는 방식입니다. 바이너리들이 함수 호출 결과를 인코딩하고 디코딩하는 데 사용하는 공통 스키마를 정의하고, 이는 대체로 HTTP 호출을 사용하는 것보다 훨씬 빠르고 작습니다. 서버에 놓인 작은 클라이언트 바이너리가 제 데스크톱의 morerandom 서버에 연결해서 서버가 노출하는 get_random() 함수를 호출합니다. 클라이언트는 바이트를 받아 표준 출력으로 내보냅니다. 그러면 다 된 거죠?
아직은 아닙니다. 원시 데이터를 빼낼 수 있다는 문제가 여전히 남아 있습니다. 이를 위해 리눅스 random.c 구현에서 한 페이지를 빌려 와 ChaCha20 알고리즘을 들여다보기 시작했습니다. 제가 그 알고리즘을 완전히 이해한다고 하진 않겠습니다. 하지만 현재의 제 이해로는, 여기에 시드를 넣으면 사실상 끝없이 이어지는 암호화된 데이터 스트림, 또는 우리 경우에는 무작위성 스트림을 외삽해 낼 수 있습니다. 시드로는 우리가 직접 수집한 엔트로피를 사용합니다! 단순하게 유지하기 위해 먼저 그 데이터를 현대적인 BLAKE3 해시 알고리즘으로 해싱합니다. 활성화된 플러그인, 카메라, 마이크에서 나온 바이트를 넣고, 그렇게 얻은 해시를 엔트로피 시드로 사용하는 거죠. 결과적으로는 ChaCha20 스트림이 되기 때문에, 어떤 의미에서는 /dev/{u}random를 다시 구현한 셈입니다. 커널 구현과의 유사성을 더하자면, 서버는 대략 1분마다 플러그인, 마이크, 카메라에서 데이터를 다시 가져오고, 기존 ChaCha20 스트림의 바이트 몇 개를 섞어 이 엔트로피를 다시 시드합니다. 이론적으로는 서버의 초기 플러그인, 카메라, 마이크 상태를 누군가 알아맞힌다 해도 시드를 계속 통제하려면 그 입력들을 계속 제어해야 한다는 뜻입니다. 그게 불가능한 건 아니고, 그래서 이것만을 유일한 엔트로피 소스로 추천하지는 않지만, 실제로는 매우 어렵습니다.
나중에는 무작위 숫자와 불리언 생성을 보여 주기 위한 HTTP 서버도 추가했습니다. 나중에 원하면 다른 프로젝트에서 쉽게 쓰기 위해서이기도 하고, 솔직히 말하면 조금 자랑하고 싶어서이기도 합니다.

코드베이스를 몇 번에 걸쳐 다듬고 정리하면서 더 견고하게 만들었지만, 핵심 엔트로피 수집 흐름은 거의 그대로입니다. 서버와 함께 독립 실행형 바이너리도 있는데, 이건 엔트로피를 한 번 수집하고, 해시하고, 시드한 뒤 32바이트의 데이터를 stdout으로 출력합니다.
원시 데이터를 볼 때는 xxd를 거쳐서 봅니다. 그렇지 않으면 출력이 아주 난잡하거든요! 원시 바이트는 재미있어요.
# 한 번 실행
00000000: 8d55 7c9f 893f f21a af3b 9436 7928 a8ed .U|..?...;.6y(..
00000010: 3a55 da08 313d ab65 96a5 9d00 c8ff b40d :U..1=.e........
# 또 한 번
00000000: 2071 208f ba71 4e0e 248f da65 b701 ac8b q ..qN.$..e....
00000010: 2c43 b572 72b4 34ca ad42 8b2e 24d2 76c1 ,C.rr.4..B..$.v.
이 모든 것을 갖추고, 제 홈랩 서버들에서는 하루에 한 번 morerandom 클라이언트 바이너리를 사용해 제 데스크톱에서 엔트로피를 가져와 각자의 엔트로피 풀에 쓰도록 systemd 타이머를 돌리고 있으니, 저는 이 프로젝트가 사실상 완료되었다고 생각합니다! 꽤 만족스럽습니다. 구현은 꽤 견고하다고 생각하지만, 분명 또 만지작거릴 다른 것들을 찾아내겠죠. WASM 기능과 사용 가능한 함수들을 더 확장하고 싶긴 하지만, 지금 당장 그럴 만한 좋은 이유는 딱히 없습니다. 이 프로젝트는 ChaCha20, BLAKE3, 그리고 리눅스의 엔트로피에 대해 조금 지나치게 집착하는 경험까지 전반적으로 정말 훌륭한 학습 기회였습니다.
피드백이나 질문 등이 있다면 fediverse에서 편하게 연락 주세요!