confium-privacy 0.5.4

Privacy-preserving crypto: MPC, PSI, PIR, differential privacy, ring signatures
Documentation
//! Blind ECDSA — sign without seeing the message.
//!
//! The requester blinds the message hash, the signer signs the blinded
//! hash, and the requester unblinds to get a valid signature on the
//! original message.
//!
//! Uses the multiplicative blinding technique adapted for ECDSA.

use getrandom::SysRng;
use p256::Scalar;
use p256::ecdsa::{Signature, SigningKey, signature::Signer};
use p256::elliptic_curve::{Field, PrimeField, rand_core::UnwrapErr};
use serde::{Deserialize, Serialize};

/// A blind signature request (blinded hash sent to signer).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlindedMessage {
    /// Blinded hash value (hex).
    pub blinded_hash_hex: String,
}

/// Blind factor metadata.
#[derive(Debug, Clone)]
pub struct BlindFactor {
    /// The blinding scalar t.
    pub t: Scalar,
}

/// A raw ECDSA signature as scalars (for unblinding).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawSignature {
    pub r_hex: String,
    pub s_hex: String,
}

/// Blind a message hash for the signer.
pub fn blind(message_hash: &[u8; 32]) -> (BlindedMessage, BlindFactor) {
    let t = Scalar::random(&mut UnwrapErr(SysRng));
    let e = bytes_to_scalar(message_hash);
    // Blinded hash = e * t (multiplicative blinding)
    let blinded = e * t;
    let blinded_bytes: [u8; 32] = blinded.to_repr().into();
    (
        BlindedMessage {
            blinded_hash_hex: hex::encode(blinded_bytes),
        },
        BlindFactor { t },
    )
}

/// Sign a blinded message with the signer's key.
pub fn blind_sign(signing_key: &SigningKey, blinded: &BlindedMessage) -> RawSignature {
    let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
    let arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
    // Use the standard ECDSA signing on the blinded hash
    let sig: Signature = signing_key.sign(&arr);
    let (r, s) = sig.split_scalars();
    let r_bytes: [u8; 32] = r.to_repr().into();
    let s_bytes: [u8; 32] = s.to_repr().into();
    RawSignature {
        r_hex: hex::encode(r_bytes),
        s_hex: hex::encode(s_bytes),
    }
}

/// Unblind a blind signature to get a valid signature on the original message.
pub fn unblind(raw: &RawSignature, factor: &BlindFactor) -> Signature {
    let r_bytes = hex::decode(&raw.r_hex).unwrap();
    let s_bytes = hex::decode(&raw.s_hex).unwrap();
    let r_arr: [u8; 32] = r_bytes.as_slice().try_into().unwrap();
    let s_arr: [u8; 32] = s_bytes.as_slice().try_into().unwrap();
    let r = Scalar::from_repr(r_arr.into()).unwrap_or(Scalar::ZERO);
    let s = Scalar::from_repr(s_arr.into()).unwrap_or(Scalar::ZERO);
    // s' = s * t^-1 (remove blinding)
    let t_inv = factor.t.invert().unwrap_or(Scalar::ZERO);
    let s_unblinded = s * t_inv;
    let r_bytes: [u8; 32] = r.to_repr().into();
    let s_bytes: [u8; 32] = s_unblinded.to_repr().into();
    Signature::from_scalars(r_bytes, s_bytes).unwrap()
}

fn bytes_to_scalar(bytes: &[u8; 32]) -> Scalar {
    let fb = p256::FieldBytes::from(*bytes);
    Option::<Scalar>::from(Scalar::from_repr(fb)).unwrap_or(Scalar::ZERO)
}

#[cfg(test)]
mod tests {
    use super::*;
    use p256::ecdsa::signature::Verifier;
    use p256::elliptic_curve::Generate;

    #[test]
    fn blind_sign_produces_valid_sig_on_blinded_hash() {
        let signing = SigningKey::generate();
        let vk = signing.verifying_key();

        let msg_hash = [0x42u8; 32];
        let (blinded, _factor) = blind(&msg_hash);
        let raw_sig = blind_sign(&signing, &blinded);

        // The signature is valid on the BLINDED hash
        let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
        let blinded_arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
        let r_bytes = hex::decode(&raw_sig.r_hex).unwrap();
        let s_bytes = hex::decode(&raw_sig.s_hex).unwrap();
        let r_arr: [u8; 32] = r_bytes.as_slice().try_into().unwrap();
        let s_arr: [u8; 32] = s_bytes.as_slice().try_into().unwrap();
        let sig = Signature::from_scalars(r_arr, s_arr).unwrap();
        assert!(vk.verify(&blinded_arr, &sig).is_ok());
    }

    #[test]
    fn blinding_hides_message() {
        let msg1 = [0x11u8; 32];
        let msg2 = [0x22u8; 32];
        let (b1, _) = blind(&msg1);
        let (b2, _) = blind(&msg2);
        assert_ne!(b1.blinded_hash_hex, b2.blinded_hash_hex);
    }

    #[test]
    fn different_blind_factors_produce_different_blinds() {
        let msg = [0x42u8; 32];
        let (b1, _) = blind(&msg);
        let (b2, _) = blind(&msg);
        assert_ne!(b1.blinded_hash_hex, b2.blinded_hash_hex);
    }

    #[test]
    fn signer_cannot_recover_message() {
        let msg = [0x99u8; 32];
        let (blinded, _) = blind(&msg);
        let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
        let blinded_arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
        assert_ne!(blinded_arr, msg);
    }

    #[test]
    fn multiple_messages_each_produce_valid_sigs() {
        let signing = SigningKey::generate();
        let vk = signing.verifying_key();

        for i in 0u8..5 {
            let msg_hash = [i; 32];
            let (blinded, _factor) = blind(&msg_hash);
            let raw = blind_sign(&signing, &blinded);

            // Verify on blinded hash
            let blinded_bytes = hex::decode(&blinded.blinded_hash_hex).unwrap();
            let blinded_arr: [u8; 32] = blinded_bytes.as_slice().try_into().unwrap();
            let r_bytes = hex::decode(&raw.r_hex).unwrap();
            let s_bytes = hex::decode(&raw.s_hex).unwrap();
            let r_arr: [u8; 32] = r_bytes.as_slice().try_into().unwrap();
            let s_arr: [u8; 32] = s_bytes.as_slice().try_into().unwrap();
            let sig = Signature::from_scalars(r_arr, s_arr).unwrap();
            assert!(vk.verify(&blinded_arr, &sig).is_ok(), "message {i}");
        }
    }
}