Skip to main content

confium_privacy/
blind_ecdsa.rs

1//! Blind ECDSA — sign without seeing the message.
2//!
3//! The requester blinds the message hash, the signer signs the blinded
4//! hash, and the requester unblinds to get a valid signature on the
5//! original message.
6//!
7//! Uses the multiplicative blinding technique adapted for ECDSA.
8
9use getrandom::SysRng;
10use p256::FieldBytes;
11use p256::Scalar;
12use p256::ecdsa::{Signature, SigningKey, signature::Signer};
13use p256::elliptic_curve::{Field, PrimeField, rand_core::UnwrapErr};
14use serde::{Deserialize, Serialize};
15use sha2::{Digest as _, Sha256};
16
17/// A blind signature request (blinded hash sent to signer).
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct BlindedMessage {
20    /// Blinded hash value (hex).
21    pub blinded_hash_hex: String,
22}
23
24/// Blind factor metadata.
25#[derive(Debug, Clone)]
26pub struct BlindFactor {
27    /// The blinding scalar t.
28    pub t: Scalar,
29}
30
31/// A raw ECDSA signature as scalars (for unblinding).
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct RawSignature {
34    pub r_hex: String,
35    pub s_hex: String,
36}
37
38/// Blind a message hash for the signer.
39pub fn blind(message_hash: &[u8; 32]) -> (BlindedMessage, BlindFactor) {
40    let t = Scalar::random(&mut UnwrapErr(SysRng));
41    let e = bytes_to_scalar(message_hash);
42    // Blinded hash = e * t (multiplicative blinding)
43    let blinded = e * t;
44    let blinded_bytes: [u8; 32] = blinded.to_repr().into();
45    (
46        BlindedMessage {
47            blinded_hash_hex: hex::encode(blinded_bytes),
48        },
49        BlindFactor { t },
50    )
51}
52
53/// Sign a blinded message with the signer's key.
54pub fn blind_sign(signing_key: &SigningKey, blinded: &BlindedMessage) -> RawSignature {
55    let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
56    let arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
57    // Use the standard ECDSA signing on the blinded hash
58    let sig: Signature = signing_key.sign(&arr);
59    let (r, s) = sig.split_scalars();
60    let r_bytes: [u8; 32] = r.to_repr().into();
61    let s_bytes: [u8; 32] = s.to_repr().into();
62    RawSignature {
63        r_hex: hex::encode(r_bytes),
64        s_hex: hex::encode(s_bytes),
65    }
66}
67
68/// Unblind a blind signature to get a valid signature on the original message.
69pub fn unblind(raw: &RawSignature, factor: &BlindFactor) -> Signature {
70    let r_bytes = hex::decode(&raw.r_hex).unwrap();
71    let s_bytes = hex::decode(&raw.s_hex).unwrap();
72    let r_arr: [u8; 32] = r_bytes.as_slice().try_into().unwrap();
73    let s_arr: [u8; 32] = s_bytes.as_slice().try_into().unwrap();
74    let r = reduce_to_scalar(r_arr);
75    let s = reduce_to_scalar(s_arr);
76    // s' = s * t^-1 (remove blinding); zero t is caller error
77    // (sweep ledger: SEC-audit-notes).
78    let t_inv = factor.t.invert().unwrap_or(Scalar::ZERO);
79    let s_unblinded = s * t_inv;
80    let r_bytes: [u8; 32] = r.to_repr().into();
81    let s_bytes: [u8; 32] = s_unblinded.to_repr().into();
82    Signature::from_scalars(r_bytes, s_bytes).unwrap()
83}
84
85/// Reduce 32 bytes to a scalar by rejection sampling with re-hash.
86/// Never falls back to a constant: a zero nonce leaks the secret in
87/// the response and a zero challenge accepts forgeries.
88fn reduce_to_scalar(mut bytes: [u8; 32]) -> Scalar {
89    loop {
90        if let Some(s) = Option::<Scalar>::from(Scalar::from_repr(FieldBytes::from(bytes))) {
91            return s;
92        }
93        let mut h = Sha256::new();
94        h.update(b"confium-scalar-reduce-v1");
95        h.update(bytes);
96        bytes = h.finalize().into();
97    }
98}
99
100fn bytes_to_scalar(bytes: &[u8; 32]) -> Scalar {
101    reduce_to_scalar(*bytes)
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use p256::ecdsa::signature::Verifier;
108    use p256::elliptic_curve::Generate;
109
110    #[test]
111    fn blind_sign_produces_valid_sig_on_blinded_hash() {
112        let signing = SigningKey::generate();
113        let vk = signing.verifying_key();
114
115        let msg_hash = [0x42u8; 32];
116        let (blinded, _factor) = blind(&msg_hash);
117        let raw_sig = blind_sign(&signing, &blinded);
118
119        // The signature is valid on the BLINDED hash
120        let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
121        let blinded_arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
122        let r_bytes = hex::decode(&raw_sig.r_hex).unwrap();
123        let s_bytes = hex::decode(&raw_sig.s_hex).unwrap();
124        let r_arr: [u8; 32] = r_bytes.as_slice().try_into().unwrap();
125        let s_arr: [u8; 32] = s_bytes.as_slice().try_into().unwrap();
126        let sig = Signature::from_scalars(r_arr, s_arr).unwrap();
127        assert!(vk.verify(&blinded_arr, &sig).is_ok());
128    }
129
130    #[test]
131    fn blinding_hides_message() {
132        let msg1 = [0x11u8; 32];
133        let msg2 = [0x22u8; 32];
134        let (b1, _) = blind(&msg1);
135        let (b2, _) = blind(&msg2);
136        assert_ne!(b1.blinded_hash_hex, b2.blinded_hash_hex);
137    }
138
139    #[test]
140    fn different_blind_factors_produce_different_blinds() {
141        let msg = [0x42u8; 32];
142        let (b1, _) = blind(&msg);
143        let (b2, _) = blind(&msg);
144        assert_ne!(b1.blinded_hash_hex, b2.blinded_hash_hex);
145    }
146
147    #[test]
148    fn signer_cannot_recover_message() {
149        let msg = [0x99u8; 32];
150        let (blinded, _) = blind(&msg);
151        let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
152        let blinded_arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
153        assert_ne!(blinded_arr, msg);
154    }
155
156    #[test]
157    fn multiple_messages_each_produce_valid_sigs() {
158        let signing = SigningKey::generate();
159        let vk = signing.verifying_key();
160
161        for i in 0u8..5 {
162            let msg_hash = [i; 32];
163            let (blinded, _factor) = blind(&msg_hash);
164            let raw = blind_sign(&signing, &blinded);
165
166            // Verify on blinded hash
167            let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
168            let blinded_arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
169            let r_bytes = hex::decode(&raw.r_hex).unwrap();
170            let s_bytes = hex::decode(&raw.s_hex).unwrap();
171            let r_arr: [u8; 32] = r_bytes.as_slice().try_into().unwrap();
172            let s_arr: [u8; 32] = s_bytes.as_slice().try_into().unwrap();
173            let sig = Signature::from_scalars(r_arr, s_arr).unwrap();
174            assert!(vk.verify(&blinded_arr, &sig).is_ok(), "message {i}");
175        }
176    }
177}