mithril_stm/signature_scheme/unique_schnorr_signature/
signing_key.rs

1use anyhow::{Context, anyhow};
2use rand_core::{CryptoRng, RngCore};
3use serde::{Deserialize, Serialize};
4
5use crate::StmResult;
6
7use super::{
8    BaseFieldElement, PrimeOrderProjectivePoint, ProjectivePoint, ScalarFieldElement,
9    SchnorrVerificationKey, UniqueSchnorrSignature, UniqueSchnorrSignatureError,
10    compute_poseidon_digest,
11};
12
13/// Schnorr Signing key, it is essentially a random scalar of the Jubjub scalar field
14#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
15pub struct SchnorrSigningKey(pub(crate) ScalarFieldElement);
16
17impl SchnorrSigningKey {
18    /// Generate a random scalar value to use as signing key
19    pub fn generate<R: RngCore + CryptoRng>(rng: &mut R) -> StmResult<Self> {
20        let scalar = ScalarFieldElement::new_random_nonzero_scalar(rng)
21            .with_context(|| "Failed to generate a non zero Schnorr signing key.")?;
22        Ok(SchnorrSigningKey(scalar))
23    }
24
25    /// This function is an adapted version of the Schnorr signature scheme that includes
26    /// the computation of a deterministic value (called commitment_point) based on the message and the signing key
27    /// and works with the Jubjub elliptic curve and the Poseidon hash function.
28    ///
29    /// Input:
30    ///     - a message: some bytes
31    ///     - a signing key: an element of the scalar field of the Jubjub curve
32    /// Output:
33    ///     - a unique signature of the form (commitment_point, response, challenge):
34    ///         - commitment_point is deterministic depending only on the message and signing key
35    ///         - the response and challenge depends on a random value generated during the signature
36    ///
37    /// The protocol computes:
38    ///     - commitment_point = signing_key * H(Sha256(msg))
39    ///     - random_scalar, a random value
40    ///     - random_point_1 = random_scalar * H(Sha256(msg))
41    ///     - random_point_2 = random_scalar * prime_order_generator_point, where generator is a generator of the prime-order subgroup of Jubjub
42    ///     - challenge = Poseidon(DST || H(Sha256(msg)) || verification_key || commitment_point || random_point_1 || random_point_2)
43    ///     - response = random_scalar - challenge * signing_key
44    ///
45    /// Output the signature (`commitment_point`, `response`, `challenge`)
46    ///
47    pub fn sign<R: RngCore + CryptoRng>(
48        &self,
49        msg: &[u8],
50        rng: &mut R,
51    ) -> StmResult<UniqueSchnorrSignature> {
52        // Use the subgroup generator to compute the curve points
53        let prime_order_generator_point = PrimeOrderProjectivePoint::create_generator();
54        let verification_key = SchnorrVerificationKey::new_from_signing_key(self.clone())
55            .with_context(|| "Could not generate verification key from signing key.")?;
56
57        // First hashing the message to a scalar then hashing it to a curve point
58        let msg_hash_point = ProjectivePoint::hash_to_projective_point(msg)?;
59
60        let commitment_point = self.0 * msg_hash_point;
61
62        let random_scalar = ScalarFieldElement::new_random_nonzero_scalar(rng)
63            .with_context(|| "Random scalar generation failed during signing.")?;
64
65        let random_point_1 = random_scalar * msg_hash_point;
66        let random_point_2 = random_scalar * prime_order_generator_point;
67
68        // Since the hash function takes as input scalar elements
69        // We need to convert the EC points to their coordinates
70        // The order must be preserved
71        let points_coordinates: Vec<BaseFieldElement> = [
72            msg_hash_point,
73            ProjectivePoint::from(verification_key.0),
74            commitment_point,
75            random_point_1,
76            ProjectivePoint::from(random_point_2),
77        ]
78        .iter()
79        .flat_map(|point| {
80            let (u, v) = point.get_coordinates();
81            [u, v]
82        })
83        .collect();
84
85        let challenge = compute_poseidon_digest(&points_coordinates);
86        let challenge_times_sk = ScalarFieldElement::from_base_field(&challenge)? * self.0;
87        let response = random_scalar - challenge_times_sk;
88
89        Ok(UniqueSchnorrSignature {
90            commitment_point,
91            response,
92            challenge,
93        })
94    }
95
96    /// Convert a `SchnorrSigningKey` into bytes.
97    pub fn to_bytes(&self) -> [u8; 32] {
98        self.0.to_bytes()
99    }
100
101    /// Convert bytes into a `SchnorrSigningKey`.
102    ///
103    /// The bytes must represent a Jubjub scalar or the conversion will fail
104    pub fn from_bytes(bytes: &[u8]) -> StmResult<Self> {
105        if bytes.len() < 32 {
106            return Err(anyhow!(UniqueSchnorrSignatureError::Serialization)).with_context(
107                || "Not enough bytes provided to re-construct a Schnorr signing key.",
108            );
109        }
110        let scalar_field_element = ScalarFieldElement::from_bytes(bytes)
111            .with_context(|| "Could not construct Schnorr signing key from given bytes.")?;
112        Ok(SchnorrSigningKey(scalar_field_element))
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use rand_chacha::ChaCha20Rng;
119    use rand_core::SeedableRng;
120
121    use crate::signature_scheme::SchnorrSigningKey;
122
123    #[test]
124    fn generate_signing_key() {
125        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
126        let sk = SchnorrSigningKey::generate(&mut rng);
127
128        assert!(sk.is_ok(), "Signing key generation should succeed");
129    }
130
131    #[test]
132    fn generate_different_keys() {
133        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
134        let sk1 = SchnorrSigningKey::generate(&mut rng).unwrap();
135        let sk2 = SchnorrSigningKey::generate(&mut rng).unwrap();
136
137        // Keys should be different
138        assert_ne!(sk1, sk2, "Different keys should be generated");
139    }
140
141    #[test]
142    fn from_bytes_not_enough_bytes() {
143        let bytes = vec![0u8; 31];
144        let result = SchnorrSigningKey::from_bytes(&bytes);
145
146        result.expect_err("Should fail with insufficient bytes");
147    }
148
149    #[test]
150    fn from_bytes_exact_size() {
151        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
152        let sk = SchnorrSigningKey::generate(&mut rng).unwrap();
153        let sk_bytes = sk.to_bytes();
154
155        let sk_restored = SchnorrSigningKey::from_bytes(&sk_bytes).unwrap();
156
157        assert_eq!(sk, sk_restored);
158    }
159
160    #[test]
161    fn from_bytes_extra_bytes() {
162        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
163        let sk = SchnorrSigningKey::generate(&mut rng).unwrap();
164        let sk_bytes = sk.to_bytes();
165
166        let mut extended_bytes = sk_bytes.to_vec();
167        extended_bytes.extend_from_slice(&[0xFF; 10]);
168
169        let sk_restored = SchnorrSigningKey::from_bytes(&extended_bytes).unwrap();
170
171        assert_eq!(sk, sk_restored);
172    }
173
174    #[test]
175    fn to_bytes_is_deterministic() {
176        let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
177        let sk = SchnorrSigningKey::generate(&mut rng).unwrap();
178
179        let bytes1 = sk.to_bytes();
180        let bytes2 = sk.to_bytes();
181
182        assert_eq!(bytes1, bytes2, "to_bytes should be deterministic");
183    }
184
185    mod golden {
186        use super::*;
187
188        const GOLDEN_BYTES: &[u8; 32] = &[
189            126, 191, 239, 197, 88, 151, 248, 254, 187, 143, 86, 35, 29, 62, 90, 13, 196, 71, 234,
190            5, 90, 124, 205, 194, 51, 192, 228, 133, 25, 140, 157, 7,
191        ];
192
193        fn golden_value() -> SchnorrSigningKey {
194            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
195            SchnorrSigningKey::generate(&mut rng).unwrap()
196        }
197
198        #[test]
199        fn golden_conversions() {
200            let value = SchnorrSigningKey::from_bytes(GOLDEN_BYTES)
201                .expect("This from bytes should not fail");
202            assert_eq!(golden_value().0, value.0);
203
204            let serialized = SchnorrSigningKey::to_bytes(&value);
205            let golden_serialized = SchnorrSigningKey::to_bytes(&golden_value());
206            assert_eq!(golden_serialized, serialized);
207        }
208    }
209
210    mod golden_json {
211        use super::*;
212
213        const GOLDEN_JSON: &str = r#"[126, 191, 239, 197, 88, 151, 248, 254, 187, 143, 86, 35, 29, 62, 90, 13, 196, 71, 234, 5, 90, 124, 205, 194, 51, 192, 228, 133, 25, 140, 157, 7]"#;
214
215        fn golden_value() -> SchnorrSigningKey {
216            let mut rng = ChaCha20Rng::from_seed([0u8; 32]);
217            SchnorrSigningKey::generate(&mut rng).unwrap()
218        }
219
220        #[test]
221        fn golden_conversions() {
222            let value = serde_json::from_str(GOLDEN_JSON)
223                .expect("This JSON deserialization should not fail");
224            assert_eq!(golden_value(), value);
225
226            let serialized =
227                serde_json::to_string(&value).expect("This JSON serialization should not fail");
228            let golden_serialized = serde_json::to_string(&golden_value())
229                .expect("This JSON serialization should not fail");
230            assert_eq!(golden_serialized, serialized);
231        }
232    }
233}