Introduction
A blind signature is a digital signature scheme in which the signer signs a message without seeing its contents, while the requester can later unblind the signature to obtain a regular signature on the original message. The blind signature was invented by David Chaum in 1982 in his paper "Blind Signatures for Untraceable Payments", where he used it to construct digital cash that was anonymous like physical cash yet could not be counterfeited. A Java learner should care because blind signatures are the cryptographic primitive behind anonymous voting systems, anonymous credentials, and modern privacy-preserving authentication protocols that are increasingly mandated by GDPR and similar data protection laws.
This tutorial walks through a blind signature java implementation of the Chaum RSA blind signature, using only java.math.BigInteger, java.security.MessageDigest, and java.security.SecureRandom. The program demonstrates the three roles (requester, signer, verifier) and shows that the unblinded signature on the original message is a regular RSA-PKCS1-v1_5 signature that the signer never produced directly.
What is a Blind Signature?
A blind signature is a two-party protocol between a requester (Alice) and a signer (Bob). Alice has a message m that she wants Bob to sign, but she does not want Bob to know m, and she does not want Bob to be able to later link the signature to this signing session. The protocol has three phases: blind, sign, and unblind.
In the blind phase, Alice picks a random blinding factor r and computes a blinded version of m called m'. She sends m' to Bob. In the sign phase, Bob signs m' using his private RSA key, producing s', and returns s' to Alice. In the unblind phase, Alice removes the blinding factor from s' to obtain s, which is a valid RSA signature on m. Bob has signed m without ever seeing it, and the signature s is unlinkable to the session in which Bob produced s'.
The mathematical core is the homomorphic property of RSA: the signature function Sign(x) = x^d mod n is multiplicative, so Sign(m * r^e) = Sign(m) * Sign(r^e) = Sign(m) * r mod n. Alice blinds by multiplying m with r^e mod n, asks Bob to sign the blinded value, then unblinds by dividing (in modular arithmetic, multiplying by r^{-1} mod n) the signed blinded value by r. The result is a signature on m alone.
Blind signatures differ from ring signatures (which anonymize the signer) and group signatures (which have a trusted opener). Blind signatures anonymize the message from the signer's perspective; the signer is identified, but the link between session and signed message is hidden.
How Blind Signatures Work - Step by Step
Step 1: Setup. Bob has an RSA keypair (n, e, d). The public key (n, e) is published; the private exponent d is held by Bob.
Step 2: Blinding. Alice has a message m. She picks a random r in [1, n-1] that is coprime to n (this is overwhelmingly likely if n is a product of two large primes). She computes the blinded message m' = Hash(m) * r^e mod n. The hash converts m into an integer the same size as n, as required by RSA-PKCS1-v1_5 signature.
Step 3: Signing. Bob signs the blinded message with his private key: s' = (m')^d mod n. Because of the multiplicative homomorphism, s' = Hash(m)^d * (r^e)^d mod n = Hash(m)^d * r mod n. Bob returns s' to Alice. Bob has not seen Hash(m) because it is masked by the random r^e.
Step 4: Unblinding. Alice computes the modular inverse of r, called r^{-1} mod n, and multiplies s' by r^{-1}: s = s' * r^{-1} mod n = Hash(m)^d * r * r^{-1} mod n = Hash(m)^d mod n. The result s is exactly the signature Bob would have produced if Alice had given him Hash(m) directly.
Step 5: Verification. Anyone with Bob's public key can verify that s^e mod n = Hash(m), which is the standard RSA-PKCS1-v1_5 verification equation. The signature is indistinguishable from a signature Bob produced in any other session.
Step 6: Unlinkability. Even if Bob logs every (m', s') pair he ever sees, he cannot map a given (m, s) back to the session that produced it, because the random r is known only to Alice.
Java Program for Blind Signatures
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.SecureRandom;
public class BlindSignatureDemo {
// Toy RSA keypair (small for demo; use 2048+ bits in production).
static final BigInteger n;
static final BigInteger e = new BigInteger("65537");
static final BigInteger d;
static {
SecureRandom rng = new SecureRandom();
BigInteger p = BigInteger.probablePrime(256, rng);
BigInteger q = BigInteger.probablePrime(256, rng);
n = p.multiply(q);
BigInteger phi = p.subtract(BigInteger.ONE).multiply(q.subtract(BigInteger.ONE));
d = e.modInverse(phi);
}
public static void main(String[] args) throws Exception {
String message = "Vote: YES on Proposal 42, voter id 0xA7F3";
// Alice (requester) blinds the message.
BigInteger r = pickBlindingFactor();
BigInteger mPrime = blind(message.getBytes("UTF-8"), r);
System.out.println("Original message: " + message);
System.out.println("Blinded message m' (hex): " + mPrime.toString(16).substring(0, 16) + "...");
// Bob (signer) signs the blinded message. He never sees the original.
BigInteger sPrime = signBlind(mPrime);
System.out.println("Signed blind s' (hex): " + sPrime.toString(16).substring(0, 16) + "...");
// Alice unblinds to obtain the signature on the original message.
BigInteger s = unblind(sPrime, r);
System.out.println("Unblinded s (hex): " + s.toString(16).substring(0, 16) + "...");
// Anyone can verify the unblinded signature using only the public key.
boolean ok = verify(message.getBytes("UTF-8"), s);
System.out.println("Signature verifies: " + ok);
// The signer Bob tries to link the unblinded signature to his session log.
// He cannot, because r was never revealed.
boolean linkable = sPrime.equals(s);
System.out.println("Signer can link session: " + linkable + " (should be false)");
}
static BigInteger pickBlindingFactor() {
SecureRandom rng = new SecureRandom();
BigInteger r;
do {
r = new BigInteger(n.bitLength(), rng);
} while (r.compareTo(BigInteger.ONE) <= 0 || r.compareTo(n) >= 0 || !r.gcd(n).equals(BigInteger.ONE));
return r;
}
static BigInteger blind(byte[] message, BigInteger r) throws Exception {
BigInteger h = hashToInteger(message);
BigInteger rPowE = r.modPow(e, n);
return h.multiply(rPowE).mod(n);
}
static BigInteger signBlind(BigInteger mPrime) {
// Signer applies the private exponent. He does not know the underlying message.
return mPrime.modPow(d, n);
}
static BigInteger unblind(BigInteger sPrime, BigInteger r) {
BigInteger rInv = r.modInverse(n);
return sPrime.multiply(rInv).mod(n);
}
static boolean verify(byte[] message, BigInteger s) throws Exception {
BigInteger h = hashToInteger(message);
BigInteger recovered = s.modPow(e, n);
return recovered.equals(h);
}
static BigInteger hashToInteger(byte[] message) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] digest = md.digest(message);
// Treat the digest as a positive integer in [0, n).
BigInteger h = new BigInteger(1, digest);
return h.mod(n);
}
}
Code Explanation - Line by Line
The RSA keypair is generated at class load time using SecureRandom and BigInteger.probablePrime. We use 256-bit primes here so the demo runs in milliseconds; the math is identical at production key sizes. The public exponent is the standard 65537, which is large enough to defeat small-exponent attacks and small enough to keep verification fast.
The pickBlindingFactor method draws r uniformly from [2, n-1] and rejects it if gcd(r, n) is not 1. The gcd check is essential: if r shares a factor with n, then r is not invertible mod n and the unblinding step fails. Because n is the product of two large primes, the probability that a random r shares a factor with n is approximately 2/p + 2/q, which is negligible in practice, but the check is cheap.
The blind method computes Hash(m) and multiplies by r^e mod n. The hash converts the variable-length message into a fixed-length integer in [0, n), matching the standard RSA-PKCS1-v1_5 signing flow. The blinding factor r^e mod n is computationally indistinguishable from a uniform random integer in [0, n), so the blinded message reveals no information about m beyond its hash.
The signBlind method is the only step the signer executes: apply the private exponent to whatever integer the requester sent. The signer does not see Hash(m) because it is masked by r^e. The signer logs (m', s') but cannot link them to (m, s) without r.
The unblind method multiplies by r^{-1} mod n, which exists because gcd(r, n) = 1. The product s = s' * r^{-1} mod n is exactly Hash(m)^d mod n, the same value Bob would have produced if Alice had handed him Hash(m) directly.
The verify method applies the public exponent and compares the result to Hash(m). The hashToInteger helper takes SHA-256 of the message and reduces mod n; a production implementation would use the full EMSA-PKCS1-v1_5 padding, but for the blind signature homomorphism we only need the integer Hash(m).
The final linkability check shows that sPrime and s differ, demonstrating that the signer cannot recognize his own signature once unblinded. This is the unlinkability property that makes blind signatures useful for anonymous voting and cash.
Sample Input and Output
Original message: Vote: YES on Proposal 42, voter id 0xA7F3 Blinded message m' (hex): 4a7c3e9b2f1d8a05... Signed blind s' (hex): 8b2c4d6e0f3a1b7c... Unblinded s (hex): 5e9f2c1a8d4b7306... Signature verifies: true Signer can link session: false (should be false)
The output confirms that the unblinded signature verifies against the original message, and that the blinded signature s' is different from the unblinded signature s, so the signer cannot recognize the session. The exact hex strings depend on the random r and the random RSA keypair, so they will differ on every run.
Common Mistakes to Avoid
The most common mistake is forgetting to hash the message before blinding. If Alice blinds the raw message bytes (treated as an integer), she loses the structure that makes RSA-PKCS1-v1_5 secure, and a malicious requester can construct m values that produce signatures on multiple distinct messages (the chosen-message attack on textbook RSA). Always hash m to an integer before blinding, or use a provably secure padding scheme such as RSASSA-PSS with random salt.
The second mistake is reusing the blinding factor r. If two signing sessions use the same r, the signer can divide the two blinded messages and obtain the ratio of the two underlying hashes, which leaks information. Always draw a fresh r per session from a CSPRNG.
The third mistake is not checking gcd(r, n) = 1. If the random r happens to share a factor with n, the modInverse call in unblind throws an ArithmeticException, which aborts the session and may leak that the requester hit the unlucky 1/p + 1/q probability. Always check the gcd and redraw r if it fails.
The fourth mistake is exposing the blinding factor r after the session. If r is leaked, the signer can link (m', s') to (m, s) by computing s = s' * r^{-1} mod n. Treat r as a session secret and zero it out of memory after use.
Real-World Applications
David Chaum's original 1982 application was untraceable digital cash: the bank blind-signs coin serial numbers, so it cannot later tell which customer spent a given coin. The eCash system built by Chaum's company DigiCash in the 1990s was the first commercial deployment, and modern privacy coins such as Monero, Zcash, and the experimental BlindCoin mixers all use blind signature variants.
Anonymous voting is the second classic application. The Civitas system, the Helios voting protocol used by the IACR, and the academic self-tallying voting schemes of Juels, Catalano, and Jakobsson all use blind signatures to let an election authority certify that a voter is on the rolls without linking the voter to the ballot they cast.
Anonymous credentials are the third application. Microsoft U-Prove, IBM Idemix, and the W3C Verifiable Credentials Privacy Preserve Working Group all use blind signature variants to let an issuer certify attributes about a holder without learning when or where the holder presents those attributes to a verifier. The Web Privacy Sandbox proposals in Chrome are now exploring blind signature variants for anti-fraud tokens that protect user privacy.
In the Java enterprise world, blind signatures are not in the standard JCA API but are straightforward to implement with BigInteger and MessageDigest as shown. The Bouncy Castle library has low-level RSA primitives that work for this construction. If you are building an anonymous voting system, a privacy-preserving loyalty program, or a token-based ad measurement system, the Chaum blind signature is a good starting point.
Conclusion
Blind signatures let a signer sign a message without seeing it, and the resulting signature is unlinkable to the signing session. We built a complete Chaum RSA blind signature implementation in Java, walked through blinding, signing, unblinding, and verification, and discussed the unlinkability and security properties. Next, study the Group Signature post to see how anonymity with a trusted opener differs, and the X.509 Certificate post to see how ordinary RSA signatures are wrapped in real-world certificate chains.
Comments
Comments
Post a Comment