Rust로 프로덕션 수준 블라인드 서명 eCash 시스템 구축

발행: (2025년 12월 7일 오전 09:34 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

소개

블록체인 없이도 블라인드 서명을 이용해 익명 디지털 현금을 구현할 수 있습니다. 이 개념은 1983년 David Chaum이 제안했으며, 세계 최초 디지털 현금 시스템인 DigiCash의 기반이 되었습니다.
오늘날 Chaum 프로토콜의 완전하고 프로덕션‑레디 구현이 Rust로 제공됩니다.

리소스:
GitHub Repository | Live Demo | Crates.io

프로토콜 개요

전통적인 디지털 서명

Alice → Message → Bob signs → Alice receives signature

문제: Bob이 메시지 내용을 볼 수 있습니다.

블라인드 서명 프로토콜

Alice → Blinded Message → Bob signs → Alice unblinds → Valid signature

Bob은 원본 메시지를 전혀 보지 못합니다.

RSA 블라인드 서명 단계

  1. 블라인딩 (클라이언트)

    let message = hash(serial_number);
    let blinding_factor = random() % n;
    let blinded = (message * blinding_factor.pow(e)) % n;
  2. 서명 (서버)

    let blind_signature = blinded.pow(d) % n;
  3. 언블라인딩 (클라이언트)

    let signature = (blind_signature * blinding_factor.inverse()) % n;

결과는 원본 메시지에 대한 유효한 RSA 서명이 되며, 서버는 메시지 자체를 보지 못합니다.

시스템 아키텍처

┌──────────┐      ┌────────┐      ┌────────────┐
│  Client  │─────▶│ Nginx │────▶ │ API Server │
│ (Wallet) │      │  :80   │      │   :8080    │
└──────────┘      └────────┘      └──────┬─────┘

                        ┌────────────────┼────────────┐
                        ▼                ▼            ▼
                   ┌─────────┐    ┌─────────┐   ┌──────┐
                   │  Redis  │    │Postgres │   │ HSM  │
                   │ (Cache) │    │ (Audit) │   │(Keys)│
                   └─────────┘    └─────────┘   └──────┘

암호학 원시 구현 (Rust)

pub struct BlindUser {
    public_key: RsaPublicKey,
}

impl BlindUser {
    pub fn blind_message(&self, message: &[u8]) -> Result {
        let n = self.public_key.n();
        let e = self.public_key.e();

        // Hash the message
        let m = BigUint::from_bytes_be(&Sha256::digest(message));

        // Generate blinding factor
        let r = loop {
            let candidate = random_biguint(n.bits());
            if candidate > 1 && gcd(&candidate, n) == 1 {
                break candidate;
            }
        };

        // Blind: m' = m * r^e mod n
        let r_e = r.modpow(e, n);
        let blinded = (&m * &r_e) % n;

        Ok((blinded, r))
    }

    pub fn unblind_signature(
        &self,
        blind_sig: &BigUint,
        blinding_factor: &BigUint,
    ) -> Result {
        let n = self.public_key.n();
        let r_inv = mod_inverse(blinding_factor, n)?;

        // Unblind: s = s' * r^-1 mod n
        Ok((blind_sig * r_inv) % n)
    }
}

이중 지불 방지

pub async fn redeem_token(&self, token: &Token) -> Result {
    // Fast check in Redis
    if self.cache.exists(&token.serial).await? {
        return Err(Error::TokenAlreadySpent);
    }

    // Reliable check in PostgreSQL
    if self.db.is_token_spent(&token.serial).await? {
        return Err(Error::TokenAlreadySpent);
    }

    // Atomic transaction
    let tx_id = Uuid::new_v4();

    sqlx::query!(
        "INSERT INTO redeemed_tokens (serial, tx_id, timestamp) 
         VALUES ($1, $2, NOW())",
        token.serial,
        tx_id
    )
    .execute(&self.db.pool)
    .await?;

    self.cache.set(&token.serial, tx_id.to_string()).await?;

    Ok(tx_id.to_string())
}

HTTP 핸들러 (Axum)

async fn withdraw(
    State(state): State>,
    Json(req): Json,
) -> Result> {
    // Verify denomination
    if !state.is_valid_denomination(req.denomination) {
        return Err(ApiError::InvalidDenomination(req.denomination));
    }

    // Sign each blinded token
    let signatures = req.blinded_tokens
        .iter()
        .map(|blinded| {
            state.institution
                .sign_blinded(&BigUint::from_str(blinded)?)
        })
        .collect::>>()?;

    Ok(Json(WithdrawResponse {
        transaction_id: Uuid::new_v4().to_string(),
        blind_signatures: signatures,
        expires_at: Utc::now() + Duration::days(90),
    }))
}

성능 벤치마크

작업p50 지연시간p99 지연시간처리량
인출 (Withdrawal)45 ms120 ms150 req/s
교환 (Redemption)25 ms80 ms600 req/s
검증 (Verification)5 ms15 ms2 000 req/s

주된 병목은 RSA 연산(CPU‑집중)입니다. API 서버가 무상태이므로 수평 확장이 용이합니다.

배포

git clone https://github.com/ChronoCoders/ecash-protocol.git
cd ecash-protocol
docker-compose up -d

스택에 포함되는 구성 요소:

  • API 서버 (localhost:8080)
  • PostgreSQL 데이터베이스
  • Redis 캐시
  • 레이트 리밋이 적용된 Nginx 리버스 프록시

프로젝트에 클라이언트 라이브러리를 추가합니다:

[dependencies]
ecash-client = "0.1.0"

클라이언트 사용 예시

use ecash_client::Wallet;

#[tokio::main]
async fn main() -> Result {
    // Initialize wallet
    let mut wallet = Wallet::new(
        "http://localhost:8080".to_string(),
        "wallet.db".to_string(),
    )?;

    wallet.initialize().await?;

    // Withdraw $100 in $10 denominations
    let tokens = wallet.withdraw(100, 10).await?;
    println!("Withdrew {} tokens", tokens.len());

    // Check balance
    let balance = wallet.get_balance()?;
    println!("Balance: ${}", balance);

    // Spend $20
    let tx_id = wallet.spend(20).await?;
    println!("Transaction: {}", tx_id);

    Ok(())
}

보안 속성

  • 연결 불가능성 (Unlinkability) – 서버는 인출과 이후 교환을 연결시킬 수 없습니다.
  • 위조 방지 (Unforgeability) – 유효 토큰은 서버의 개인 RSA‑3072 키(128‑비트 보안) 없이는 생성할 수 없습니다.
  • 이중 지불 방지 – Redis와 PostgreSQL에서 원자적 검사를 수행해 동시 요청이 있더라도 토큰이 두 번 사용되지 않도록 보장합니다.

프로덕션 고려 사항

  • HSM을 이용한 개인 키 저장(디스크에 키를 저장하지 않음).
  • TLS/HTTPS를 통한 모든 네트워크 트래픽 보호.
  • 레이트 리밋(Nginx 설정)으로 DoS 공격 완화.
  • 모니터링 – 검증 실패 및 이중 지불 시도 감시.
  • 키 회전 – 주기적인 키 교체 구현(아직 포함되지 않음).

확장 전략

배포 규모예상 처리량
단일 노드~1 000 req/s
3‑노드 클러스터~3 000 req/s
10‑노드 클러스터~10 000 req/s

처리량이 ~5 000 req/s에 도달하면 데이터베이스가 병목이 되므로, 읽기 복제본을 추가하고 커넥션 풀을 확대합니다.

참고 문헌

  • Chaum, D. (1983). Blind Signatures for Untraceable Payments.
  • 저장소에 포함된 전체 학술 백서(164 KB) – 보안 증명, 프로토콜 사양, 성능 분석 포함.

기술 스택

  • 언어: Rust 1.91+
  • 웹 프레임워크: Axum 0.7
  • 데이터베이스: PostgreSQL 16
  • 캐시: Redis 7
  • 암호화: rsa 크레이트와 3072‑비트 키
  • 배포: Docker, Kubernetes

로드맵

  • 자동 키 회전 메커니즘 구현
  • 단일 트랜잭션에서 다중 금액 단위 지원 추가

Production‑Ready v1.0.0

Back to Blog

관련 글

더 보기 »