diff --git a/CHANGELOG.md b/CHANGELOG.md index 2957f2c9..762a5831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Fixed a bug in the implementation of `draw_integers` for `RpoRandomCoin` (#343). - [BREAKING] Refactor error messages and use `thiserror` to derive errors (#344). - [BREAKING] Updated Winterfell dependency to v0.11 (#346). +- Added RPO-STARK based DSA (#349). ## 0.12.0 (2024-10-30) diff --git a/Cargo.lock b/Cargo.lock index 00e6328d..ea60d062 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,9 +113,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "blake3" -version = "1.5.4" +version = "1.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +checksum = "b8ee0c1824c4dea5b5f81736aff91bae041d2c07ee1192bec91054e10e3e601e" dependencies = [ "arrayref", "arrayvec", @@ -153,9 +153,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.1" +version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd9de9f2205d5ef3fd67e685b0df337994ddd4495e2a28d185500d0e1edfea47" +checksum = "27f657647bcff5394bf56c7317665bbf790a137a50eaaa5c6bfbb9e27a518f2d" dependencies = [ "jobserver", "libc", @@ -197,9 +197,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f" +checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" dependencies = [ "clap_builder", "clap_derive", @@ -207,9 +207,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.21" +version = "4.5.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec" +checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" dependencies = [ "anstream", "anstyle", @@ -231,9 +231,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" @@ -351,19 +351,19 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] name = "fastrand" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "486f806e73c5707928240ddc295403b1b93c96a02038563881c4a2fd84b81ac4" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "fnv" @@ -456,9 +456,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "540654e97a3f4470a492cd30ff187bc95d89557a903a2bbf112e2fae98104ef2" +checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "jobserver" @@ -471,10 +471,11 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7" dependencies = [ + "once_cell", "wasm-bindgen", ] @@ -495,9 +496,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.164" +version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f" +checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" [[package]] name = "libm" @@ -546,10 +547,13 @@ dependencies = [ "serde", "sha3", "thiserror", + "winter-air", "winter-crypto", "winter-math", + "winter-prover", "winter-rand-utils", "winter-utils", + "winter-verifier", ] [[package]] @@ -638,6 +642,12 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "pin-project-lite" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" + [[package]] name = "plotters" version = "0.3.7" @@ -809,15 +819,15 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rustix" -version = "0.38.41" +version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7f649912bc1495e167a6edee79151c84b1bad49748cb4f1f1167f459f6224f6" +checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -909,9 +919,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.89" +version = "2.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d46482f1c1c87acd84dea20c1bf5ebff4c757009ed6bf19cfd36fb10e92c4e" +checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" dependencies = [ "proc-macro2", "quote", @@ -933,18 +943,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c006c85c7651b3cf2ada4584faa36773bd07bac24acfb39f3c431b36d7e667aa" +checksum = "8fec2a1820ebd077e2b90c4df007bebf344cd394098a13c563957d0afc83ea47" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.3" +version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f077553d607adc1caf65430528a576c757a71ed73944b66ebb58ef2bbd243568" +checksum = "d65750cab40f4ff1929fb1ba509e9914eb756131cef4210da8d5d700d26f6312" dependencies = [ "proc-macro2", "quote", @@ -961,6 +971,34 @@ dependencies = [ "serde_json", ] +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" + [[package]] name = "typenum" version = "1.17.0" @@ -1018,9 +1056,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396" dependencies = [ "cfg-if", "once_cell", @@ -1029,13 +1067,12 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79" dependencies = [ "bumpalo", "log", - "once_cell", "proc-macro2", "quote", "syn", @@ -1044,9 +1081,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1054,9 +1091,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2" dependencies = [ "proc-macro2", "quote", @@ -1067,15 +1104,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.95" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6" [[package]] name = "web-sys" -version = "0.3.72" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" +checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc" dependencies = [ "js-sys", "wasm-bindgen", @@ -1172,33 +1209,82 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winter-air" +version = "0.11.0" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" +dependencies = [ + "libm", + "winter-crypto", + "winter-fri", + "winter-math", + "winter-utils", +] + [[package]] name = "winter-crypto" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c57748fd2da77742be601f03eda639ff6046879738fd1faae86e80018263cb" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" dependencies = [ "blake3", + "rand", + "rand_chacha", "sha3", "winter-math", "winter-utils", ] +[[package]] +name = "winter-fri" +version = "0.11.0" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" +dependencies = [ + "rand", + "rand_chacha", + "winter-crypto", + "winter-math", + "winter-utils", +] + [[package]] name = "winter-math" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6020c17839fa107ce4a7cc178e407ebbc24adfac1980f4fa2111198e052700ab" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" dependencies = [ "serde", "winter-utils", ] +[[package]] +name = "winter-maybe-async" +version = "0.11.0" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "winter-prover" +version = "0.11.0" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" +dependencies = [ + "rand", + "rand_chacha", + "tracing", + "winter-air", + "winter-crypto", + "winter-fri", + "winter-math", + "winter-maybe-async", + "winter-rand-utils", + "winter-utils", +] + [[package]] name = "winter-rand-utils" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226e4c455f6eb72f64ac6eeb7642df25e21ff2280a4f6b09db75392ad6b390ef" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" dependencies = [ "rand", "winter-utils", @@ -1207,8 +1293,19 @@ dependencies = [ [[package]] name = "winter-utils" version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1507ef312ea5569d54c2c7446a18b82143eb2a2e21f5c3ec7cfbe8200c03bd7c" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" + +[[package]] +name = "winter-verifier" +version = "0.11.0" +source = "git+https://github.com/Al-Kindi-0/winterfell?branch=al-zk#5bafedbc2ba00cf85c6182725754547f6cddafc3" +dependencies = [ + "winter-air", + "winter-crypto", + "winter-fri", + "winter-math", + "winter-utils", +] [[package]] name = "zerocopy" diff --git a/Cargo.toml b/Cargo.toml index 46ac3bdd..2b7b5791 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,24 +66,27 @@ clap = { version = "4.5", optional = true, features = ["derive"] } num = { version = "0.4", default-features = false, features = ["alloc", "libm"] } num-complex = { version = "0.4", default-features = false } rand = { version = "0.8", default-features = false } +rand_chacha = { version = "0.3", default-features = false } rand_core = { version = "0.6", default-features = false } -rand-utils = { version = "0.11", package = "winter-rand-utils", optional = true } rayon = { version = "1.10", optional = true } serde = { version = "1.0", default-features = false, optional = true, features = ["derive"] } sha3 = { version = "0.10", default-features = false } thiserror = { version = "2.0", default-features = false } -winter-crypto = { version = "0.11", default-features = false } -winter-math = { version = "0.11", default-features = false } -winter-utils = { version = "0.11", default-features = false } +winter-air = {git = 'https://github.com/Al-Kindi-0/winterfell', branch = 'al-zk' } +winter-crypto = {git = 'https://github.com/Al-Kindi-0/winterfell', branch = 'al-zk' } +winter-prover = {git = 'https://github.com/Al-Kindi-0/winterfell', branch = 'al-zk' } +winter-verifier = {git = 'https://github.com/Al-Kindi-0/winterfell', branch = 'al-zk' } +winter-math = {git = 'https://github.com/Al-Kindi-0/winterfell', branch = 'al-zk' } +winter-utils = {git = 'https://github.com/Al-Kindi-0/winterfell', branch = 'al-zk' } +rand-utils = {git = 'https://github.com/Al-Kindi-0/winterfell', package = "winter-rand-utils" , branch = 'al-zk', optional = true } +getrandom = { version = "0.2", features = ["js"] } [dev-dependencies] assert_matches = { version = "1.5", default-features = false } criterion = { version = "0.5", features = ["html_reports"] } -getrandom = { version = "0.2", features = ["js"] } hex = { version = "0.4", default-features = false, features = ["alloc"] } proptest = "1.5" -rand_chacha = { version = "0.3", default-features = false } -rand-utils = { version = "0.11", package = "winter-rand-utils" } +rand-utils = {git = 'https://github.com/Al-Kindi-0/winterfell', package = "winter-rand-utils" , branch = 'al-zk' } seq-macro = { version = "0.3" } [build-dependencies] diff --git a/src/dsa/mod.rs b/src/dsa/mod.rs index 9c5c0a5e..470cde04 100644 --- a/src/dsa/mod.rs +++ b/src/dsa/mod.rs @@ -1,3 +1,5 @@ //! Digital signature schemes supported by default in the Miden VM. pub mod rpo_falcon512; + +pub mod rpo_stark; diff --git a/src/dsa/rpo_stark/mod.rs b/src/dsa/rpo_stark/mod.rs new file mode 100644 index 00000000..f35ef163 --- /dev/null +++ b/src/dsa/rpo_stark/mod.rs @@ -0,0 +1,24 @@ +mod signature; +pub use signature::{PublicKey, SecretKey, Signature}; + +mod stark; +pub use stark::{PublicInputs, RescueAir}; + +// TESTS +// ================================================================================================ + +#[cfg(test)] +mod tests { + use super::SecretKey; + use crate::Word; + + #[test] + fn test_signature() { + let sk = SecretKey::new(Word::default()); + + let message = Word::default(); + let signature = sk.sign(message); + let pk = sk.public_key(); + assert!(pk.verify(message, &signature)) + } +} diff --git a/src/dsa/rpo_stark/signature/mod.rs b/src/dsa/rpo_stark/signature/mod.rs new file mode 100644 index 00000000..c92a33d6 --- /dev/null +++ b/src/dsa/rpo_stark/signature/mod.rs @@ -0,0 +1,173 @@ +use rand::{distributions::Uniform, prelude::Distribution, Rng}; +use winter_air::{FieldExtension, ProofOptions}; +use winter_math::{fields::f64::BaseElement, FieldElement}; +use winter_prover::Proof; +use winter_utils::{ByteReader, ByteWriter, Deserializable, DeserializationError, Serializable}; + +use crate::{ + dsa::rpo_stark::stark::RpoSignatureScheme, + hash::{rpo::Rpo256, DIGEST_SIZE}, + StarkField, Word, ZERO, +}; + +// CONSTANTS +// ================================================================================================ + +/// Specifies the parameters of the STARK underlying the signature scheme. These parameters provide +/// at least 102 bits of security under the conjectured security of the toy protocol in +/// the ethSTARK paper [1]. +/// +/// [1]: https://eprint.iacr.org/2021/582 +pub const PROOF_OPTIONS: ProofOptions = + ProofOptions::new(30, 8, 12, FieldExtension::Quadratic, 4, 7, true); + +// PUBLIC KEY +// ================================================================================================ + +/// A public key for verifying signatures. +/// +/// The public key is a [Word] (i.e., 4 field elements) that is the hash of the secret key. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PublicKey(Word); + +impl PublicKey { + /// Returns the [Word] defining the public key. + pub fn inner(&self) -> Word { + self.0 + } +} + +impl PublicKey { + /// Verifies the provided signature against provided message and this public key. + pub fn verify(&self, message: Word, signature: &Signature) -> bool { + signature.verify(message, *self) + } +} + +impl Serializable for PublicKey { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for PublicKey { + fn read_from(source: &mut R) -> Result { + let pk = ::read_from(source)?; + Ok(Self(pk)) + } +} + +// SECRET KEY +// ================================================================================================ + +/// A secret key for generating signatures. +/// +/// The secret key is a [Word] (i.e., 4 field elements). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SecretKey(Word); + +impl SecretKey { + /// Generates a secret key from OS-provided randomness. + pub fn new(word: Word) -> Self { + Self(word) + } + + /// Generates a secret key from a [Word]. + #[cfg(feature = "std")] + pub fn random() -> Self { + use rand::{rngs::StdRng, SeedableRng}; + + let mut rng = StdRng::from_entropy(); + Self::with_rng(&mut rng) + } + + /// Generates a secret_key using the provided random number generator `Rng`. + pub fn with_rng(rng: &mut R) -> Self { + let mut sk = [ZERO; 4]; + let uni_dist = Uniform::from(0..BaseElement::MODULUS); + + for s in sk.iter_mut() { + let sampled_integer = uni_dist.sample(rng); + *s = BaseElement::new(sampled_integer); + } + + Self(sk) + } + + /// Computes the public key corresponding to this secret key. + pub fn public_key(&self) -> PublicKey { + let mut elements = [BaseElement::ZERO; 8]; + elements[..DIGEST_SIZE].copy_from_slice(&self.0); + let pk = Rpo256::hash_elements(&elements); + PublicKey(pk.into()) + } + + /// Signs a message with this secret key. + pub fn sign(&self, message: Word) -> Signature { + let signature: RpoSignatureScheme = RpoSignatureScheme::new(PROOF_OPTIONS); + let proof = signature.sign(self.0, message); + Signature { proof } + } +} + +impl Serializable for SecretKey { + fn write_into(&self, target: &mut W) { + self.0.write_into(target); + } +} + +impl Deserializable for SecretKey { + fn read_from(source: &mut R) -> Result { + let sk = ::read_from(source)?; + Ok(Self(sk)) + } +} + +// SIGNATURE +// ================================================================================================ + +/// An RPO STARK-based signature over a message. +/// +/// The signature is a STARK proof of knowledge of a pre-image given an image where the map is +/// the RPO permutation, the pre-image is the secret key and the image is the public key. +/// The current implementation follows the description in [1] but relies on the conjectured security +/// of the toy protocol in the ethSTARK paper [2], which gives us using the parameter set +/// given in `PROOF_OPTIONS` a signature with $102$ bits of average-case existential unforgeability +/// security against $2^{113}$-query bound adversaries that can obtain up to $2^{64}$ signatures +/// under the same public key. +/// +/// [1]: https://eprint.iacr.org/2024/1553 +/// [2]: https://eprint.iacr.org/2021/582 +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Signature { + proof: Proof, +} + +impl Signature { + /// Returns the STARK proof constituting the signature. + pub fn inner(&self) -> Proof { + self.proof.clone() + } + + /// Returns true if this signature is a valid signature for the specified message generated + /// against the secret key matching the specified public key. + pub fn verify(&self, message: Word, pk: PublicKey) -> bool { + let signature: RpoSignatureScheme = RpoSignatureScheme::new(PROOF_OPTIONS); + + let res = signature.verify(pk.inner(), message, self.proof.clone()); + res.is_ok() + } +} + +impl Serializable for Signature { + fn write_into(&self, target: &mut W) { + self.proof.write_into(target); + } +} + +impl Deserializable for Signature { + fn read_from(source: &mut R) -> Result { + let proof = Proof::read_from(source)?; + Ok(Self { proof }) + } +} diff --git a/src/dsa/rpo_stark/stark/air.rs b/src/dsa/rpo_stark/stark/air.rs new file mode 100644 index 00000000..eb7fede3 --- /dev/null +++ b/src/dsa/rpo_stark/stark/air.rs @@ -0,0 +1,198 @@ +use alloc::vec::Vec; + +use winter_math::{fields::f64::BaseElement, FieldElement, ToElements}; +use winter_prover::{ + Air, AirContext, Assertion, EvaluationFrame, ProofOptions, TraceInfo, + TransitionConstraintDegree, +}; + +use crate::{ + hash::{ARK1, ARK2, MDS, STATE_WIDTH}, + Word, ZERO, +}; + +// CONSTANTS +// ================================================================================================ + +pub const HASH_CYCLE_LEN: usize = 8; + +// AIR +// ================================================================================================ + +pub struct RescueAir { + context: AirContext, + pub_key: Word, +} + +impl Air for RescueAir { + type BaseField = BaseElement; + type PublicInputs = PublicInputs; + + type GkrProof = (); + type GkrVerifier = (); + + // CONSTRUCTOR + // -------------------------------------------------------------------------------------------- + fn new(trace_info: TraceInfo, pub_inputs: PublicInputs, options: ProofOptions) -> Self { + let degrees = vec![ + // Apply RPO rounds. + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + TransitionConstraintDegree::new(7), + ]; + assert_eq!(STATE_WIDTH, trace_info.width()); + let context = AirContext::new(trace_info, degrees, 12, options); + let context = context.set_num_transition_exemptions(1); + RescueAir { context, pub_key: pub_inputs.pub_key } + } + + fn context(&self) -> &AirContext { + &self.context + } + + fn evaluate_transition>( + &self, + frame: &EvaluationFrame, + periodic_values: &[E], + result: &mut [E], + ) { + let current = frame.current(); + let next = frame.next(); + // expected state width is 12 field elements + debug_assert_eq!(STATE_WIDTH, current.len()); + debug_assert_eq!(STATE_WIDTH, next.len()); + + enforce_rpo_round(frame, result, periodic_values); + } + + fn get_assertions(&self) -> Vec> { + let initial_step = 0; + let last_step = self.trace_length() - 1; + vec![ + // Assert that the capacity as well as the second half of the rate portion of the state + // are initialized to `ZERO`.The first half of the rate is unconstrained as it will + // contain the secret key + Assertion::single(0, initial_step, Self::BaseField::ZERO), + Assertion::single(1, initial_step, Self::BaseField::ZERO), + Assertion::single(2, initial_step, Self::BaseField::ZERO), + Assertion::single(3, initial_step, Self::BaseField::ZERO), + Assertion::single(8, initial_step, Self::BaseField::ZERO), + Assertion::single(9, initial_step, Self::BaseField::ZERO), + Assertion::single(10, initial_step, Self::BaseField::ZERO), + Assertion::single(11, initial_step, Self::BaseField::ZERO), + // Assert that the public key is the correct one + Assertion::single(4, last_step, self.pub_key[0]), + Assertion::single(5, last_step, self.pub_key[1]), + Assertion::single(6, last_step, self.pub_key[2]), + Assertion::single(7, last_step, self.pub_key[3]), + ] + } + + fn get_periodic_column_values(&self) -> Vec> { + get_round_constants() + } +} + +pub struct PublicInputs { + pub(crate) pub_key: Word, + pub(crate) msg: Word, +} + +impl PublicInputs { + pub fn new(pub_key: Word, msg: Word) -> Self { + Self { pub_key, msg } + } +} + +impl ToElements for PublicInputs { + fn to_elements(&self) -> Vec { + let mut res = self.pub_key.to_vec(); + res.extend_from_slice(self.msg.as_ref()); + res + } +} + +// HELPER EVALUATORS +// ------------------------------------------------------------------------------------------------ + +/// Enforces constraints for a single round of the Rescue Prime Optimized hash functions. +pub fn enforce_rpo_round>( + frame: &EvaluationFrame, + result: &mut [E], + ark: &[E], +) { + // compute the state that should result from applying the first 5 operations of the RPO round to + // the current hash state. + let mut step1 = [E::ZERO; STATE_WIDTH]; + step1.copy_from_slice(frame.current()); + + apply_mds(&mut step1); + // add constants + for i in 0..STATE_WIDTH { + step1[i] += ark[i]; + } + apply_sbox(&mut step1); + apply_mds(&mut step1); + // add constants + for i in 0..STATE_WIDTH { + step1[i] += ark[STATE_WIDTH + i]; + } + + // compute the state that should result from applying the inverse of the last operation of the + // RPO round to the next step of the computation. + let mut step2 = [E::ZERO; STATE_WIDTH]; + step2.copy_from_slice(frame.next()); + apply_sbox(&mut step2); + + // make sure that the results are equal. + for i in 0..STATE_WIDTH { + result[i] = step2[i] - step1[i] + } +} + +#[inline(always)] +fn apply_sbox>(state: &mut [E; STATE_WIDTH]) { + state.iter_mut().for_each(|v| { + let t2 = v.square(); + let t4 = t2.square(); + *v *= t2 * t4; + }); +} + +#[inline(always)] +fn apply_mds>(state: &mut [E; STATE_WIDTH]) { + let mut result = [E::ZERO; STATE_WIDTH]; + result.iter_mut().zip(MDS).for_each(|(r, mds_row)| { + state.iter().zip(mds_row).for_each(|(&s, m)| { + *r += E::from(m) * s; + }); + }); + *state = result +} + +/// Returns RPO round constants arranged in column-major form. +pub fn get_round_constants() -> Vec> { + let mut constants = Vec::new(); + for _ in 0..(STATE_WIDTH * 2) { + constants.push(vec![ZERO; HASH_CYCLE_LEN]); + } + + #[allow(clippy::needless_range_loop)] + for i in 0..HASH_CYCLE_LEN - 1 { + for j in 0..STATE_WIDTH { + constants[j][i] = ARK1[i][j]; + constants[j + STATE_WIDTH][i] = ARK2[i][j]; + } + } + + constants +} diff --git a/src/dsa/rpo_stark/stark/mod.rs b/src/dsa/rpo_stark/stark/mod.rs new file mode 100644 index 00000000..80d409cd --- /dev/null +++ b/src/dsa/rpo_stark/stark/mod.rs @@ -0,0 +1,69 @@ +use core::marker::PhantomData; + +use prover::RpoSignatureProver; +use rand::{distributions::Standard, prelude::Distribution, thread_rng, RngCore, SeedableRng}; +use rand_chacha::ChaCha20Rng; +use winter_crypto::{ElementHasher, Hasher, SaltedMerkleTree}; +use winter_math::fields::f64::BaseElement; +use winter_prover::{Proof, ProofOptions, Prover}; +use winter_verifier::{verify, AcceptableOptions, VerifierError}; + +use crate::{ + hash::{rpo::Rpo256, DIGEST_SIZE}, + rand::RpoRandomCoin, +}; + +mod air; +pub use air::{PublicInputs, RescueAir}; +mod prover; + +/// Represents an abstract STARK-based signature scheme with knowledge of RPO pre-image as +/// the hard relation. +pub struct RpoSignatureScheme { + options: ProofOptions, + _h: PhantomData, +} + +impl + Sync> RpoSignatureScheme +where + Standard: Distribution<::Digest>, +{ + pub fn new(options: ProofOptions) -> Self { + RpoSignatureScheme { options, _h: PhantomData } + } + + pub fn sign(&self, sk: [BaseElement; DIGEST_SIZE], msg: [BaseElement; DIGEST_SIZE]) -> Proof { + // create a prover + let prover = RpoSignatureProver::::new(msg, self.options.clone()); + + // generate execution trace + let trace = prover.build_trace(sk); + + // generate the initial seed for the PRNG used for zero-knowledge + let mut seed = ::Seed::default(); + let mut rng = thread_rng(); + rng.fill_bytes(&mut seed); + + // generate the proof + prover.prove(trace, Some(seed)).expect("failed to generate the signature") + } + + pub fn verify( + &self, + pub_key: [BaseElement; DIGEST_SIZE], + msg: [BaseElement; DIGEST_SIZE], + proof: Proof, + ) -> Result<(), VerifierError> { + // we make sure that the parameters used in generating the proof match the expected ones + if *proof.options() != self.options { + return Err(VerifierError::UnacceptableProofOptions); + } + let pub_inputs = PublicInputs { pub_key, msg }; + let acceptable_options = AcceptableOptions::OptionSet(vec![proof.options().clone()]); + verify::>( + proof, + pub_inputs, + &acceptable_options, + ) + } +} diff --git a/src/dsa/rpo_stark/stark/prover.rs b/src/dsa/rpo_stark/stark/prover.rs new file mode 100644 index 00000000..9018dab2 --- /dev/null +++ b/src/dsa/rpo_stark/stark/prover.rs @@ -0,0 +1,148 @@ +use core::marker::PhantomData; + +use rand_chacha::ChaCha20Rng; +use winter_air::{ + AuxRandElements, ConstraintCompositionCoefficients, PartitionOptions, ZkParameters, +}; +use winter_crypto::{ElementHasher, SaltedMerkleTree}; +use winter_math::{fields::f64::BaseElement, FieldElement}; +use winter_prover::{ + matrix::ColMatrix, CompositionPoly, CompositionPolyTrace, DefaultConstraintCommitment, + DefaultConstraintEvaluator, DefaultTraceLde, ProofOptions, Prover, StarkDomain, Trace, + TraceInfo, TracePolyTable, TraceTable, +}; + +use super::air::{PublicInputs, RescueAir, HASH_CYCLE_LEN}; +use crate::{ + hash::{rpo::Rpo256, STATE_WIDTH}, + rand::RpoRandomCoin, + Word, ZERO, +}; + +// PROVER +// ================================================================================================ + +/// A prover for the RPO STARK-based signature scheme. +/// +/// The signature is based on the the one-wayness of the RPO hash function but it is generic over +/// the hash function used for instantiating the random oracle for the BCS transform. +pub(crate) struct RpoSignatureProver { + message: Word, + options: ProofOptions, + _hasher: PhantomData, +} + +impl RpoSignatureProver { + pub(crate) fn new(message: Word, options: ProofOptions) -> Self { + Self { message, options, _hasher: PhantomData } + } + + pub(crate) fn build_trace(&self, sk: Word) -> TraceTable { + let mut trace = TraceTable::new(STATE_WIDTH, HASH_CYCLE_LEN); + + trace.fill( + |state| { + // initialize first half of the rate portion of the state with the secret key + state[0] = ZERO; + state[1] = ZERO; + state[2] = ZERO; + state[3] = ZERO; + state[4] = sk[0]; + state[5] = sk[1]; + state[6] = sk[2]; + state[7] = sk[3]; + state[8] = ZERO; + state[9] = ZERO; + state[10] = ZERO; + state[11] = ZERO; + }, + |step, state| { + Rpo256::apply_round( + state.try_into().expect("should not fail given the size of the array"), + step, + ); + }, + ); + trace + } +} + +impl Prover for RpoSignatureProver +where + H: ElementHasher + Sync, +{ + type BaseField = BaseElement; + type Air = RescueAir; + type Trace = TraceTable; + type HashFn = Rpo256; + type VC = SaltedMerkleTree; + type RandomCoin = RpoRandomCoin; + type TraceLde> = + DefaultTraceLde; + type ConstraintCommitment> = + DefaultConstraintCommitment; + type ConstraintEvaluator<'a, E: FieldElement> = + DefaultConstraintEvaluator<'a, Self::Air, E>; + type ZkPrng = ChaCha20Rng; + + fn get_pub_inputs(&self, trace: &Self::Trace) -> PublicInputs { + let last_step = trace.length() - 1; + // Note that the message is not part of the execution trace but is part of the public + // inputs. This is explained in the reference description of the DSA and intuitively + // it is done in order to make sure that the message is part of the Fiat-Shamir + // transcript and hence binds the proof/signature to the message + PublicInputs { + pub_key: [ + trace.get(4, last_step), + trace.get(5, last_step), + trace.get(6, last_step), + trace.get(7, last_step), + ], + msg: self.message, + } + } + + fn options(&self) -> &ProofOptions { + &self.options + } + + fn new_trace_lde>( + &self, + trace_info: &TraceInfo, + main_trace: &ColMatrix, + domain: &StarkDomain, + partition_option: PartitionOptions, + zk_parameters: Option, + prng: &mut Option, + ) -> (Self::TraceLde, TracePolyTable) { + DefaultTraceLde::new(trace_info, main_trace, domain, partition_option, zk_parameters, prng) + } + + fn new_evaluator<'a, E: FieldElement>( + &self, + air: &'a Self::Air, + aux_rand_elements: Option>, + composition_coefficients: ConstraintCompositionCoefficients, + ) -> Self::ConstraintEvaluator<'a, E> { + DefaultConstraintEvaluator::new(air, aux_rand_elements, composition_coefficients) + } + + fn build_constraint_commitment>( + &self, + composition_poly_trace: CompositionPolyTrace, + num_constraint_composition_columns: usize, + domain: &StarkDomain, + partition_options: PartitionOptions, + zk_parameters: Option, + prng: &mut Option, + ) -> (Self::ConstraintCommitment, CompositionPoly) { + DefaultConstraintCommitment::new( + composition_poly_trace, + num_constraint_composition_columns, + domain, + partition_options, + zk_parameters, + prng, + ) + } +} diff --git a/src/hash/mod.rs b/src/hash/mod.rs index e7fd9c72..4d11511a 100644 --- a/src/hash/mod.rs +++ b/src/hash/mod.rs @@ -5,6 +5,7 @@ use super::{CubeExtension, Felt, FieldElement, StarkField, ZERO}; pub mod blake; mod rescue; +pub(crate) use rescue::{ARK1, ARK2, DIGEST_SIZE, MDS, STATE_WIDTH}; pub mod rpo { pub use super::rescue::{Rpo256, RpoDigest, RpoDigestError}; } diff --git a/src/hash/rescue/mod.rs b/src/hash/rescue/mod.rs index fee20aba..42bdb60e 100644 --- a/src/hash/rescue/mod.rs +++ b/src/hash/rescue/mod.rs @@ -6,7 +6,7 @@ mod arch; pub use arch::optimized::{add_constants_and_apply_inv_sbox, add_constants_and_apply_sbox}; mod mds; -use mds::{apply_mds, MDS}; +pub(crate) use mds::{apply_mds, MDS}; mod rpo; pub use rpo::{Rpo256, RpoDigest, RpoDigestError}; @@ -26,7 +26,7 @@ const NUM_ROUNDS: usize = 7; /// Sponge state is set to 12 field elements or 96 bytes; 8 elements are reserved for rate and /// the remaining 4 elements are reserved for capacity. -const STATE_WIDTH: usize = 12; +pub(crate) const STATE_WIDTH: usize = 12; /// The rate portion of the state is located in elements 4 through 11. const RATE_RANGE: Range = 4..12; @@ -42,8 +42,8 @@ const CAPACITY_RANGE: Range = 0..4; /// /// The digest is returned from state elements 4, 5, 6, and 7 (the first four elements of the /// rate portion). -const DIGEST_RANGE: Range = 4..8; -const DIGEST_SIZE: usize = DIGEST_RANGE.end - DIGEST_RANGE.start; +pub(crate) const DIGEST_RANGE: Range = 4..8; +pub(crate) const DIGEST_SIZE: usize = DIGEST_RANGE.end - DIGEST_RANGE.start; /// The number of bytes needed to encoded a digest const DIGEST_BYTES: usize = 32; @@ -144,7 +144,7 @@ fn add_constants(state: &mut [Felt; STATE_WIDTH], ark: &[Felt; STATE_WIDTH]) { /// /// The constants are broken up into two arrays ARK1 and ARK2; ARK1 contains the constants for the /// first half of RPO round, and ARK2 contains constants for the second half of RPO round. -const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ +pub(crate) const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ [ Felt::new(5789762306288267392), Felt::new(6522564764413701783), @@ -245,7 +245,7 @@ const ARK1: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ ], ]; -const ARK2: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ +pub(crate) const ARK2: [[Felt; STATE_WIDTH]; NUM_ROUNDS] = [ [ Felt::new(6077062762357204287), Felt::new(15277620170502011191), diff --git a/src/hash/rescue/rpo/digest.rs b/src/hash/rescue/rpo/digest.rs index 44663691..c69200e0 100644 --- a/src/hash/rescue/rpo/digest.rs +++ b/src/hash/rescue/rpo/digest.rs @@ -1,6 +1,10 @@ use alloc::string::String; use core::{cmp::Ordering, fmt::Display, ops::Deref, slice}; +use rand::{ + distributions::{Standard, Uniform}, + prelude::Distribution, +}; use thiserror::Error; use super::{Digest, Felt, StarkField, DIGEST_BYTES, DIGEST_SIZE, ZERO}; @@ -126,6 +130,18 @@ impl Randomizable for RpoDigest { } } +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> RpoDigest { + let mut res = [ZERO; DIGEST_SIZE]; + let uni_dist = Uniform::from(0..Felt::MODULUS); + for r in res.iter_mut() { + let sampled_integer = uni_dist.sample(rng); + *r = Felt::new(sampled_integer); + } + RpoDigest::new(res) + } +} + // CONVERSIONS: FROM RPO DIGEST // ================================================================================================ diff --git a/src/rand/rpo.rs b/src/rand/rpo.rs index 2669592f..4f8ff731 100644 --- a/src/rand/rpo.rs +++ b/src/rand/rpo.rs @@ -174,6 +174,36 @@ impl RandomCoin for RpoRandomCoin { Ok(values) } + + fn reseed_with_salt( + &mut self, + data: ::Digest, + salt: Option<::Digest>, + ) { + // Reset buffer + self.current = RATE_START; + + // Add the new seed material to the first half of the rate portion of the RPO state + let data: Word = data.into(); + + self.state[RATE_START] += data[0]; + self.state[RATE_START + 1] += data[1]; + self.state[RATE_START + 2] += data[2]; + self.state[RATE_START + 3] += data[3]; + + if let Some(salt) = salt { + // Add the salt to the second half of the rate portion of the RPO state + let data: Word = salt.into(); + + self.state[RATE_START + 4] += data[0]; + self.state[RATE_START + 5] += data[1]; + self.state[RATE_START + 6] += data[2]; + self.state[RATE_START + 7] += data[3]; + } + + // Absorb + Rpo256::apply_permutation(&mut self.state); + } } // FELT RNG IMPLEMENTATION diff --git a/src/rand/rpx.rs b/src/rand/rpx.rs index 2f91b0ca..e2693a03 100644 --- a/src/rand/rpx.rs +++ b/src/rand/rpx.rs @@ -172,6 +172,36 @@ impl RandomCoin for RpxRandomCoin { Ok(values) } + + fn reseed_with_salt( + &mut self, + data: ::Digest, + salt: Option<::Digest>, + ) { + // Reset buffer + self.current = RATE_START; + + // Add the new seed material to the first half of the rate portion of the RPO state + let data: Word = data.into(); + + self.state[RATE_START] += data[0]; + self.state[RATE_START + 1] += data[1]; + self.state[RATE_START + 2] += data[2]; + self.state[RATE_START + 3] += data[3]; + + if let Some(salt) = salt { + // Add the salt to the second half of the rate portion of the RPO state + let data: Word = salt.into(); + + self.state[RATE_START + 4] += data[0]; + self.state[RATE_START + 5] += data[1]; + self.state[RATE_START + 6] += data[2]; + self.state[RATE_START + 7] += data[3]; + } + + // Absorb + Rpx256::apply_permutation(&mut self.state); + } } // FELT RNG IMPLEMENTATION