바이낸스(Binance) API 서명의 표준 알고리즘은 HMAC-SHA256입니다. 이는 Secret Key를 키(Key)로 사용하고 쿼리 문자열(Query String)을 메시지(Message)로 사용하여 생성된 64자리 16진수 문자열을 signature 파라미터로 요청 끝에 추가하는 방식입니다. 2023년부터는 보안성이 더 높은 Ed25519 비대칭 서명 방식도 추가되었습니다. 본문에서는 알고리즘 원리, 5개 언어별 구현 방법, 그리고 8가지 일반적인 서명 오류 해결 방법을 자세히 설명합니다. 바이낸스 계정이 없으신 분은 먼저 바이낸스 공식 사이트에서 가입을 완료해 주시고, 신규 사용자는 무료 가입을 통해 시작할 수 있습니다.
1. HMAC-SHA256의 수학적 원리
HMAC(Hash-based Message Authentication Code)은 해시 기반 메시지 인증 코드로, 공식은 다음과 같습니다:
HMAC(K, m) = H((K ⊕ opad) || H((K ⊕ ipad) || m))
여기서:
- K는 키(Secret Key)
- m은 메시지(쿼리 문자열)
- H는 SHA256 해시 함수
- ipad = 0x36을 64번 반복
- opad = 0x5C를 64번 반복
- ⊕는 XOR 연산, ||는 연결(Concatenation)을 의미함
바이낸스 적용 프로세스:
- 모든 파라미터를 순서대로
key1=value1&key2=value2형식으로 연결합니다. - Secret Key를 HMAC 키로, 위 문자열을 메시지로 사용합니다.
- SHA256 해시 값을 계산하여 32바이트 결과물을 얻습니다.
- 이를 64자리 16진수(Hexadecimal) 문자열로 변환합니다.
2. 바이낸스 서명의 3가지 파라미터 규칙
규칙 1: 파라미터 순서가 서명과 일치해야 함
# 올바른 예: 서명 생성 시 사용된 순서와 요청 쿼리가 일치함
params = {"symbol": "BTCUSDT", "timestamp": 1713027384562}
signature = hmac_sha256("symbol=BTCUSDT×tamp=1713027384562")
final_query = "symbol=BTCUSDT×tamp=1713027384562&signature=" + signature
# 잘못된 예: 서명에 사용된 순서와 final_query의 순서가 다르면 -1022 오류 발생
규칙 2: signature는 서명 계산에 포함되지 않음
# signature 필드는 항상 마지막에 위치하며, HMAC 계산 대상이 아님
params = {"symbol": "BTCUSDT", "timestamp": 1713027384562}
sig = hmac_sha256(urlencode(params)) # 먼저 서명을 계산
params["signature"] = sig # 그 다음 필드 추가
규칙 3: 숫자에 따옴표를 넣거나 문자열에 공백을 넣지 않음
# 올바른 형식
timestamp=1713027384562
# 잘못된 형식 (서명 값이 달라짐)
timestamp="1713027384562"
timestamp= 1713027384562
3. Python 표준 구현
import hmac
import hashlib
from urllib.parse import urlencode
SECRET_KEY = "YOUR_SECRET_KEY"
def sign(params: dict) -> str:
"""표준 서명 구현"""
query = urlencode(params)
return hmac.new(
SECRET_KEY.encode('utf-8'),
query.encode('utf-8'),
hashlib.sha256
).hexdigest()
# 테스트
params = {
"symbol": "BTCUSDT",
"side": "BUY",
"type": "LIMIT",
"timeInForce": "GTC",
"quantity": "0.001",
"price": "60000.00",
"timestamp": 1713027384562
}
print(sign(params))
# 출력 예시: b42e1fa3d8c7e9f2a6b5c4d1e8f9a0b3c2d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9
4. Node.js 표준 구현
const crypto = require('crypto');
const SECRET_KEY = 'YOUR_SECRET_KEY';
function sign(params) {
// URLSearchParams는 자동으로 인코딩을 처리함
const query = new URLSearchParams(params).toString();
return crypto
.createHmac('sha256', SECRET_KEY)
.update(query)
.digest('hex');
}
const params = {
symbol: 'BTCUSDT',
side: 'BUY',
type: 'LIMIT',
timeInForce: 'GTC',
quantity: '0.001',
price: '60000.00',
timestamp: Date.now()
};
console.log(sign(params));
주의: Node.js에서 JSON.stringify로 얻은 JSON 문자열은 서명에 사용할 수 없습니다. 반드시 URLSearchParams를 사용하거나 수동으로 연결해야 합니다.
5. Go 표준 구현
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/url"
)
func sign(params url.Values, secret string) string {
query := params.Encode()
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(query))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
params := url.Values{}
params.Set("symbol", "BTCUSDT")
params.Set("side", "BUY")
params.Set("type", "LIMIT")
params.Set("timeInForce", "GTC")
params.Set("quantity", "0.001")
params.Set("price", "60000.00")
params.Set("timestamp", "1713027384562")
fmt.Println(sign(params, "YOUR_SECRET_KEY"))
}
중요: url.Values.Encode()는 알파벳 순서로 정렬하고 자동으로 URL 인코딩을 수행하여 Python의 urlencode와 동일한 결과물을 생성합니다.
6. Rust 구현
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
fn sign(query: &str, secret: &str) -> String {
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.expect("HMAC 초기화 오류");
mac.update(query.as_bytes());
hex::encode(mac.finalize().into_bytes())
}
fn main() {
let query = "symbol=BTCUSDT&side=BUY&type=LIMIT&timeInForce=GTC&quantity=0.001&price=60000.00×tamp=1713027384562";
let secret = "YOUR_SECRET_KEY";
println!("{}", sign(query, secret));
}
Cargo.toml 설정:
[dependencies]
hmac = "0.12"
sha2 = "0.10"
hex = "0.4"
7. Java 구현
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
public class BinanceSigner {
public static String sign(String query, String secret) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(keySpec);
byte[] hash = mac.doFinal(query.getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : hash) {
hex.append(String.format("%02x", b));
}
return hex.toString();
}
public static void main(String[] args) throws Exception {
String query = "symbol=BTCUSDT×tamp=1713027384562";
String secret = "YOUR_SECRET_KEY";
System.out.println(sign(query, secret));
}
}
8. Ed25519 대체 방안 (최신)
2023년 바이낸스는 Self-generated API Key를 출시하여 Ed25519 비대칭 서명을 사용할 수 있게 했습니다. 장점은 Secret이 로컬 환경을 절대 떠나지 않는다는 것입니다.
1. 키 쌍 생성
openssl genpkey -algorithm Ed25519 -out private.pem
openssl pkey -in private.pem -pubout -out public.pem
# public.pem을 바이낸스 API 관리 페이지에 업로드
2. Python Ed25519 서명 구현
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
import base64
with open("private.pem", "rb") as f:
private_key = load_pem_private_key(f.read(), password=None)
def ed25519_sign(query: str) -> str:
signature_bytes = private_key.sign(query.encode())
return base64.b64encode(signature_bytes).decode()
# 사용 방식은 HMAC과 유사하나, 결과값이 Hex가 아닌 Base64임
query = "symbol=BTCUSDT×tamp=1713027384562"
sig = ed25519_sign(query)
final_url = f"/api/v3/order?{query}&signature={sig}"
9. 서명 오류 체크리스트
-1022 Signature for this request is not valid 오류 발생 시 다음 순서로 확인하세요:
| 번호 | 문제점 | 확인 방법 |
|---|---|---|
| 1 | Secret Key에 불필요한 공백/줄바꿈 | len(secret)이 64자리인지 확인 |
| 2 | 파라미터 순서 불일치 | 서명에 사용된 문자열과 실제 요청 쿼리를 대조 |
| 3 | signature가 HMAC 계산에 포함됨 | 서명 계산 후 signature 필드를 추가했는지 확인 |
| 4 | URL 인코딩 방식 차이 | 일부 라이브러리는 @를 %40으로 인코딩함, 인코딩 소스 확인 |
| 5 | 숫자에 따옴표 사용 | timestamp="123"과 timestamp=123은 서로 다른 서명을 생성함 |
| 6 | 문자 집합(Charset) 오류 | 한글 등의 문자는 반드시 UTF-8로 인코딩되어야 함 |
| 7 | 키 유형 혼용 | HMAC 키는 HMAC 알고리즘에, Ed25519는 Ed25519 알고리즘에 사용 |
| 8 | POST body vs query string | POST 요청 시 파라미터 위치(Body 또는 URL) 중복 여부 확인 |
10. 서명 디버깅용 도구 코드
def debug_sign(params, secret):
"""단계별 상세 출력"""
query = urlencode(params)
print(f"Step 1. 쿼리 문자열: {query}")
print(f" 길이: {len(query)} 바이트")
sig = hmac.new(secret.encode(), query.encode(), hashlib.sha256).hexdigest()
print(f"Step 2. 서명 결과: {sig}")
print(f" 길이: {len(sig)}자 (64자여야 함)")
final = f"{query}&signature={sig}"
print(f"Step 3. 최종 URL: {final}")
return sig
debug_sign({"symbol": "BTCUSDT", "timestamp": 1713027384562}, "YOUR_SECRET")
11. 자주 묻는 질문 FAQ
Q1: 동일한 파라미터인데 서명 결과가 항상 같은가요?
A: HMAC-SHA256은 결정론적 알고리즘으로, 동일한 입력에 대해 항상 동일한 출력을 냅니다. 그래서 timestamp가 중요합니다. 매 밀리초마다 값이 변하므로 매번 다른 서명이 생성되어 재전송 공격(Replay Attack)을 방지할 수 있습니다.
Q2: 서명 값으로 Secret Key를 유추할 수 있나요?
A: 불가능합니다. SHA-256은 단방향 함수이며, HMAC 값에서 Secret을 역추산하는 것은 수학적으로 불가능에 가깝습니다(약 2^128번의 연산 필요).
Q3: signature는 왜 항상 64자인가요?
A: SHA-256 출력값은 256비트 = 32바이트입니다. 이를 1바이트당 2개의 16진수 문자로 변환하면 정확히 64자가 됩니다. 길이가 64자가 아니라면 구현 방식에 문제가 있는 것입니다.
Q4: 타임스탬프를 마이크로초(microseconds)로 써도 되나요?
A: 안 됩니다. 바이낸스는 밀리초(13자리 숫자)를 요구합니다. 16자리의 마이크로초를 사용하면 미래의 시간으로 인식되어 -1021 Timestamp out of recv window 오류가 발생합니다.
Q5: Ed25519와 HMAC-SHA256 중 무엇이 더 안전한가요?
A: Ed25519가 더 안전합니다. 개인키가 사용자의 기기를 절대 떠나지 않고 바이낸스에는 공개키만 제공되기 때문입니다. 다만 SDK 지원 범용성 측면에서는 HMAC이 더 성숙해 있으므로, 일반적인 퀀트 전략에서는 HMAC으로도 충분히 안전합니다.
서명 원리를 확인하셨다면 카테고리로 돌아가 「API 연동」 카테고리의 다른 기술 주제를 살펴보세요.