miden_crypto/dsa/falcon512_rpo/signature.rs
1use alloc::{string::ToString, vec::Vec};
2use core::ops::Deref;
3
4use num::Zero;
5
6use super::{
7 ByteReader, ByteWriter, Deserializable, DeserializationError, LOG_N, MODULUS, N, Nonce,
8 SIG_L2_BOUND, SIG_POLY_BYTE_LEN, Serializable,
9 hash_to_point::hash_to_point_rpo256,
10 keys::PublicKey,
11 math::{FalconFelt, FastFft, Polynomial},
12};
13use crate::Word;
14
15// FALCON SIGNATURE
16// ================================================================================================
17
18/// A deterministic RPO Falcon512 signature over a message.
19///
20/// The signature is a pair of polynomials (s1, s2) in (Z_p\[x\]/(phi))^2 a nonce `r`, and a public
21/// key polynomial `h` where:
22/// - p := 12289
23/// - phi := x^512 + 1
24///
25/// The signature verifies against a public key `pk` if and only if:
26/// 1. s1 = c - s2 * h
27/// 2. |s1|^2 + |s2|^2 <= SIG_L2_BOUND
28///
29/// where |.| is the norm and:
30/// - c = HashToPoint(r || message)
31/// - pk = Rpo256::hash(h)
32///
33/// Here h is a polynomial representing the public key and pk is its digest using the Rpo256 hash
34/// function. c is a polynomial that is the hash-to-point of the message being signed.
35///
36/// To summarize the main points of differences with the reference implementation, we have that:
37///
38/// 1. the hash-to-point algorithm is made deterministic by using a fixed nonce `r`. This fixed
39/// nonce is formed as `nonce_version_byte || preversioned_nonce` where `preversioned_nonce` is a
40/// 39-byte string that is defined as: i. a byte representing `log_2(512)`, followed by ii. the
41/// UTF8 representation of the string "RPO-FALCON-DET", followed by iii. the required number of
42/// 0_u8 padding to make the total length equal 39 bytes. Note that the above means in particular
43/// that only the `nonce_version_byte` needs to be serialized when serializing the signature.
44/// This reduces the deterministic signature compared to the reference implementation by 39
45/// bytes.
46/// 2. the RNG used in the trapdoor sampler (i.e., the ffSampling algorithm) is ChaCha20Rng seeded
47/// with the `Blake3` hash of `log_2(512) || sk || message`.
48///
49/// The signature is serialized as:
50///
51/// 1. A header byte specifying the algorithm used to encode the coefficients of the `s2` polynomial
52/// together with the degree of the irreducible polynomial phi. For RPO Falcon512, the header
53/// byte is set to `10111001` to differentiate it from the standardized instantiation of the
54/// Falcon signature.
55/// 2. 1 byte for the nonce version.
56/// 4. 625 bytes encoding the `s2` polynomial above.
57///
58/// In addition to the signature itself, the polynomial h is also serialized with the signature as:
59///
60/// 1. 1 byte representing the log2(512) i.e., 9.
61/// 2. 896 bytes for the public key itself.
62///
63/// The total size of the signature (including the extended public key) is 1524 bytes.
64///
65/// [1]: https://github.com/algorand/falcon/blob/main/falcon-det.pdf
66/// [2]: https://datatracker.ietf.org/doc/html/rfc6979#section-3.5
67#[derive(Debug, Clone, PartialEq, Eq)]
68pub struct Signature {
69 header: SignatureHeader,
70 nonce: Nonce,
71 s2: SignaturePoly,
72 h: PublicKey,
73}
74
75impl Signature {
76 // CONSTRUCTOR
77 // --------------------------------------------------------------------------------------------
78
79 /// Creates a new signature from the given nonce, public key polynomial, and signature
80 /// polynomial.
81 pub fn new(nonce: Nonce, h: PublicKey, s2: SignaturePoly) -> Signature {
82 Self {
83 header: SignatureHeader::default(),
84 nonce,
85 s2,
86 h,
87 }
88 }
89
90 // PUBLIC ACCESSORS
91 // --------------------------------------------------------------------------------------------
92
93 /// Returns the public key polynomial h.
94 pub fn public_key(&self) -> &PublicKey {
95 &self.h
96 }
97
98 /// Returns the polynomial representation of the signature in Z_p\[x\]/(phi).
99 pub fn sig_poly(&self) -> &Polynomial<FalconFelt> {
100 &self.s2
101 }
102
103 /// Returns the nonce component of the signature.
104 pub fn nonce(&self) -> &Nonce {
105 &self.nonce
106 }
107
108 // SIGNATURE VERIFICATION
109 // --------------------------------------------------------------------------------------------
110
111 /// Returns true if this signature is a valid signature for the specified message generated
112 /// against the secret key matching the specified public key commitment.
113 pub fn verify(&self, message: Word, pub_key: &PublicKey) -> bool {
114 if self.h != *pub_key {
115 return false;
116 }
117 let c = hash_to_point_rpo256(message, &self.nonce);
118 verify_helper(&c, &self.s2, pub_key)
119 }
120}
121
122impl Serializable for Signature {
123 fn write_into<W: ByteWriter>(&self, target: &mut W) {
124 target.write(&self.header);
125 target.write(&self.nonce);
126 target.write(&self.s2);
127 target.write(&self.h);
128 }
129}
130
131impl Deserializable for Signature {
132 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
133 let header = source.read()?;
134 let nonce = source.read()?;
135 let s2 = source.read()?;
136 let h = source.read()?;
137
138 Ok(Self { header, nonce, s2, h })
139 }
140}
141
142// SIGNATURE HEADER
143// ================================================================================================
144
145/// The header byte used to encode the signature metadata.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct SignatureHeader(u8);
148
149impl Default for SignatureHeader {
150 /// According to section 3.11.3 in the specification [1], the signature header has the format
151 /// `0cc1nnnn` where:
152 ///
153 /// 1. `cc` signifies the encoding method. `01` denotes using the compression encoding method
154 /// and `10` denotes encoding using the uncompressed method.
155 /// 2. `nnnn` encodes `LOG_N`.
156 ///
157 /// For RPO Falcon 512 we use compression encoding and N = 512. Moreover, to differentiate the
158 /// RPO Falcon variant from the reference variant using SHAKE256, we flip the first bit in the
159 /// header. Thus, for RPO Falcon 512 the header is `10111001`
160 ///
161 /// [1]: https://falcon-sign.info/falcon.pdf
162 fn default() -> Self {
163 Self(0b1011_1001)
164 }
165}
166
167impl Serializable for &SignatureHeader {
168 fn write_into<W: ByteWriter>(&self, target: &mut W) {
169 target.write_u8(self.0)
170 }
171}
172
173impl Deserializable for SignatureHeader {
174 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
175 let header = source.read_u8()?;
176 let (encoding, log_n) = (header >> 4, header & 0b00001111);
177 if encoding != 0b1011 {
178 return Err(DeserializationError::InvalidValue(
179 "Failed to decode signature: not supported encoding algorithm".to_string(),
180 ));
181 }
182
183 if log_n != LOG_N {
184 return Err(DeserializationError::InvalidValue(format!(
185 "Failed to decode signature: only supported irreducible polynomial degree is 512, 2^{log_n} was provided"
186 )));
187 }
188
189 Ok(Self(header))
190 }
191}
192
193// SIGNATURE POLYNOMIAL
194// ================================================================================================
195
196/// A polynomial used as the `s2` component of the signature.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct SignaturePoly(pub Polynomial<FalconFelt>);
199
200impl Deref for SignaturePoly {
201 type Target = Polynomial<FalconFelt>;
202
203 fn deref(&self) -> &Self::Target {
204 &self.0
205 }
206}
207
208impl From<Polynomial<FalconFelt>> for SignaturePoly {
209 fn from(pk_poly: Polynomial<FalconFelt>) -> Self {
210 Self(pk_poly)
211 }
212}
213
214impl TryFrom<&[i16; N]> for SignaturePoly {
215 type Error = ();
216
217 fn try_from(coefficients: &[i16; N]) -> Result<Self, Self::Error> {
218 if are_coefficients_valid(coefficients) {
219 Ok(Self(coefficients.to_vec().into()))
220 } else {
221 Err(())
222 }
223 }
224}
225
226impl Serializable for &SignaturePoly {
227 fn write_into<W: ByteWriter>(&self, target: &mut W) {
228 let sig_coeff: Vec<i16> = self.0.coefficients.iter().map(|a| a.balanced_value()).collect();
229 let mut sk_bytes = vec![0_u8; SIG_POLY_BYTE_LEN];
230
231 let mut acc = 0;
232 let mut acc_len = 0;
233 let mut v = 0;
234 let mut t;
235 let mut w;
236
237 // For each coefficient of x:
238 // - the sign is encoded on 1 bit
239 // - the 7 lower bits are encoded naively (binary)
240 // - the high bits are encoded in unary encoding
241 //
242 // Algorithm 17 p. 47 of the specification [1].
243 //
244 // [1]: https://falcon-sign.info/falcon.pdf
245 for &c in sig_coeff.iter() {
246 acc <<= 1;
247 t = c;
248
249 if t < 0 {
250 t = -t;
251 acc |= 1;
252 }
253 w = t as u16;
254
255 acc <<= 7;
256 let mask = 127_u32;
257 acc |= (w as u32) & mask;
258 w >>= 7;
259
260 acc_len += 8;
261
262 acc <<= w + 1;
263 acc |= 1;
264 acc_len += w + 1;
265
266 while acc_len >= 8 {
267 acc_len -= 8;
268
269 sk_bytes[v] = (acc >> acc_len) as u8;
270 v += 1;
271 }
272 }
273
274 if acc_len > 0 {
275 sk_bytes[v] = (acc << (8 - acc_len)) as u8;
276 }
277 target.write_bytes(&sk_bytes);
278 }
279}
280
281impl Deserializable for SignaturePoly {
282 fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
283 let input = source.read_array::<SIG_POLY_BYTE_LEN>()?;
284
285 let mut input_idx = 0;
286 let mut acc = 0u32;
287 let mut acc_len = 0;
288 let mut coefficients = [FalconFelt::zero(); N];
289
290 // Algorithm 18 p. 48 of the specification [1].
291 //
292 // [1]: https://falcon-sign.info/falcon.pdf
293 for c in coefficients.iter_mut() {
294 acc = (acc << 8) | (input[input_idx] as u32);
295 input_idx += 1;
296 let b = acc >> acc_len;
297 let s = b & 128;
298 let mut m = b & 127;
299
300 loop {
301 if acc_len == 0 {
302 acc = (acc << 8) | (input[input_idx] as u32);
303 input_idx += 1;
304 acc_len = 8;
305 }
306 acc_len -= 1;
307 if ((acc >> acc_len) & 1) != 0 {
308 break;
309 }
310 m += 128;
311 if m >= 2048 {
312 return Err(DeserializationError::InvalidValue(format!(
313 "Failed to decode signature: high bits {m} exceed 2048",
314 )));
315 }
316 }
317 if s != 0 && m == 0 {
318 return Err(DeserializationError::InvalidValue(
319 "Failed to decode signature: -0 is forbidden".to_string(),
320 ));
321 }
322
323 let felt = if s != 0 { (MODULUS as u32 - m) as u16 } else { m as u16 };
324 *c = FalconFelt::new(felt as i16);
325 }
326
327 if (acc & ((1 << acc_len) - 1)) != 0 {
328 return Err(DeserializationError::InvalidValue(
329 "Failed to decode signature: Non-zero unused bits in the last byte".to_string(),
330 ));
331 }
332 Ok(Polynomial::new(coefficients.to_vec()).into())
333 }
334}
335
336// HELPER FUNCTIONS
337// ================================================================================================
338
339/// Takes the hash-to-point polynomial `c` of a message, the signature polynomial over
340/// the message `s2` and a public key polynomial and returns `true` is the signature is a valid
341/// signature for the given parameters, otherwise it returns `false`.
342fn verify_helper(c: &Polynomial<FalconFelt>, s2: &SignaturePoly, h: &PublicKey) -> bool {
343 let h_fft = h.fft();
344 let s2_fft = s2.fft();
345 let c_fft = c.fft();
346
347 // compute the signature polynomial s1 using s1 = c - s2 * h
348 let s1_fft = c_fft - s2_fft.hadamard_mul(&h_fft);
349 let s1 = s1_fft.ifft();
350
351 // compute the norm squared of (s1, s2)
352 let length_squared_s1 = s1.norm_squared();
353 let length_squared_s2 = s2.norm_squared();
354 let length_squared = length_squared_s1 + length_squared_s2;
355
356 length_squared < SIG_L2_BOUND
357}
358
359/// Checks whether a set of coefficients is a valid one for a signature polynomial.
360fn are_coefficients_valid(x: &[i16]) -> bool {
361 if x.len() != N {
362 return false;
363 }
364
365 for &c in x {
366 if !(-2047..=2047).contains(&c) {
367 return false;
368 }
369 }
370
371 true
372}
373
374// TESTS
375// ================================================================================================
376
377#[cfg(test)]
378mod tests {
379 use rand::SeedableRng;
380 use rand_chacha::ChaCha20Rng;
381
382 use super::{
383 super::{SIG_SERIALIZED_LEN, SecretKey},
384 *,
385 };
386
387 #[test]
388 fn test_serialization_round_trip() {
389 let seed = [0_u8; 32];
390 let mut rng = ChaCha20Rng::from_seed(seed);
391
392 let sk = SecretKey::with_rng(&mut rng);
393 let signature = sk.sign_with_rng(Word::default(), &mut rng);
394 let serialized = signature.to_bytes();
395 assert_eq!(serialized.len(), SIG_SERIALIZED_LEN);
396 let deserialized = Signature::read_from_bytes(&serialized).unwrap();
397 assert_eq!(signature.sig_poly(), deserialized.sig_poly());
398 }
399}