Building a Production-Ready Blind Signature eCash System in Rust
Source: Dev.to
Introduction
Anonymous digital cash can be achieved without a blockchain using blind signatures, a concept introduced by David Chaum in 1983. This protocol underpins the world’s first digital cash system, DigiCash.
Today, a complete, production‑ready implementation of Chaum’s protocol is available in Rust.
Resources:
GitHub Repository | Live Demo | Crates.io
Protocol Overview
Traditional Digital Signature
Alice → Message → Bob signs → Alice receives signature
Problem: Bob sees the message content.
Blind Signature Protocol
Alice → Blinded Message → Bob signs → Alice unblinds → Valid signature
Bob never sees the original message.
RSA Blind Signature Steps
-
Blinding (Client)
let message = hash(serial_number); let blinding_factor = random() % n; let blinded = (message * blinding_factor.pow(e)) % n; -
Signing (Server)
let blind_signature = blinded.pow(d) % n; -
Unblinding (Client)
let signature = (blind_signature * blinding_factor.inverse()) % n;
The result is a valid RSA signature on the original message, while the server never sees the message itself.
System Architecture
┌──────────┐ ┌────────┐ ┌────────────┐
│ Client │─────▶│ Nginx │────▶ │ API Server │
│ (Wallet) │ │ :80 │ │ :8080 │
└──────────┘ └────────┘ └──────┬─────┘
│
┌────────────────┼────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌──────┐
│ Redis │ │Postgres │ │ HSM │
│ (Cache) │ │ (Audit) │ │(Keys)│
└─────────┘ └─────────┘ └──────┘
Cryptographic Primitives (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)
}
}
Double‑Spend Prevention
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 Handlers (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),
}))
}
Performance Benchmarks
| Operation | p50 Latency | p99 Latency | Throughput |
|---|---|---|---|
| Withdrawal | 45 ms | 120 ms | 150 req/s |
| Redemption | 25 ms | 80 ms | 600 req/s |
| Verification | 5 ms | 15 ms | 2 000 req/s |
The primary bottleneck is RSA computation (CPU‑bound). Because API servers are stateless, horizontal scaling is straightforward.
Deployment
git clone https://github.com/ChronoCoders/ecash-protocol.git
cd ecash-protocol
docker-compose up -d
The stack includes:
- API server (
localhost:8080) - PostgreSQL database
- Redis cache
- Nginx reverse proxy with rate limiting
Add the client library to your project:
[dependencies]
ecash-client = "0.1.0"
Client Usage Example
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(())
}
Security Properties
- Unlinkability – The server cannot associate a withdrawal with a later redemption.
- Unforgeability – Valid tokens require the server’s private RSA‑3072 key (128‑bit security).
- Double‑Spend Prevention – Atomic checks in Redis and PostgreSQL guarantee that a token cannot be spent twice, even under concurrent requests.
Production Considerations
- HSM for private key storage (never store keys on disk).
- TLS/HTTPS for all network traffic.
- Rate limiting (configured in Nginx) to mitigate DoS attacks.
- Monitoring of verification failures and double‑spend attempts.
- Key rotation – implement periodic rotation (not yet included).
Scaling Strategy
| Deployment Size | Approx. Throughput |
|---|---|
| Single node | ~1 000 req/s |
| 3‑node cluster | ~3 000 req/s |
| 10‑node cluster | ~10 000 req/s |
When throughput approaches ~5 000 req/s, the database becomes the bottleneck; add read replicas and increase connection pooling.
References
- Chaum, D. (1983). Blind Signatures for Untraceable Payments.
- Full academic whitepaper (164 KB) in the repository – includes security proofs, protocol specification, and performance analysis.
Technical Stack
- Language: Rust 1.91+
- Web Framework: Axum 0.7
- Database: PostgreSQL 16
- Cache: Redis 7
- Crypto:
rsacrate with 3072‑bit keys - Deployment: Docker, Kubernetes
Roadmap
- Implement automated key rotation mechanism
- Add support for multiple denominations in a single transaction
Production‑Ready v1.0.0