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