HSM 안의 키로 블록체인 거래에 서명하기까지
서버가 고객의 토큰을 보내려면 개인키로 거래에 서명해야 한다. 그런데 개인키를 HSM 안에 두고 서버가 그 값을 읽지 않는다면, 서명할 거래는 누가 만들고 결과는 어떻게 받아야 할까?
HSM은 Hardware Security Module, 암호키를 보호하고 암호 연산을 수행하는 장치다. 블록체인 연동에서는 장치 안의 키로 서명을 요청하고 그 결과를 받아 쓰는 역할을 맡는다. 이때 서버에는 여전히 거래를 구성하고, 서명 결과를 체인의 형식에 맞추고, 제출 결과를 확인하는 일이 남는다.
고객 계정과 블록체인 서명의 연결에서는 고객 식별자·지갑·키 선택의 관계를 살펴봤다. 이번에는 BXB의 HSM 연동 코드에서 키를 선택한 다음 실제 거래가 만들어질 때까지를 따라가 보자.
1. 토큰 10개를 보내지만, HSM에 전달하는 것은 거래 해시다
EVM 네트워크의 토큰 T를 A가 100개, B가 0개 가지고 있다고 하자. T의 소수 자릿수는 0이고 별도의 토큰 전송 수수료는 없다. A는 네트워크 가스비를 낼 기본 코인도 충분히 가지고 있다. 이번 목표는 A에서 B로 T 10개를 보내는 것이다. 가스비 대납이나 MPC 공동 서명은 사용하지 않는다.
A의 지갑은 HSM에 준비된 키와 연결돼 있다.
그 키를 찾는 이름을 설명용으로 K-A라고 부르자. 실제 키 이름이나 운영 거래를 옮긴 예제가 아니다.
요청자는 업무상 전송 권한이 있고, 지갑과 주소 관련 검사를 통과한다고 가정한다.
BXB가 구성하는 거래의 핵심은 다음과 같다. A의 다음 거래 순번은 42라고 하자.
보내는 주소: A
거래 목적지: 토큰 T의 컨트랙트
호출 내용: transfer(B, 10)
거래 순번(nonce): 42
토큰의 수취인은 B지만, 바깥 거래가 호출하는 주소는 T의 컨트랙트다. 여기에 네트워크 식별자, 가스 한도와 수수료 조건 등이 함께 들어간다. BXB는 이 서명 전 거래를 직렬화하고, 그 바이트로부터 Keccak-256 해시 32바이트를 만든다. HSM에는 이 해시와 사용할 키에 대한 참조를 전달해 서명을 요청한다.
이 예제의 EIP-1559 거래에서 무엇을 해시하는지는 거래 형식 명세에 정의돼 있다. 받는 주소와 수량만 따로 서명하는 것이 아니라, 거래를 실행할 조건까지 포함한 바이트에 서명한다. 따라서 거래 내용을 바꾸면 서명할 해시도 다시 만들어야 한다.
아직 토큰 잔액은 A 100 / B 0이다. 서명을 받았다는 사실과 체인이 그 거래를 실행했다는 사실은 구별된다.
2. 키 라벨과 키 핸들은 개인키 값과 다르다
소프트웨어 서명 경로에서는 서버의 서명 코드가 개인키 값을 사용한다. HSM 경로에서는 같은 자리에 키를 찾는 라벨이 연결된다. 라벨은 개인키를 암호화한 문자열이 아니라, 장치 안의 어느 키를 사용할지 식별하는 이름이다.
Java 프로그램이 이 장치와 연결하는 한 방법이 PKCS#11이다. 암호키를 담는 장치나 모듈에 키 생성·조회·서명 같은 작업을 요청하는 인터페이스다. 여기서 말하는 PKCS#11의 token은 암호키를 담는 쪽을 가리키며, 예제의 블록체인 토큰 T와는 다른 용어다.
BXB의 EVM 경로는 Java 암호 API의 provider, 즉 암호 연산을 수행하는 구현체를 통해 HSM을 사용한다. SunPKCS11을 사용하는 구성에서는 이 provider가 Java 호출을 장치의 PKCS#11 호출로 연결한다. Oracle의 PKCS#11 안내는 이 연결 역할과 장치가 제공해야 하는 알고리즘의 관계를 설명한다.
애플리케이션은 인증된 세션에서 K-A에 해당하는 키를 찾는다.
이때 받는 키 핸들은 장치 안의 키를 지정해 연산을 요청할 수 있게 하는 참조다.
Java 코드에서 PrivateKey라는 타입으로 받더라도, 이 HSM 서명 경로에서는 개인키의 숫자 값을 꺼내 계산하는 용도로 쓰지 않는다.
선택한 provider에 그 참조를 넘겨 서명을 요청한다.
| 구분 | 이 예제에서 하는 일 |
|---|---|
| 고객·지갑 식별자 | 업무 요청을 A의 지갑과 연결 |
키 라벨 K-A | HSM에서 사용할 키를 선택 |
| 키 핸들 | 세션에서 그 키로 연산을 요청 |
| 공개키와 주소 A | 서명 주체를 확인 |
키 라벨을 안다고 장치의 키를 바로 사용할 수 있는 것은 아니다. 장치 접근과 인증, 해당 키의 사용 조건이 함께 맞아야 한다. 반대로 올바른 키 핸들을 얻었다고 이번 송금의 수취인과 수량까지 업무적으로 승인된 것은 아니다.
3. 서명 전에 해시를 한 번 더 만들면 안 되는 이유
서명 API에는 원문을 받아 내부에서 해시하는 방식과, 이미 계산한 해시를 받아 서명하는 방식이 있다. 이 둘을 같은 것으로 다루면 의도와 다른 값에 서명하게 된다.
BXB의 EVM 서명 입력은 앞에서 만든 32바이트 해시다.
HSM 서명에서는 NONEwithECDSA를 사용한다.
여기서 NONE은 암호 연산이 없다는 뜻이 아니라, 이 API 단계에서 메시지를 다시 해시하지 않는다는 뜻이다.
SunPKCS11은 이를 CKM_ECDSA 메커니즘과 연결한다.
예를 들어 이미 Keccak-256으로 만든 해시를 SHA-256까지 수행하는 서명 API에 전달하면, 처음 거래 해시와는 다른 입력에 ECDSA 서명을 하게 된다. 키와 서명 알고리즘 이름만 맞추는 것으로는 충분하지 않은 이유다. BXB의 공통 EVM 서명 인터페이스도 입력이 정확히 32바이트인지 검사한 뒤 서명 구현을 호출한다.
이 경로를 역할별로 놓으면 다음과 같다.
HSM은 이 경로에서 해시를 서명한다. “B에게 토큰 10개”라는 업무 의미는 거래를 구성하는 쪽에서 결정한다. 따라서 입력 검증과 업무 인가를 마친 내용을 해시해야 한다.
4. HSM의 서명 결과가 곧 이더리움 거래는 아니다
ECDSA 서명 결과의 핵심은 두 정수 r과 s다. 하지만 서명 인터페이스가 반환한 바이트를 거래 뒤에 그대로 붙이는 것으로 끝나지는 않는다.
먼저 바이트 표현을 해석해야 한다. PKCS#11의 ECDSA 서명 형식과 Java 서명 API의 표현은 구별된다. 표준 PKCS#11 형식은 r과 s의 바이트를 이어 놓는 방식이고, 확인한 BXB의 Java 경로는 provider에서 받은 DER 형식의 결과를 파싱한다. DER은 정수의 종류와 길이를 포함해 구조화된 값을 인코딩하는 형식이다. “HSM은 언제나 DER을 반환한다”가 아니라, 지금 연결한 API의 반환 형식을 맞추는 작업이다.
그다음 low-s 정규화를 한다. ECDSA에는 같은 서명 관계를 나타내는 s와 반대쪽 값이 존재할 수 있어, 이더리움 거래는 s의 허용 범위를 제한한다. EIP-2는 s가 곡선 차수의 절반보다 큰 거래 서명을 거부하도록 정한다. BXB는 반환된 서명을 이 범위에 맞춘다.
마지막으로 서명자를 복구하는 데 필요한 정보를 맞춘다.
여기서 복구하는 것은 개인키가 아니라 공개키다.
해시와 r·s, 복구 후보를 이용해 얻은 공개키를 K-A에 대응하는 공개키와 비교한다.
BXB는 맞는 복구 값을 찾은 뒤 서명에서 공개키를 다시 얻어 일치 여부를 검사한다.
일치하는 값을 찾을 수 없으면 서명 결과를 정상 반환하지 않는다.
내부 라이브러리는 이를 v·r·s 형태로 전달하지만,
EIP-1559 거래에 최종 인코딩하는 값은 그 거래 형식에 맞는 yParity·r·s다.
서명 라이브러리의 표현과 실제 거래 필드도 구분해야 한다.
이 검사는 서명이 선택한 HSM 키와 맞는지 확인하는 일이다. 업무 요청이 애초에 올바른 고객 지갑을 골랐는지, 수량과 수취인이 맞는지는 앞 단계의 책임으로 남는다. 이후 BXB는 서명된 거래 바이트를 만들고 RPC 노드에 제출한다. 이때 계산하는 거래 해시 역시, 앞에서 서명을 요청했던 해시와 입력 바이트가 다르다.
5. 같은 HSM이라도 체인마다 서명 입력이 다르다
EVM의 설명을 다른 체인에 그대로 적용할 수는 없다. 확인한 BXB 구현은 EVM·Cardano·Solana의 HSM 경로를 구분한다. 서명 알고리즘뿐 아니라 그 알고리즘에 건네는 입력도 다르다.
| 경로 | 알고리즘 | 서명에 전달하는 입력 |
|---|---|---|
| EVM | secp256k1 ECDSA | 거래의 Keccak-256 해시 |
| Cardano | Ed25519 | 거래 본문의 Blake2b-256 해시 |
| Solana | Ed25519 | 직렬화한 거래 메시지 |
이 표는 거래 서명 경로의 비교다. Cardano와 Solana가 같은 Ed25519를 사용한다고 해서 Solana 메시지도 Cardano처럼 먼저 해시해 넘기면 안 된다. 여기서 “먼저 해시하지 않는다”는 말은 Ed25519 알고리즘 내부에 해시 연산이 없다는 뜻이 아니다. 호출자가 어떤 바이트를 서명 입력으로 제공하는지를 구분하는 말이다.
BXB의 Cardano·Solana 경로는 PKCS#11의 CKM_EDDSA를 직접 호출하는 별도 연동을 사용한다.
EVM의 Java 서명 provider를 바꾸는 것만으로 모든 체인의 입력과 결과가 저절로 맞춰지는 구조는 아니다.
HSM 구성에서 XRPL·BIP122에 대한 키 작업은 미지원으로 거절하는 분기도 있다.
따라서 HSM 지원 여부를 판단하려면 장치 연결뿐 아니라 키 종류, 연산 메커니즘, 입력 바이트와 반환 형식까지 맞춰 봐야 한다. 이 글은 확인한 BXB 코드의 연결 범위를 설명하며, 모든 HSM 제품과 체인 조합의 호환 시험 결과를 뜻하지 않는다.
6. 연결 실패와 서명 재시도를 어디에서 다룰까
HSM을 사용하도록 설정했는데 장치를 열 수 없다면 어떻게 해야 할까? BXB의 HSM 관리자는 시작 시 provider와 키 저장소를 열어 보는 검사를 한다. 그 과정이 실패하면 초기화 오류로 처리한다. HSM 사용 설정 아래에서 연결이 안 된다는 이유로 소프트웨어 키를 사용하는 경로로 조용히 전환하지 않는다.
이 시작 검사가 모든 키의 존재와 모든 체인의 서명 성공까지 확인하는 것은 아니다. Cardano·Solana의 별도 세션은 실제 사용 시 초기화되는 경로를 갖는다. 장치 연결, 사용할 키, 필요한 서명 연산은 각각 확인할 조건이다.
시작에 성공해도 실행 중 세션이 끊기거나 키 조회·서명 호출이 실패할 수 있다. 확인한 EVM 구현은 공개키 조회나 서명 작업에서 예외가 나면 서명 세션을 다시 준비하고 해당 작업을 한 번 재시도한다. 재시도도 실패하면 오류를 반환한다. 이 처리를 모든 오류의 원인을 정확히 진단하거나 무한히 복구하는 기능으로 이해할 필요는 없다.
여기서 다시 하는 것은 공개키 읽기 또는 서명 계산이다. 아직 체인에 거래를 제출하는 단계가 아니므로, 이 재시도 자체가 토큰을 두 번 보내지는 않는다. 다만 같은 해시를 다시 서명한다고 매번 같은 서명 바이트를 받는다고 가정해서도 안 된다. 서명 결과와 실제 제출 여부를 나눠 추적해야 한다.
이미 RPC로 거래를 보낸 뒤 응답이 끊겼다면 다른 문제다. 그 경우는 타임아웃과 재전송을 다룬 글처럼 기존 거래가 처리됐는지를 먼저 확인해야 한다. HSM 세션 재연결이 이미 제출한 거래를 취소하거나 실패로 확정해 주지는 않는다.
7. 새 키 생성과 기존 키 가져오기는 같은 이력이 아니다
키를 준비하는 방법도 구분해야 한다. BXB에는 HSM provider를 통해 새 키를 생성하는 경로와, 기존 개인키를 가져와 장치에 등록하는 경로가 있다. 이 글의 서명 예제는 키가 준비된 뒤부터 시작했으므로 두 경우 모두 같은 라벨·서명 인터페이스로 이어질 수 있다.
그러나 키의 이력은 다르다. 장치에서 새로 생성한 키와 외부에서 생성해 가져온 키를 모두 “생성부터 지금까지 개인키가 장치 밖에 있었던 적이 없다”고 설명할 수는 없다. 기존 키를 가져오는 경우에는 외부의 원본과 복사본, 가져오기 과정도 관리 대상이다.
키를 내보낼 수 있는지 역시 단순히 HSM이라는 이름만으로 정해지지 않는다. PKCS#11 명세는 키의 민감성과 추출 가능성에 관한 속성을 구분한다. 실제 보장 범위는 키를 만든 방식, 키 속성과 장치 정책을 함께 확인해야 한다. 서명할 때 개인키 값을 읽지 않는 구조와 그 키의 전체 수명 동안 외부 복사본이 없다는 보장은 다른 주장이다.
8. 서명을 받았을 때와 송금이 끝났을 때
처음의 A와 B로 돌아가 보자.
BXB는 K-A로 32바이트 해시의 서명을 요청하고, 결과를 이더리움 형식에 맞춰 공개키와 대조한다.
서명된 거래를 제출한 뒤 실행 성공과 토큰 이동을 확인했을 때 A 90 / B 10이라는 목표에 도달한다.
네트워크 가스비는 A의 기본 코인에서 별도로 지불하며 토큰 10개에서 빼지 않는다.
반대로 이 요청의 HSM 서명 단계에서 끝내 실패해 제출하지 못했다면, 다른 거래가 없는 한 A 100 / B 0이 유지되고 이 요청의 온체인 거래 수수료도 발생하지 않는다. 이미 제출된 거래의 결과가 불명확한 경우를 이 실패 사례에 섞어서는 안 된다.
HSM의 역할은 키를 보호하고 그 키로 허용된 암호 연산을 수행하는 데 있다. 업무 시스템은 그 앞에서 올바른 거래 내용을 결정하고, 그 뒤에서 서명 형식과 실행 결과를 확인한다. 이 역할이 맞물려야 장치 안의 키가 고객이 의도한 블록체인 거래로 이어진다.