Tracking PR for v0.9.0 release (#278)

* chore: update crate version to v0.9.0
* chore: remove deprecated re-exports
* chore: remove Box re-export
* feat: implement pure-Rust keygen and signing for RpoFalcon512 (#285)
* feat: add reproducible builds (#296)
* fix: address a few issues for migrating Miden VM  (#298)
* feat: add RngCore supertrait for FeltRng (#299)

---------

Co-authored-by: Al-Kindi-0 <82364884+Al-Kindi-0@users.noreply.github.com>
Co-authored-by: Paul-Henry Kajfasz <42912740+phklive@users.noreply.github.com>
This commit is contained in:
Bobbin Threadbare
2024-03-24 08:38:08 -07:00
committed by GitHub
parent 2be17b74fb
commit 5a2e917dd5
39 changed files with 5964 additions and 2334 deletions

View File

@@ -1,289 +1,373 @@
use alloc::string::ToString;
use core::cell::OnceCell;
use alloc::{string::ToString, vec::Vec};
use core::ops::Deref;
use super::{
ByteReader, ByteWriter, Deserializable, DeserializationError, Felt, NonceBytes, NonceElements,
Polynomial, PublicKeyBytes, Rpo256, Serializable, SignatureBytes, Word, MODULUS, N,
SIG_L2_BOUND, ZERO,
hash_to_point::hash_to_point_rpo256,
keys::PubKeyPoly,
math::{FalconFelt, FastFft, Polynomial},
ByteReader, ByteWriter, Deserializable, DeserializationError, Felt, Nonce, Rpo256,
Serializable, Word, LOG_N, MODULUS, N, SIG_L2_BOUND, SIG_POLY_BYTE_LEN,
};
use num::Zero;
// FALCON SIGNATURE
// ================================================================================================
/// An RPO Falcon512 signature over a message.
///
/// The signature is a pair of polynomials (s1, s2) in (Z_p\[x\]/(phi))^2, where:
/// The signature is a pair of polynomials (s1, s2) in (Z_p\[x\]/(phi))^2 a nonce `r`, and a public
/// key polynomial `h` where:
/// - p := 12289
/// - phi := x^512 + 1
/// - s1 = c - s2 * h
/// - h is a polynomial representing the public key and c is a polynomial that is the hash-to-point
/// of the message being signed.
///
/// The signature verifies if and only if:
/// The signature verifies against a public key `pk` if and only if:
/// 1. s1 = c - s2 * h
/// 2. |s1|^2 + |s2|^2 <= SIG_L2_BOUND
///
/// where |.| is the norm.
/// where |.| is the norm and:
/// - c = HashToPoint(r || message)
/// - pk = Rpo256::hash(h)
///
/// [Signature] also includes the extended public key which is serialized as:
/// Here h is a polynomial representing the public key and pk is its digest using the Rpo256 hash
/// function. c is a polynomial that is the hash-to-point of the message being signed.
///
/// The polynomial h is serialized as:
/// 1. 1 byte representing the log2(512) i.e., 9.
/// 2. 896 bytes for the public key. This is decoded into the `h` polynomial above.
/// 2. 896 bytes for the public key itself.
///
/// The actual signature is serialized as:
/// The signature is serialized as:
/// 1. A header byte specifying the algorithm used to encode the coefficients of the `s2` polynomial
/// together with the degree of the irreducible polynomial phi.
/// The general format of this byte is 0b0cc1nnnn where:
/// a. cc is either 01 when the compressed encoding algorithm is used and 10 when the
/// uncompressed algorithm is used.
/// b. nnnn is log2(N) where N is the degree of the irreducible polynomial phi.
/// The current implementation works always with cc equal to 0b01 and nnnn equal to 0b1001 and
/// thus the header byte is always equal to 0b00111001.
/// together with the degree of the irreducible polynomial phi. For RPO Falcon512, the header
/// byte is set to `10111001` which differentiates it from the standardized instantiation of
/// the Falcon signature.
/// 2. 40 bytes for the nonce.
/// 3. 625 bytes encoding the `s2` polynomial above.
/// 4. 625 bytes encoding the `s2` polynomial above.
///
/// The total size of the signature (including the extended public key) is 1563 bytes.
#[derive(Debug, Clone)]
/// The total size of the signature is (including the extended public key) is 1563 bytes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Signature {
pub(super) pk: PublicKeyBytes,
pub(super) sig: SignatureBytes,
// Cached polynomial decoding for public key and signatures
pub(super) pk_polynomial: OnceCell<Polynomial>,
pub(super) sig_polynomial: OnceCell<Polynomial>,
header: SignatureHeader,
nonce: Nonce,
s2: SignaturePoly,
h: PubKeyPoly,
}
impl Signature {
// CONSTRUCTOR
// --------------------------------------------------------------------------------------------
pub fn new(nonce: Nonce, h: PubKeyPoly, s2: SignaturePoly) -> Signature {
Self {
header: SignatureHeader::default(),
nonce,
s2,
h,
}
}
// PUBLIC ACCESSORS
// --------------------------------------------------------------------------------------------
/// Returns the public key polynomial h.
pub fn pub_key_poly(&self) -> Polynomial {
*self.pk_polynomial.get_or_init(|| {
// we assume that the signature was constructed with a valid public key, and thus
// expect() is OK here.
Polynomial::from_pub_key(&self.pk).expect("invalid public key")
})
}
/// Returns the nonce component of the signature represented as field elements.
///
/// Nonce bytes are converted to field elements by taking consecutive 5 byte chunks
/// of the nonce and interpreting them as field elements.
pub fn nonce(&self) -> NonceElements {
// we assume that the signature was constructed with a valid signature, and thus
// expect() is OK here.
let nonce = self.sig[1..41].try_into().expect("invalid signature");
decode_nonce(nonce)
pub fn pk_poly(&self) -> &PubKeyPoly {
&self.h
}
// Returns the polynomial representation of the signature in Z_p[x]/(phi).
pub fn sig_poly(&self) -> Polynomial {
*self.sig_polynomial.get_or_init(|| {
// we assume that the signature was constructed with a valid signature, and thus
// expect() is OK here.
Polynomial::from_signature(&self.sig).expect("invalid signature")
})
pub fn sig_poly(&self) -> &Polynomial<FalconFelt> {
&self.s2
}
// HASH-TO-POINT
// --------------------------------------------------------------------------------------------
/// Returns a polynomial in Z_p\[x\]/(phi) representing the hash of the provided message.
pub fn hash_to_point(&self, message: Word) -> Polynomial {
hash_to_point(message, &self.nonce())
/// Returns the nonce component of the signature.
pub fn nonce(&self) -> &Nonce {
&self.nonce
}
// SIGNATURE VERIFICATION
// --------------------------------------------------------------------------------------------
/// Returns true if this signature is a valid signature for the specified message generated
/// against key pair matching the specified public key commitment.
/// against the secret key matching the specified public key commitment.
pub fn verify(&self, message: Word, pubkey_com: Word) -> bool {
// Make sure the expanded public key matches the provided public key commitment
let h = self.pub_key_poly();
let h_digest: Word = Rpo256::hash_elements(&h.to_elements()).into();
// compute the hash of the public key polynomial
let h_felt: Polynomial<Felt> = (&**self.pk_poly()).into();
let h_digest: Word = Rpo256::hash_elements(&h_felt.coefficients).into();
if h_digest != pubkey_com {
return false;
}
// Make sure the signature is valid
let s2 = self.sig_poly();
let c = self.hash_to_point(message);
let s1 = c - s2 * h;
let sq_norm = s1.sq_norm() + s2.sq_norm();
sq_norm <= SIG_L2_BOUND
let c = hash_to_point_rpo256(message, &self.nonce);
h_digest == pubkey_com && verify_helper(&c, &self.s2, self.pk_poly())
}
}
// SERIALIZATION / DESERIALIZATION
// ================================================================================================
impl Serializable for Signature {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_bytes(&self.pk);
target.write_bytes(&self.sig);
target.write(&self.header);
target.write(&self.nonce);
target.write(&self.s2);
target.write(&self.h);
}
}
impl Deserializable for Signature {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let pk: PublicKeyBytes = source.read_array()?;
let sig: SignatureBytes = source.read_array()?;
let header = source.read()?;
let nonce = source.read()?;
let s2 = source.read()?;
let h = source.read()?;
// make sure public key and signature can be decoded correctly
let pk_polynomial = Polynomial::from_pub_key(&pk)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
.into();
let sig_polynomial = Polynomial::from_signature(&sig)
.map_err(|err| DeserializationError::InvalidValue(err.to_string()))?
.into();
Ok(Self { header, nonce, s2, h })
}
}
Ok(Self { pk, sig, pk_polynomial, sig_polynomial })
// SIGNATURE HEADER
// ================================================================================================
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignatureHeader(u8);
impl Default for SignatureHeader {
/// According to section 3.11.3 in the specification [1], the signature header has the format
/// `0cc1nnnn` where:
///
/// 1. `cc` signifies the encoding method. `01` denotes using the compression encoding method
/// and `10` denotes encoding using the uncompressed method.
/// 2. `nnnn` encodes `LOG_N`.
///
/// For RPO Falcon 512 we use compression encoding and N = 512. Moreover, to differentiate the
/// RPO Falcon variant from the reference variant using SHAKE256, we flip the first bit in the
/// header. Thus, for RPO Falcon 512 the header is `10111001`
///
/// [1]: https://falcon-sign.info/falcon.pdf
fn default() -> Self {
Self(0b1011_1001)
}
}
impl Serializable for &SignatureHeader {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
target.write_u8(self.0)
}
}
impl Deserializable for SignatureHeader {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let header = source.read_u8()?;
let (encoding, log_n) = (header >> 4, header & 0b00001111);
if encoding != 0b1011 {
return Err(DeserializationError::InvalidValue(
"Failed to decode signature: not supported encoding algorithm".to_string(),
));
}
if log_n != LOG_N {
return Err(DeserializationError::InvalidValue(
format!("Failed to decode signature: only supported irreducible polynomial degree is 512, 2^{log_n} was provided")
));
}
Ok(Self(header))
}
}
// SIGNATURE POLYNOMIAL
// ================================================================================================
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignaturePoly(pub Polynomial<FalconFelt>);
impl Deref for SignaturePoly {
type Target = Polynomial<FalconFelt>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Polynomial<FalconFelt>> for SignaturePoly {
fn from(pk_poly: Polynomial<FalconFelt>) -> Self {
Self(pk_poly)
}
}
impl TryFrom<&[i16; N]> for SignaturePoly {
type Error = ();
fn try_from(coefficients: &[i16; N]) -> Result<Self, Self::Error> {
if are_coefficients_valid(coefficients) {
Ok(Self(coefficients.to_vec().into()))
} else {
Err(())
}
}
}
impl Serializable for &SignaturePoly {
fn write_into<W: ByteWriter>(&self, target: &mut W) {
let sig_coeff: Vec<i16> = self.0.coefficients.iter().map(|a| a.balanced_value()).collect();
let mut sk_bytes = vec![0_u8; SIG_POLY_BYTE_LEN];
let mut acc = 0;
let mut acc_len = 0;
let mut v = 0;
let mut t;
let mut w;
// For each coefficient of x:
// - the sign is encoded on 1 bit
// - the 7 lower bits are encoded naively (binary)
// - the high bits are encoded in unary encoding
//
// Algorithm 17 p. 47 of the specification [1].
//
// [1]: https://falcon-sign.info/falcon.pdf
for &c in sig_coeff.iter() {
acc <<= 1;
t = c;
if t < 0 {
t = -t;
acc |= 1;
}
w = t as u16;
acc <<= 7;
let mask = 127_u32;
acc |= (w as u32) & mask;
w >>= 7;
acc_len += 8;
acc <<= w + 1;
acc |= 1;
acc_len += w + 1;
while acc_len >= 8 {
acc_len -= 8;
sk_bytes[v] = (acc >> acc_len) as u8;
v += 1;
}
}
if acc_len > 0 {
sk_bytes[v] = (acc << (8 - acc_len)) as u8;
}
target.write_bytes(&sk_bytes);
}
}
impl Deserializable for SignaturePoly {
fn read_from<R: ByteReader>(source: &mut R) -> Result<Self, DeserializationError> {
let input = source.read_array::<SIG_POLY_BYTE_LEN>()?;
let mut input_idx = 0;
let mut acc = 0u32;
let mut acc_len = 0;
let mut coefficients = [FalconFelt::zero(); N];
// Algorithm 18 p. 48 of the specification [1].
//
// [1]: https://falcon-sign.info/falcon.pdf
for c in coefficients.iter_mut() {
acc = (acc << 8) | (input[input_idx] as u32);
input_idx += 1;
let b = acc >> acc_len;
let s = b & 128;
let mut m = b & 127;
loop {
if acc_len == 0 {
acc = (acc << 8) | (input[input_idx] as u32);
input_idx += 1;
acc_len = 8;
}
acc_len -= 1;
if ((acc >> acc_len) & 1) != 0 {
break;
}
m += 128;
if m >= 2048 {
return Err(DeserializationError::InvalidValue(
"Failed to decode signature: high bits {m} exceed 2048".to_string(),
));
}
}
if s != 0 && m == 0 {
return Err(DeserializationError::InvalidValue(
"Failed to decode signature: -0 is forbidden".to_string(),
));
}
let felt = if s != 0 { (MODULUS as u32 - m) as u16 } else { m as u16 };
*c = FalconFelt::new(felt as i16);
}
if (acc & ((1 << acc_len) - 1)) != 0 {
return Err(DeserializationError::InvalidValue(
"Failed to decode signature: Non-zero unused bits in the last byte".to_string(),
));
}
Ok(Polynomial::new(coefficients.to_vec()).into())
}
}
// HELPER FUNCTIONS
// ================================================================================================
/// Returns a polynomial in Z_p[x]/(phi) representing the hash of the provided message and
/// nonce.
fn hash_to_point(message: Word, nonce: &NonceElements) -> Polynomial {
let mut state = [ZERO; Rpo256::STATE_WIDTH];
/// Takes the hash-to-point polynomial `c` of a message, the signature polynomial over
/// the message `s2` and a public key polynomial and returns `true` is the signature is a valid
/// signature for the given parameters, otherwise it returns `false`.
fn verify_helper(c: &Polynomial<FalconFelt>, s2: &SignaturePoly, h: &PubKeyPoly) -> bool {
let h_fft = h.fft();
let s2_fft = s2.fft();
let c_fft = c.fft();
// absorb the nonce into the state
for (&n, s) in nonce.iter().zip(state[Rpo256::RATE_RANGE].iter_mut()) {
*s = n;
}
Rpo256::apply_permutation(&mut state);
// compute the signature polynomial s1 using s1 = c - s2 * h
let s1_fft = c_fft - s2_fft.hadamard_mul(&h_fft);
let s1 = s1_fft.ifft();
// absorb message into the state
for (&m, s) in message.iter().zip(state[Rpo256::RATE_RANGE].iter_mut()) {
*s = m;
// compute the norm squared of (s1, s2)
let length_squared_s1 = s1.norm_squared();
let length_squared_s2 = s2.norm_squared();
let length_squared = length_squared_s1 + length_squared_s2;
length_squared < SIG_L2_BOUND
}
/// Checks whether a set of coefficients is a valid one for a signature polynomial.
fn are_coefficients_valid(x: &[i16]) -> bool {
if x.len() != N {
return false;
}
// squeeze the coefficients of the polynomial
let mut i = 0;
let mut res = [0_u16; N];
for _ in 0..64 {
Rpo256::apply_permutation(&mut state);
for a in &state[Rpo256::RATE_RANGE] {
res[i] = (a.as_int() % MODULUS as u64) as u16;
i += 1;
for &c in x {
if !(-2047..=2047).contains(&c) {
return false;
}
}
// using the raw constructor is OK here because we reduce all coefficients by the modulus above
unsafe { Polynomial::new(res) }
}
/// Converts byte representation of the nonce into field element representation.
fn decode_nonce(nonce: &NonceBytes) -> NonceElements {
let mut buffer = [0_u8; 8];
let mut result = [ZERO; 8];
for (i, bytes) in nonce.chunks(5).enumerate() {
buffer[..5].copy_from_slice(bytes);
// we can safely (without overflow) create a new Felt from u64 value here since this value
// contains at most 5 bytes
result[i] = Felt::new(u64::from_le_bytes(buffer));
}
result
true
}
// TESTS
// ================================================================================================
#[cfg(all(test, feature = "std"))]
#[cfg(test)]
mod tests {
use core::ffi::c_void;
use rand_utils::rand_vector;
use super::{
super::{ffi::*, KeyPair},
*,
};
// Wrappers for unsafe functions
impl Rpo128Context {
/// Initializes the RPO state.
pub fn init() -> Self {
let mut ctx = Rpo128Context { content: [0u64; 13] };
unsafe {
rpo128_init(&mut ctx as *mut Rpo128Context);
}
ctx
}
/// Absorbs data into the RPO state.
pub fn absorb(&mut self, data: &[u8]) {
unsafe {
rpo128_absorb(
self as *mut Rpo128Context,
data.as_ptr() as *const c_void,
data.len(),
)
}
}
/// Finalizes the RPO state to prepare for squeezing.
pub fn finalize(&mut self) {
unsafe { rpo128_finalize(self as *mut Rpo128Context) }
}
}
#[test]
fn test_hash_to_point() {
// Create a random message and transform it into a u8 vector
let msg_felts: Word = rand_vector::<Felt>(4).try_into().unwrap();
let msg_bytes = msg_felts
.iter()
.flat_map(|e| e.as_int().to_le_bytes())
.collect::<alloc::vec::Vec<_>>();
// Create a nonce i.e. a [u8; 40] array and pack into a [Felt; 8] array.
let nonce: [u8; 40] = rand_vector::<u8>(40).try_into().unwrap();
let mut buffer = [0_u8; 64];
for i in 0..8 {
buffer[8 * i] = nonce[5 * i];
buffer[8 * i + 1] = nonce[5 * i + 1];
buffer[8 * i + 2] = nonce[5 * i + 2];
buffer[8 * i + 3] = nonce[5 * i + 3];
buffer[8 * i + 4] = nonce[5 * i + 4];
}
// Initialize the RPO state
let mut rng = Rpo128Context::init();
// Absorb the nonce and message into the RPO state
rng.absorb(&buffer);
rng.absorb(&msg_bytes);
rng.finalize();
// Generate the coefficients of the hash-to-point polynomial.
let mut res: [u16; N] = [0; N];
unsafe {
PQCLEAN_FALCON512_CLEAN_hash_to_point_rpo(
&mut rng as *mut Rpo128Context,
res.as_mut_ptr(),
9,
);
}
// Check that the coefficients are correct
let nonce = decode_nonce(&nonce);
assert_eq!(res, hash_to_point(msg_felts, &nonce).inner());
}
use super::{super::SecretKey, *};
use rand::SeedableRng;
use rand_chacha::ChaCha20Rng;
#[test]
fn test_serialization_round_trip() {
let key = KeyPair::new().unwrap();
let signature = key.sign(Word::default()).unwrap();
let seed = [0_u8; 32];
let mut rng = ChaCha20Rng::from_seed(seed);
let sk = SecretKey::with_rng(&mut rng);
let signature = sk.sign_with_rng(Word::default(), &mut rng);
let serialized = signature.to_bytes();
let deserialized = Signature::read_from_bytes(&serialized).unwrap();
assert_eq!(signature.sig_poly(), deserialized.sig_poly());
assert_eq!(signature.pub_key_poly(), deserialized.pub_key_poly());
}
}