chie_crypto/
blind.rs

1//! Privacy-preserving unlinkable tokens for anonymous credentials.
2//!
3//! This module provides unlinkable token credentials using commitments:
4//! - Issuer signs committed values without seeing actual token data
5//! - Tokens can be redeemed anonymously without linking to issuance
6//! - Useful for anonymous bandwidth credits, reputation tokens, etc.
7//!
8//! Protocol:
9//! 1. User creates token with random serial number
10//! 2. User creates commitment to token (hash of serial + blinding factor)
11//! 3. Issuer signs the commitment without seeing the serial
12//! 4. User redeems by revealing token, blinding, and signature
13//! 5. Verifier validates that commitment matches and signature is valid
14
15use blake3;
16use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
17use rand::Rng;
18use thiserror::Error;
19use zeroize::Zeroize;
20
21/// Errors for unlinkable token operations.
22#[derive(Debug, Error)]
23pub enum BlindError {
24    #[error("Invalid commitment")]
25    InvalidCommitment,
26    #[error("Invalid token signature")]
27    InvalidSignature,
28    #[error("Signature verification failed")]
29    VerificationFailed,
30    #[error("Invalid public key")]
31    InvalidPublicKey,
32    #[error("Token already spent")]
33    TokenAlreadySpent,
34}
35
36pub type BlindResult<T> = Result<T, BlindError>;
37
38/// An unlinkable token with a unique serial number.
39#[derive(Clone, Debug)]
40pub struct UnlinkableToken {
41    /// Unique serial number (prevents double-spending)
42    pub serial: [u8; 32],
43    /// Token value/amount
44    pub value: u64,
45    /// Expiration timestamp (Unix timestamp)
46    pub expiry: u64,
47}
48
49/// Blinding factor used to create commitment.
50#[derive(Clone, Zeroize)]
51#[zeroize(drop)]
52pub struct BlindingFactor {
53    factor: [u8; 32],
54}
55
56/// Commitment to a token (sent to issuer for signing).
57#[derive(Clone, Debug)]
58pub struct TokenCommitment {
59    commitment: [u8; 32],
60}
61
62/// Signed token commitment from issuer.
63#[derive(Clone, Debug)]
64pub struct SignedCommitment {
65    commitment: [u8; 32],
66    signature: [u8; 64],
67}
68
69/// Redeemable token with all information needed for verification.
70#[derive(Clone, Debug)]
71pub struct RedeemableToken {
72    pub token: UnlinkableToken,
73    pub blinding_factor: [u8; 32],
74    pub signature: [u8; 64],
75}
76
77impl UnlinkableToken {
78    /// Create a new token with random serial number.
79    pub fn new(value: u64, expiry: u64) -> Self {
80        let mut rng = rand::thread_rng();
81        let mut serial = [0u8; 32];
82        rng.fill(&mut serial);
83        Self {
84            serial,
85            value,
86            expiry,
87        }
88    }
89
90    /// Create token with specific serial (for testing).
91    pub fn with_serial(serial: [u8; 32], value: u64, expiry: u64) -> Self {
92        Self {
93            serial,
94            value,
95            expiry,
96        }
97    }
98
99    /// Serialize token to bytes for hashing.
100    pub fn to_bytes(&self) -> Vec<u8> {
101        let mut bytes = Vec::new();
102        bytes.extend_from_slice(&self.serial);
103        bytes.extend_from_slice(&self.value.to_le_bytes());
104        bytes.extend_from_slice(&self.expiry.to_le_bytes());
105        bytes
106    }
107}
108
109impl BlindingFactor {
110    /// Generate random blinding factor.
111    pub fn generate() -> Self {
112        let mut rng = rand::thread_rng();
113        let mut factor = [0u8; 32];
114        rng.fill(&mut factor);
115        Self { factor }
116    }
117
118    /// Create from bytes.
119    pub fn from_bytes(bytes: [u8; 32]) -> Self {
120        Self { factor: bytes }
121    }
122
123    /// Get bytes (warning: sensitive data).
124    pub fn to_bytes(&self) -> [u8; 32] {
125        self.factor
126    }
127
128    /// Create commitment to a token.
129    pub fn commit(&self, token: &UnlinkableToken) -> TokenCommitment {
130        let mut hasher = blake3::Hasher::new();
131        hasher.update(b"TOKEN_COMMITMENT:");
132        hasher.update(&token.to_bytes());
133        hasher.update(&self.factor);
134        TokenCommitment {
135            commitment: *hasher.finalize().as_bytes(),
136        }
137    }
138
139    /// Verify a commitment matches the token and blinding.
140    pub fn verify_commitment(&self, token: &UnlinkableToken, commitment: &TokenCommitment) -> bool {
141        let recomputed = self.commit(token);
142        recomputed.commitment == commitment.commitment
143    }
144}
145
146impl TokenCommitment {
147    /// Get commitment bytes.
148    pub fn as_bytes(&self) -> &[u8; 32] {
149        &self.commitment
150    }
151
152    /// Create from bytes.
153    pub fn from_bytes(bytes: [u8; 32]) -> Self {
154        Self { commitment: bytes }
155    }
156}
157
158/// Token issuer that signs commitments.
159pub struct BlindSigner {
160    signing_key: SigningKey,
161}
162
163impl BlindSigner {
164    /// Create new issuer with random key.
165    pub fn generate() -> Self {
166        let mut rng = rand::thread_rng();
167        Self {
168            signing_key: SigningKey::generate(&mut rng),
169        }
170    }
171
172    /// Create from existing key.
173    pub fn from_signing_key(signing_key: SigningKey) -> Self {
174        Self { signing_key }
175    }
176
177    /// Create from bytes.
178    pub fn from_bytes(bytes: &[u8; 32]) -> Self {
179        Self {
180            signing_key: SigningKey::from_bytes(bytes),
181        }
182    }
183
184    /// Get public key.
185    pub fn public_key(&self) -> BlindPublicKey {
186        BlindPublicKey {
187            verifying_key: self.signing_key.verifying_key(),
188        }
189    }
190
191    /// Sign a token commitment (issuer doesn't see actual token).
192    pub fn sign_commitment(&self, commitment: &TokenCommitment) -> SignedCommitment {
193        let signature = self.signing_key.sign(&commitment.commitment);
194        SignedCommitment {
195            commitment: commitment.commitment,
196            signature: signature.to_bytes(),
197        }
198    }
199
200    /// Get signing key bytes (warning: secret).
201    pub fn to_bytes(&self) -> [u8; 32] {
202        self.signing_key.to_bytes()
203    }
204}
205
206/// Public key for verifying redeemed tokens.
207#[derive(Clone, Debug)]
208pub struct BlindPublicKey {
209    verifying_key: VerifyingKey,
210}
211
212impl BlindPublicKey {
213    /// Create from bytes.
214    pub fn from_bytes(bytes: &[u8; 32]) -> BlindResult<Self> {
215        let verifying_key =
216            VerifyingKey::from_bytes(bytes).map_err(|_| BlindError::InvalidPublicKey)?;
217        Ok(Self { verifying_key })
218    }
219
220    /// Get public key bytes.
221    pub fn to_bytes(&self) -> [u8; 32] {
222        self.verifying_key.to_bytes()
223    }
224
225    /// Verify a redeemable token.
226    ///
227    /// Checks that:
228    /// 1. The commitment is correctly formed from token + blinding
229    /// 2. The signature on the commitment is valid
230    pub fn verify_token(&self, redeemable: &RedeemableToken) -> BlindResult<()> {
231        // Recompute commitment from token and blinding
232        let blinding = BlindingFactor::from_bytes(redeemable.blinding_factor);
233        let commitment = blinding.commit(&redeemable.token);
234
235        // Verify signature on commitment
236        let signature = Signature::from_bytes(&redeemable.signature);
237        self.verifying_key
238            .verify(&commitment.commitment, &signature)
239            .map_err(|_| BlindError::VerificationFailed)?;
240
241        Ok(())
242    }
243
244    /// Verify a signed commitment directly.
245    pub fn verify_commitment(&self, signed: &SignedCommitment) -> BlindResult<()> {
246        let signature = Signature::from_bytes(&signed.signature);
247        self.verifying_key
248            .verify(&signed.commitment, &signature)
249            .map_err(|_| BlindError::VerificationFailed)?;
250        Ok(())
251    }
252}
253
254/// Complete unlinkable token protocol.
255pub struct BlindSignatureProtocol;
256
257impl BlindSignatureProtocol {
258    /// User: Create a new token and commitment (Step 1-2).
259    ///
260    /// Returns (commitment to send to issuer, token, blinding factor to keep secret).
261    pub fn create_token(
262        value: u64,
263        expiry: u64,
264    ) -> (TokenCommitment, UnlinkableToken, BlindingFactor) {
265        let token = UnlinkableToken::new(value, expiry);
266        let blinding = BlindingFactor::generate();
267        let commitment = blinding.commit(&token);
268        (commitment, token, blinding)
269    }
270
271    /// Issuer: Sign a token commitment (Step 3).
272    pub fn issue_token(issuer: &BlindSigner, commitment: &TokenCommitment) -> SignedCommitment {
273        issuer.sign_commitment(commitment)
274    }
275
276    /// User: Create redeemable token (Step 4).
277    pub fn prepare_redemption(
278        token: UnlinkableToken,
279        blinding: BlindingFactor,
280        signed: SignedCommitment,
281    ) -> RedeemableToken {
282        RedeemableToken {
283            token,
284            blinding_factor: blinding.to_bytes(),
285            signature: signed.signature,
286        }
287    }
288
289    /// Verifier: Verify and redeem token (Step 5).
290    pub fn verify_and_redeem(
291        public_key: &BlindPublicKey,
292        redeemable: &RedeemableToken,
293        current_time: u64,
294    ) -> BlindResult<()> {
295        // Check expiry
296        if current_time > redeemable.token.expiry {
297            return Err(BlindError::VerificationFailed);
298        }
299
300        // Verify token
301        public_key.verify_token(redeemable)?;
302
303        Ok(())
304    }
305}
306
307#[cfg(test)]
308mod tests {
309    use super::*;
310
311    #[test]
312    fn test_unlinkable_token_flow() {
313        let issuer = BlindSigner::generate();
314        let public_key = issuer.public_key();
315
316        // User creates token and commitment
317        let (commitment, token, blinding) = BlindSignatureProtocol::create_token(100, u64::MAX);
318
319        // Issuer signs commitment without seeing token
320        let signed = BlindSignatureProtocol::issue_token(&issuer, &commitment);
321
322        // User prepares redeemable token
323        let redeemable = BlindSignatureProtocol::prepare_redemption(token, blinding, signed);
324
325        // Verifier redeems token
326        BlindSignatureProtocol::verify_and_redeem(&public_key, &redeemable, 0).unwrap();
327    }
328
329    #[test]
330    fn test_commitment_verification() {
331        let token = UnlinkableToken::new(50, u64::MAX);
332        let blinding = BlindingFactor::generate();
333        let commitment = blinding.commit(&token);
334
335        // Correct blinding should verify
336        assert!(blinding.verify_commitment(&token, &commitment));
337
338        // Wrong blinding should not verify
339        let wrong_blinding = BlindingFactor::generate();
340        assert!(!wrong_blinding.verify_commitment(&token, &commitment));
341    }
342
343    #[test]
344    fn test_different_tokens() {
345        let issuer = BlindSigner::generate();
346        let public_key = issuer.public_key();
347
348        // Create two different tokens
349        let (comm1, tok1, blind1) = BlindSignatureProtocol::create_token(100, u64::MAX);
350        let (comm2, tok2, blind2) = BlindSignatureProtocol::create_token(200, u64::MAX);
351
352        assert_ne!(tok1.serial, tok2.serial);
353        assert_ne!(comm1.as_bytes(), comm2.as_bytes());
354
355        // Sign both
356        let signed1 = issuer.sign_commitment(&comm1);
357        let signed2 = issuer.sign_commitment(&comm2);
358
359        // Both should redeem correctly
360        let redeem1 = BlindSignatureProtocol::prepare_redemption(tok1, blind1, signed1);
361        let redeem2 = BlindSignatureProtocol::prepare_redemption(tok2, blind2, signed2);
362
363        public_key.verify_token(&redeem1).unwrap();
364        public_key.verify_token(&redeem2).unwrap();
365    }
366
367    #[test]
368    fn test_wrong_blinding() {
369        let issuer = BlindSigner::generate();
370        let public_key = issuer.public_key();
371
372        let (commitment, token, _correct_blinding) =
373            BlindSignatureProtocol::create_token(100, u64::MAX);
374        let signed = issuer.sign_commitment(&commitment);
375
376        // Try to redeem with wrong blinding
377        let wrong_blinding = BlindingFactor::generate();
378        let wrong_redeemable = RedeemableToken {
379            token,
380            blinding_factor: wrong_blinding.to_bytes(),
381            signature: signed.signature,
382        };
383
384        // Should fail verification
385        assert!(public_key.verify_token(&wrong_redeemable).is_err());
386    }
387
388    #[test]
389    fn test_expired_token() {
390        let issuer = BlindSigner::generate();
391        let public_key = issuer.public_key();
392
393        // Create token that expires at time 1000
394        let (commitment, token, blinding) = BlindSignatureProtocol::create_token(100, 1000);
395        let signed = issuer.sign_commitment(&commitment);
396        let redeemable = BlindSignatureProtocol::prepare_redemption(token, blinding, signed);
397
398        // Should succeed before expiry
399        BlindSignatureProtocol::verify_and_redeem(&public_key, &redeemable, 999).unwrap();
400
401        // Should fail after expiry
402        assert!(BlindSignatureProtocol::verify_and_redeem(&public_key, &redeemable, 1001).is_err());
403    }
404
405    #[test]
406    fn test_unlinkability() {
407        let issuer = BlindSigner::generate();
408
409        // Same token value but different serials
410        let (comm1, tok1, _) = BlindSignatureProtocol::create_token(100, u64::MAX);
411        let (comm2, tok2, _) = BlindSignatureProtocol::create_token(100, u64::MAX);
412
413        // Commitments should be different (unlinkable)
414        assert_ne!(comm1.as_bytes(), comm2.as_bytes());
415        assert_ne!(tok1.serial, tok2.serial);
416
417        // Signatures should be different
418        let sig1 = issuer.sign_commitment(&comm1);
419        let sig2 = issuer.sign_commitment(&comm2);
420        assert_ne!(sig1.signature, sig2.signature);
421    }
422
423    #[test]
424    fn test_serialization() {
425        let token = UnlinkableToken::new(123, 456);
426        let bytes = token.to_bytes();
427        assert_eq!(bytes.len(), 32 + 8 + 8);
428
429        // Serial should be first 32 bytes
430        assert_eq!(&bytes[..32], &token.serial);
431    }
432
433    #[test]
434    fn test_key_serialization() {
435        let issuer = BlindSigner::generate();
436        let public_key = issuer.public_key();
437
438        // Serialize and deserialize
439        let issuer_bytes = issuer.to_bytes();
440        let issuer2 = BlindSigner::from_bytes(&issuer_bytes);
441
442        let pk_bytes = public_key.to_bytes();
443        let pk2 = BlindPublicKey::from_bytes(&pk_bytes).unwrap();
444
445        // Should work the same
446        let (commitment, token, blinding) = BlindSignatureProtocol::create_token(100, u64::MAX);
447        let signed = issuer2.sign_commitment(&commitment);
448        let redeemable = BlindSignatureProtocol::prepare_redemption(token, blinding, signed);
449
450        pk2.verify_token(&redeemable).unwrap();
451    }
452
453    #[test]
454    fn test_blinding_factor_zeroize() {
455        let factor = BlindingFactor::generate();
456        let _bytes = factor.to_bytes();
457
458        // Drop should zeroize (verified by type implementing Zeroize)
459        drop(factor);
460    }
461}