MPC 중계 서버는 무엇을 알고 무엇을 전달할까
개인키를 여러 사람에게 나누어 보관하더라도, 공동 서명을 시작하려면 서로 만나야 한다. 누가 접속했는지 확인하고, 이번 거래의 참여자를 정하고, 계산 중에 나온 메시지를 상대에게 전달해야 한다. 이 일을 서버가 맡는다면 새로운 질문이 생긴다. 서버는 키를 대신 사용하는 것일까, 아니면 키를 가진 사람들이 계산하도록 연결하는 것일까?
답은 서버라는 이름보다 그 서버가 받는 정보와 수행하는 연산에 있다. 메시지를 전달하는 권한, 암호문을 여는 권한, 지갑의 거래에 서명하는 권한은 서로 다르다. BXB의 사용자 간 통신과 별도 암호화 중계 모듈을 살펴보며 이 경계를 따라가 보자.
다음 10개 전송에는 새 대화가 필요하다
한 EVM 네트워크에서 주소 A가 토큰 T 90개를, 주소 B가 10개를 갖고 있다고 하자. A의 지갑은 P1·P2·P4 중 두 사람이 참여해야 서명할 수 있는 2-of-3 지갑이다. P4는 이전 담당자를 대신해 참여한 사람이며, 세 사람은 현재 사용할 키 조각을 각자 보관한다. 담당자 교체 자체는 토큰을 옮기지 않았고, 이번에 별도로 A에서 B로 T 10개를 더 보내려 한다.
T의 소수 자릿수는 0이며 발행·소각·토큰 전송 수수료·동시 거래는 없다고 가정한다. 거래 수수료에 필요한 기본 코인은 A가 따로 갖고 있다. 전송이 성공하면 A 80개, B 20개가 된다. 이 결과는 설명용 예제이며 실제 전송을 실행해 측정한 기록은 아니다.
이번에는 P1과 P4가 서명에 참여한다. 두 사람은 개인키 조각을 서버에 합쳐 놓지 않고, 각자의 조각으로 중간 계산을 수행하며 필요한 메시지를 주고받아 하나의 ECDSA 서명을 만든다. 개인키를 모으지 않는 공동 서명의 원리는 이 계산이 가능한 이유를 설명한다. 여기서는 그 계산에 들어갈 메시지를 어떻게 같은 작업에 연결하는지 살펴본다.
이후의 거래도 같은 P1과 P4가 맡더라도 이전 메시지를 이어 쓰면 안 된다. 새 거래에는 새로운 서명 대상과 세션이 필요하다. 세션은 이번에 선택한 사람들이 특정 작업을 함께 수행하는 한 번의 실행을 구별한다. 같은 지갑을 쓰는 다른 송금이나 지난 담당자 구성의 메시지가 섞이지 않도록 하는 출발점이다.
조정하는 일, 전달하는 일, 계산하는 일
공동 서명에는 세 역할이 있다. 같은 프로그램에 들어 있을 수 있지만 책임은 나누어 읽어야 한다.
| 역할 | 처리하는 것 | 그 자체로 끝나지 않는 것 |
|---|---|---|
| 세션 조정 | 참여자 선정·초대·시작·만료 | 지갑 서명 생성 |
| 메시지 전달 | 연결된 송신자와 수신자 사이의 운반 | 암호 계산의 정당성 판단 |
| 참여자의 암호 연산 | 자기 보관 자료와 수신 메시지로 계산 | 거래의 체인 실행 확인 |
BXB의 사용자 전용 서명 경로에서는 확장 프로그램이 PeerJS 연결로 다른 사용자의 메시지를 주고받는다. 연결 신호와 초대·진행을 다루는 서버가 있고, 실제 서명 연산은 참여자 쪽 WebAssembly 모듈에서 수행한다. 확장 프로그램은 모듈의 송신 메시지를 꺼내 전송하고, 받은 메시지를 다시 모듈에 넣는다. 암호 모듈이 다음 계산을 할 수 있는지 판단하는 일과, 네트워크 연결이 열렸는지 판단하는 일은 별개다.
소스에는 이 경로와 별도로, 사용자와 서버 참여자를 위한 cowork 중계 모듈도 있다. 여기서는 WebSocket으로 암호화한 봉투를 중계하며, 참여자 명세와 메시지 보호를 더 명시적으로 다룬다. 이 글의 세션 봉투와 HPKE 설명은 이 별도 모듈에서 확인한 클라이언트·중계·암호화 구성을 기준으로 한다. 사용자 PeerJS 경로에 같은 봉투가 적용된다고 보거나, 두 경로를 하나의 서버 포함 서명 완료 사례로 합치지는 않는다.
따라서 P1과 P4의 송금은 역할을 이해하기 위한 공통 예제다. 이후 봉투 설명에서도 같은 이름을 쓰지만, 특정 배포에서 모든 구성 요소의 상호운용과 체인 실행을 재현했다는 뜻은 아니다.
참가 명단에 키 버전까지 적는 이유
서명이 시작되기 전에 “P1과 P4가 참여한다”만 합의하면 충분할까? 같은 P4라도 지난달 기기와 새 기기에 등록한 전송 키가 다를 수 있다. 지갑 주소는 그대로여도 담당자 교체 전후의 키 조각은 다른 보관본일 수 있다. 참여자 이름만 맞고 나머지 조건이 다르면 메시지를 전달하고도 같은 계산을 진행하지 못한다.
이 조건들을 묶은 세션 명세를 manifest라고 부른다. BXB의 별도 중계 명세에는 세션 ID와 작업 종류, 참여 조건, 만료 시각, 참여자 목록을 담는다. 지갑·거래 요청 식별자, 서명할 메시지, 조각 버전도 해당 작업의 입력에 따라 들어간다. 참여자 항목에는 암호 연산에서 사용하는 참여자 ID와 사용자·서버 구분, 전송 공개키와 그 버전이 연결된다.
특히 shareVersion과 keyVersion은 다른 질문에 답한다.
전자는 어느 세대의 지갑 조각으로 공동 서명하는가, 후자는 어느 기기·전송 키로 메시지를 주고받는가를 구별한다.
P4가 통신 키를 교체했다고 지갑 조각도 자동으로 재분배되는 것은 아니다.
반대로 지갑 조각을 새로 받았다는 사실이 기존 기기의 통신 자격을 모두 정리해 주지는 않는다.
명세를 식별하기 위해 manifestHash도 사용한다.
서버는 객체 필드 순서를 일정하게 정리한 표현에 SHA-256을 적용한다.
필드 내용이 같은데 JSON 객체의 작성 순서만 달라졌다는 이유로 다른 해시가 생기는 일을 줄이려는 것이다.
봉투에는 긴 명세 대신 이 해시를 담아 어떤 명세에 속한 메시지인지 연결한다.
해시가 일치한다는 사실은 기준으로 삼은 내용이 같다는 뜻이다. 그 명세를 만든 사람이 올바른 거래를 골랐다거나, 명세에 적힌 공개키가 처음부터 신뢰할 수 있었다는 사실까지 증명하지는 않는다. 명세의 배포와 참여자·공개키의 등록을 신뢰할 수 있게 만드는 과정이 함께 필요하다.
지갑 키, 기기 인증키, 전송 키는 다르다
P4가 가진 모든 것을 “키”라고 부르면 역할이 섞인다. 지갑 서명에 참여하는 자료와 메시지를 열기 위한 자료는 목적이 다르다.
| 자료 | 사용하는 곳 | 확인하거나 수행하는 일 |
|---|---|---|
| 지갑의 ECDSA 비밀 조각 | 공동 서명 모듈 | 다른 참여자와 지갑 서명 계산 |
| 기기 신원용 Ed25519 키 | 기기 공개키 등록 | 등록 요청과 키 소유 관계 확인 |
| 전송용 X25519 키 | HPKE 수신 측 | 해당 수신자 앞으로 암호화한 메시지 열기 |
BXB 확장 프로그램의 등록 흐름은 별도 Ed25519 키와 X25519 키를 생성한다. 서버가 준 일회성 확인값과 사용자 식별자, 두 공개키를 연결한 등록 내용을 Ed25519 키로 서명한다. 서버는 로그인한 사용자와 확인값을 대조하고 등록 서명을 검사한다. 이 과정은 등록할 전송 공개키가 어느 사용자 요청에 속하는지 연결하는 절차다.
반면 중계 WebSocket의 사용자 인증에는 로그인 토큰을 사용하며, 서버 참여자에는 별도 접속 자격을 사용한다. 등록에 쓴 Ed25519 서명이 이후의 모든 중계 봉투에 자동으로 붙는다고 읽어서는 안 된다. 공개키 등록 때의 소유 증명과, 현재 접속한 주체의 인증과, 개별 메시지의 암호 보호는 서로 다른 단계다.
X25519 수신 키가 있다고 지갑의 ECDSA 서명을 혼자 만들 수 있는 것은 아니다. 지갑 조각을 보유했다고 서버의 모든 세션에 접속할 권한이 생기는 것도 아니다. 운영자가 기기 교체나 계정 비활성화를 처리할 때도 어느 자료와 자격을 바꾸는지 구별해야 한다. 같은 장치에 저장하더라도 논리적으로 같은 키가 되지는 않는다.
TLS 연결 위에 메시지를 다시 암호화하는 이유
P1이 중계 서버에 TLS로 연결하고, 서버가 P4와도 TLS로 연결했다고 하자. TLS는 각 연결 구간에서 도청과 변조를 막는 데 쓰이지만, 서버가 연결의 종단이면 서버에서 그 구간의 암호화가 풀린다. 따라서 연결을 보호했다는 사실만으로 서버가 애플리케이션 본문을 읽지 못한다고 할 수는 없다. 보호의 종단을 어디에 두는지는 TLS 1.3의 채널 모델과 함께 봐야 한다.
사용자 간 PeerJS 경로는 WebRTC 데이터 채널이라는 다른 통신 구성을 사용한다. WebRTC의 데이터 채널은 DTLS로 보호되며, 연결 신호와 데이터 채널의 역할도 구분된다. 이 원리는 WebRTC 보안 구조에서 확인할 수 있다. 이는 개별 BXB 배포의 연결 설정이나 참여자 인증 전체를 검증했다는 뜻은 아니다.
별도 cowork 모듈의 HPKE는 중계 연결 안에서 운반할 본문 자체를 수신자 공개키에 맞춰 보호한다. HPKE는 Hybrid Public Key Encryption의 약자다. 공개키 연산으로 필요한 비밀을 공유하고 대칭키 암호로 본문을 처리하는 구성을 사용한다. 핵심은 KEM·KDF·AEAD의 세 역할이다. RFC 9180의 구성
먼저 KEM, 키 캡슐화에서 P1은 P4의 공개키와 새로 만든 일회성 비밀값을 이용한다.
그 결과로 공유 비밀과 전달할 캡슐화 값 enc를 얻는다.
P4는 자신의 비밀키와 enc로 같은 공유 비밀을 얻는다.
여기서 확인한 X25519 구성의 enc는 일회성 공개키이며, P1이 지갑 비밀 조각을 암호화해 보낸 값이 아니다.
다음 KDF, 키 유도 함수가 공유 비밀과 사용 문맥으로부터 실제 암호 키와 필요한 값을 유도한다. 확인한 모듈은 HKDF-SHA256을 사용한다. HKDF는 입력을 키 재료로 추출하고, 용도와 문맥을 담은 정보를 사용해 필요한 길이로 확장한다. 서로 다른 용도의 값을 구별하는 이유는 HKDF의 설명에서도 다룬다.
마지막 AEAD, 부가 데이터를 인증하는 암호화가 MPC 메시지 본문을 암호화하고 검증용 태그를 만든다.
이 모듈의 선택은 AES-256-GCM이다. 수신자는 같은 문맥으로 검증과 복호화를 수행하며,
검증에 실패한 데이터를 정상 메시지로 받아들이지 않는다.
중계 모듈이 운반하는 것은 이 암호문과 enc, 그리고 전달에 필요한 바깥 정보다.
봉투의 주소도 본문과 함께 묶는다
본문이 암호화돼 있어도 “누구에게 보내는 어느 세션의 메시지인가”라는 꼬리표가 따로 놀면 곤란하다. P1이 P4에게 보낸 이번 송금 메시지를 이전 세션의 것으로 바꾸어 붙일 수 없어야 한다. 이 연결에 쓰는 것이 AAD, Additional Authenticated Data다. AAD는 암호문과 함께 검증하지만 암호화해서 숨기지는 않는 부가 데이터다. AEAD의 인터페이스는 본문과 AAD를 별도 입력으로 받는다.
BXB의 이 봉투는 세션 ID, 송신자와 수신자, 메시지 ID, 순번, 작업 표시와 명세 해시를 AAD에 넣는다.
P1이 P4에게 보내는 메시지 하나를 짧게 적으면 다음과 같은 관계다.
S-new, msg-7은 설명용 이름이다.
세션: S-new
보낸 사람: P1
받는 사람: P4
메시지: msg-7 / 순번 7
작업: SIGN
명세: 이번 세션의 해시
↓ AAD로 연결
암호화한 MPC 메시지 본문
수신 측은 봉투에서 같은 AAD를 구성해 복호화에 사용한다.
기존 암호문을 그대로 두고 세션이나 수신자만 바꾸면, 원래 함께 보호한 문맥과 맞지 않아 검증이 실패한다.
확인한 암호 모듈은 이 문맥을 HPKE의 키 유도용 info에도 연결한다.
봉투의 aadHash는 AAD를 식별·대조하기 위한 해시이며, 누구나 계산할 수 있는 해시 자체가 비밀키 인증을 대신하지는 않는다.
또한 수신자가 자신이 기대한 세션의 문맥인지 확인하는 일도 남는다. 암호화할 때 사용한 이름과 복호화할 때 사용한 이름이 같다는 사실만으로 그 세션에 참여할 업무 권한까지 생기지는 않는다. 중계의 세션·명세 검사와 수신 측의 암호문 검증은 이렇게 서로 다른 조건을 확인한다.
AAD에 넣은 내용은 중계에서 볼 수 있다. 서버가 봉투를 어디로 전달할지 판단하려면 수신자와 세션을 알아야 하기 때문이다. 암호화된 본문과 공개된 전달 정보가 함께 존재하는 것은 이 구조의 의도된 구분이다.
순번과 라운드는 같은 숫자가 아니다
P1이 보낸 봉투가 두 번 도착하거나, 이전 메시지가 뒤늦게 도착하면 어떻게 할까? 암호문이 올바르다는 사실만으로 이번에 처음 처리하는 메시지인지 알 수는 없다. 과거의 정상 암호문을 다시 보내는 재전송 공격, replay와 통신 과정의 중복도 세션 상태로 구별해야 한다.
확인한 중계 코드는 같은 세션에서 이미 사용한 messageId를 거절한다.
seq는 세션의 송신자·수신자 쌍별로 증가해야 한다.
예를 들어 P1→P4의 마지막 값이 7이면 7이나 6은 받아들이지 않는다.
P4→P1에는 별도의 순번이 있다. 이 검사는 순번이 반드시 하나씩 이어졌거나 빠진 메시지가 자동 복구됐다는 보장은 아니다.
round도 구별해야 한다. 이 서명 봉투에서 round는 SIGN이라는 작업 표시다.
임계서명 알고리즘 내부의 첫째·둘째 수학 라운드를 중계 서버가 이 필드로 판정한다는 뜻은 아니다.
실제 암호 프로토콜의 메시지 처리와 다음 단계 진행은 본문을 받은 연산 모듈의 책임이다.
전달 순서가 맞는 것과 암호학적으로 다음 계산에 사용할 수 있는 것은 같은 조건이 아니다.
여기서 nonce라는 이름도 세 가지 역할로 나누어야 한다. 계정의 트랜잭션 nonce는 체인 거래 순서를 구별한다. ECDSA의 비밀 nonce는 서명 계산에 쓰는 비밀값이다. AEAD nonce는 비밀일 필요는 없지만 같은 대칭키로 암호화할 때 중복되면 안 되는 입력이다. 거래 순번이나 서명 비밀값을 그대로 가져다 쓰는 자리가 아니다. 기기 등록 때 받은 일회성 확인값 역시 그 등록 절차의 용도에 속한다.
봉투의 seq와 HPKE 내부의 암호화 카운터도 자동으로 같은 값이 되는 것은 아니다.
확인한 Go 모듈의 봉투 암호화 호출은 새 일회성 키와 HPKE 문맥을 만들고,
그 문맥 안에서 AEAD nonce를 유도한다. 애플리케이션 순번은 AAD에 별도로 들어간다.
재사용할 수 있는 통신 객체와 재사용하면 안 되는 암호 자료를 이름만 보고 판단해서는 안 된다.
HPKE 자체가 모든 세션의 중복과 재시작을 관리하는 것은 아니다. RFC 9180의 replay 설명도 보호 범위를 한정한다. 중복 검사 기록과 세션 수명, 수신 처리 결과를 함께 관리해야 하며, 순번 하나로 전달·처리의 정확히 한 번 실행을 주장할 수는 없다.
서버가 안다는 것과 서명할 수 있다는 것
이제 중계 서버가 볼 수 있는 것을 정리해 보자. 별도 HPKE 봉투의 서버는 어느 접속 주체가 어느 세션에서 누구에게 보내는지, 메시지 ID·순번·작업 표시·키 버전과 암호문 크기를 알 수 있다. 전송 시각과 빈도로 작업 진행을 짐작할 수도 있다. 조정 서버가 거래 준비까지 맡는다면 그 업무에서 받은 거래 내용도 알고 있을 수 있다. 본문 암호화가 전체 업무 정보를 서버로부터 숨기는 것은 아니다.
반면 이 모듈은 수신자의 전송 비밀키로 본문을 열도록 구성한다. 중계 서버의 봉투 처리에는 참여자의 지갑 조각으로 서명을 계산하는 단계가 없다. 메시지를 보았다는 사실, 암호문을 보관했다는 사실, 특정 지갑의 정족수를 확보했다는 사실을 구별해야 한다.
인증된 송신자라는 표현도 범위를 정해야 한다.
BXB 중계는 접속 인증에서 얻은 주체를 바탕으로 from을 붙이고,
그 주체와 수신자가 세션 명세에 포함되는지, 봉투의 키 버전이 명세와 맞는지 확인한다.
이는 신뢰하는 중계가 인증된 연결과 송신자 표시를 연결하는 방식이다.
여기서 사용하는 HPKE base mode는 그 자체로 P1의 장기 신원키 소유를 증명하지 않는다. P4의 공개키를 아는 사람은 P4가 열 수 있는 새 암호문을 만들 수 있다. 따라서 복호화 성공을 “P1이 직접 서명한 봉투”의 증거로 취급할 수 없다. 송신자 식별을 어떤 접속 인증과 명세 배포에 기대는지 함께 보아야 한다. HPKE의 인증 모드 구분은 이런 차이를 설명한다.
마지막으로, 허용된 참여자가 보낸 올바른 암호문 안에도 잘못된 MPC 메시지가 들어갈 수 있다. 전송 계층은 암호문과 문맥을 검사하고, 암호 연산 모듈은 자기 프로토콜의 메시지와 증명을 검사한다. 업무 시스템은 그와 별도로 수취인·수량·권한을 확인해야 한다. 어느 한 검증의 성공도 나머지 검증을 대신하지 않는다.
읽지 못하는 서버도 계산을 멈출 수 있다
수신자 비밀키가 없는 중계가 본문을 열지 못하더라도, 연결을 끊거나 메시지 전달을 지연할 수는 있다. P1과 P4가 필요한 메시지를 받지 못하면, 각각 키 조각을 안전하게 보관하고 있어도 공동 서명은 진행되지 않는다. 비밀의 보호와 작업의 가용성은 다른 성질이다.
확인한 중계에는 세션 상태·만료·접속 여부 검사와 전달 결과가 있고, 클라이언트에는 메시지를 기다리다가 세션 실패를 처리하는 흐름이 있다. 그러나 중계가 전달 응답을 보냈다는 사실만으로 P4가 복호화와 암호 연산까지 마쳤다고 볼 수는 없다. 연결 재접속 역시 중단 전의 모든 연산 상태를 안전하게 되살렸다는 증거가 아니다. 진행 중인 P1·P4 세션에 P2를 끼워 넣는 대신, 참여자와 일회성 자료를 프로토콜에 맞춰 새로 다뤄야 한다.
예제에서 서명을 완성하기 전에 멈추고 거래도 제출하지 않았다면 A 90개, B 10개는 그대로다. 그 미제출 시도로 발생한 온체인 수수료도 없다. 이미 거래를 제출한 뒤 응답을 놓쳤다면, 중계 화면의 실패만 보고 송금을 다시 만들지 말고 체인의 처리 상태부터 확인해야 한다.
공동 서명과 제출을 마친 뒤에도 실행 영수증과 필요한 확정 수준을 별도로 확인한다. 이번 전송이 실제로 성공해야 A 80개, B 20개로 예제를 마무리할 수 있고, A의 기본 코인 잔액에서는 거래 수수료가 별도로 빠진다. 온체인에 포함됐지만 실행이 실패한 경우에는 가스비가 발생할 수 있으므로 미제출 중단과도 구분한다.
MPC 중계의 역할은 여러 사람의 계산을 같은 세션 안에서 이어 주는 것이다. 세션 명세는 누구와 어떤 조건으로 계산하는지 정하고, 메시지 보호는 본문과 전달 문맥을 연결하며, 참여자의 암호 모듈은 그 입력으로 공동 서명을 계산한다. 서버의 존재 자체보다 이 세 책임과 권한이 어디에서 만나고 어디에서 끝나는지를 보면, 키를 나누어 보관하는 설계가 실제 통신과 운영에서 무엇을 요구하는지 알 수 있다.