diff --git a/Cargo.lock b/Cargo.lock index 6a7707f7..347e59c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,6 +125,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + [[package]] name = "ansi_term" version = "0.12.1" @@ -869,7 +875,16 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", ] [[package]] @@ -878,6 +893,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -1185,6 +1206,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c" +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + [[package]] name = "cc" version = "1.2.4" @@ -1286,6 +1313,33 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + [[package]] name = "cid" version = "0.9.0" @@ -1399,6 +1453,12 @@ dependencies = [ "sp-std", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1759,6 +1819,42 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -1861,7 +1957,7 @@ dependencies = [ "cpufeatures", "curve25519-dalek-derive", "digest 0.10.7", - "fiat-crypto 0.2.9", + "fiat-crypto", "rand_core", "rustc_version 0.4.1", "subtle 2.6.1", @@ -2060,6 +2156,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -2390,7 +2487,7 @@ dependencies = [ "digest 0.10.7", "elliptic-curve", "rfc6979", - "serdect", + "serdect 0.2.0", "signature", "spki", ] @@ -2436,15 +2533,16 @@ dependencies = [ ] [[package]] -name = "ed448-goldilocks" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88322282bccdc6fa7ab65b0c30cb877fba541547653436d08bb775fa4a4307b4" +name = "ed448-goldilocks-plus" +version = "0.13.2" +source = "git+https://github.com/webb-tools/Ed448-Goldilocks#5af06c81541ed19e2456117536be66331b9fac69" dependencies = [ - "fiat-crypto 0.1.20", - "hex", + "elliptic-curve", "rand_core", + "serdect 0.3.0-rc.0", + "sha3", "subtle 2.6.1", + "zeroize", ] [[package]] @@ -2487,19 +2585,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", + "base64ct", "crypto-bigint", "digest 0.10.7", "ff", "generic-array 0.14.7", "group", + "pem-rfc7468", "pkcs8", "rand_core", "sec1", - "serdect", + "serde_json", + "serdect 0.2.0", "subtle 2.6.1", + "tap", "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ena" version = "0.14.3" @@ -3364,6 +3478,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" dependencies = [ + "bitvec", "byteorder", "ff_derive", "rand_core", @@ -3386,12 +3501,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "fiat-crypto" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" - [[package]] name = "fiat-crypto" version = "0.2.9" @@ -4010,16 +4119,15 @@ dependencies = [ [[package]] name = "frost-ed448" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c4dffab2dc334fc1b864cf1ce930aafc29e05e4ab9281c81858e92752c1410" +version = "1.2.3" dependencies = [ - "document-features", - "ed448-goldilocks", - "frost-core", - "frost-rerandomized", + "ed448-goldilocks-plus", + "parity-scale-codec", "rand_core", "sha3", + "sp-std", + "subtle 2.6.1", + "tg-frost-core", ] [[package]] @@ -4036,6 +4144,19 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "frost-p384" +version = "1.2.3" +dependencies = [ + "p384", + "parity-scale-codec", + "rand_core", + "sha2 0.10.8", + "sp-std", + "subtle 2.6.1", + "tg-frost-core", +] + [[package]] name = "frost-rerandomized" version = "2.0.0" @@ -4077,6 +4198,20 @@ dependencies = [ "sha2 0.10.8", ] +[[package]] +name = "frost-secp256k1-tr" +version = "1.2.3" +dependencies = [ + "k256", + "parity-scale-codec", + "rand_core", + "sha2 0.10.8", + "signature", + "sp-std", + "subtle 2.6.1", + "tg-frost-core", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -5646,7 +5781,7 @@ dependencies = [ "ecdsa", "elliptic-curve", "once_cell", - "serdect", + "serdect 0.2.0", "sha2 0.10.8", "signature", ] @@ -5706,7 +5841,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55cb077ad656299f160924eb2912aa147d7339ea7d69e1b5517326fdcec3c1ca" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.5.3", "ena", "itertools 0.11.0", "lalrpop-util", @@ -7297,6 +7432,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + [[package]] name = "opaque-debug" version = "0.2.3" @@ -7408,10 +7549,20 @@ checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ "ecdsa", "elliptic-curve", - "primeorder", + "primeorder 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", "sha2 0.10.8", ] +[[package]] +name = "p384" +version = "0.13.0" +source = "git+https://github.com/LIT-Protocol/elliptic-curves.git#67924afc93d236e1508afd5f55bbf738e1c41eaa" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder 0.13.6 (git+https://github.com/LIT-Protocol/elliptic-curves.git)", +] + [[package]] name = "pallet-airdrop-claims" version = "1.2.3" @@ -8266,8 +8417,10 @@ dependencies = [ "frost-ed25519", "frost-ed448", "frost-p256", + "frost-p384", "frost-ristretto255", "frost-secp256k1", + "frost-secp256k1-tr", "hex-literal 0.4.1", "pallet-balances", "pallet-evm", @@ -8281,6 +8434,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", + "tg-frost-core", ] [[package]] @@ -9138,6 +9292,15 @@ dependencies = [ "base64 0.13.1", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -9328,6 +9491,34 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + [[package]] name = "polkadot-core-primitives" version = "15.0.0" @@ -9482,6 +9673,18 @@ version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +[[package]] +name = "postcard" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -9615,6 +9818,14 @@ dependencies = [ "elliptic-curve", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "git+https://github.com/LIT-Protocol/elliptic-curves.git#67924afc93d236e1508afd5f55bbf738e1c41eaa" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "primitive-types" version = "0.12.2" @@ -9756,6 +9967,8 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14cae93065090804185d3b75f0bf93b8eeda30c7a9b4a33d3bdb3988d6229e50" dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", "bitflags 2.6.0", "lazy_static", "num-traits", @@ -9763,6 +9976,8 @@ dependencies = [ "rand_chacha", "rand_xorshift", "regex-syntax 0.8.5", + "rusty-fork", + "tempfile", "unarray", ] @@ -10823,6 +11038,18 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "ruzstd" version = "0.5.0" @@ -12219,7 +12446,7 @@ dependencies = [ "der", "generic-array 0.14.7", "pkcs8", - "serdect", + "serdect 0.2.0", "subtle 2.6.1", "zeroize", ] @@ -12414,6 +12641,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serdect" +version = "0.3.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a504c8ee181e3e594d84052f983d60afe023f4d94d050900be18062bbbf7b58" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -13787,7 +14024,7 @@ checksum = "2ad5e4452423e6bceebf2ecb2c4680f7159a62196e3e6b0ffb824b8d59c3fc90" dependencies = [ "ff", "hex-literal 0.3.4", - "primeorder", + "primeorder 0.13.6 (registry+https://github.com/rust-lang/crates.io-index)", "subtle 2.6.1", "zeroize", ] @@ -14776,6 +15013,36 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" +[[package]] +name = "tg-frost-core" +version = "1.2.3" +dependencies = [ + "byteorder", + "const-crc32-nostd", + "criterion", + "debugless-unwrap", + "derive-getters", + "document-features", + "hex", + "itertools 0.13.0", + "lazy_static", + "parity-scale-codec", + "postcard", + "proptest", + "rand", + "rand_chacha", + "rand_core", + "serde", + "serde_json", + "serdect 0.2.0", + "sp-std", + "subtle 2.6.1", + "thiserror 2.0.8", + "thiserror-nostd-notrait", + "visibility", + "zeroize", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -14921,6 +15188,16 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -15598,6 +15875,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 27137179..cfa91e7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -129,11 +129,15 @@ starknet-crypto = { version = "0.7.1", default-features = false, features = ["si frost-core = { version = "2.0.0", default-features = false } frost-ed25519 = { version = "2.0.0", default-features = false } -frost-ed448 = { version = "2.0.0", default-features = false } frost-ristretto255 = { version = "2.0.0", default-features = false } frost-secp256k1 = { version = "2.0.0", default-features = false } frost-p256 = { version = "2.0.0", default-features = false } +tg-frost-core = { path = "frost", default-features = false } +frost-p384 = { path = "frost/frost-p384", default-features = false } +frost-ed448 = { path = "frost/frost-ed448", default-features = false } +frost-secp256k1-tr = { path = "frost/frost-secp256k1-tr", default-features = false } + # Substrate dependencies sp-api = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } sp-block-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "stable2407", default-features = false } diff --git a/frost/Cargo.toml b/frost/Cargo.toml new file mode 100644 index 00000000..a176b637 --- /dev/null +++ b/frost/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "tg-frost-core" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } + +[dependencies] +byteorder = { version = "1.4", default-features = false } +const-crc32 = { version = "1.2.0", package = "const-crc32-nostd" } +document-features = "0.2.7" +debugless-unwrap = "0.0.4" +derive-getters = "0.5.0" +hex = { workspace = true, features = ["alloc"] } +itertools = { version = "0.13.0", default-features = false } +parity-scale-codec = { workspace = true } +postcard = { version = "1.0.0", default-features = false, features = ["alloc"], optional = true } +rand_core = { workspace = true } +serde = { workspace = true, optional = true } +serdect = { workspace = true, features = ["alloc"], optional = true } +sp-std = { workspace = true } +subtle = { workspace = true } +thiserror = { version = "2.0.3", default-features = false, optional = true } +thiserror-nostd-notrait = { version = "1.0.29", default-features = false } +visibility = "0.1.0" +zeroize = { version = "1.8.1", default-features = false, features = ["derive"] } + +# Test dependencies used with the test-impl feature +proptest = { version = "1.0", optional = true } +serde_json = { version = "1.0", optional = true } +criterion = { version = "0.5", optional = true } + +[dev-dependencies] +criterion = { version = "0.5" } +lazy_static = "1.4" +proptest = "1.0" +rand = "0.8" +rand_chacha = "0.3" +serde_json = "1.0" + +[features] +default = ["serialization", "cheater-detection", "std"] +std = [ + "byteorder/std", + "rand_core/std", + "hex/std", + "sp-std/std", + "dep:thiserror", + +] +## Expose internal types, which do not have SemVer guarantees. This is an advanced +## feature which can be useful if you need to build a modified version of FROST. +## The docs won't list them, you will need to check the source code. +internals = [] +# Exposes ciphersuite-generic tests for other crates to use +test-impl = ["dep:proptest", "dep:serde_json", "dep:criterion"] +serialization = ["serde", "dep:postcard"] +serde = ["dep:serde", "dep:serdect"] +cheater-detection = [] diff --git a/frost/frost-ed448/Cargo.toml b/frost/frost-ed448/Cargo.toml new file mode 100644 index 00000000..ff36d1ce --- /dev/null +++ b/frost/frost-ed448/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "frost-ed448" +edition = "2021" +# When releasing to crates.io: +# - Update html_root_url +# - Update CHANGELOG.md +# - Create git tag. +version.workspace = true +authors = [ + "Deirdre Connolly ", + "Chelsea Komlo ", + "Conrado Gouvea " +] +readme = "README.md" +license = "MIT OR Apache-2.0" +categories = ["cryptography"] +keywords = ["cryptography", "crypto", "ed25519", "threshold", "signature"] +description = "A Schnorr signature scheme over Ed25519 that supports FROST." + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +parity-scale-codec = { workspace = true } +ed448-goldilocks = { git = "https://github.com/webb-tools/Ed448-Goldilocks", package = "ed448-goldilocks-plus", default-features = false, features = ["zeroize"] } +sp-std = { workspace = true } +tg-frost-core = { workspace = true, features = ["internals"] } +rand_core = { workspace = true } +sha3 = { version = "0.10", default-features = false } +subtle = { workspace = true } + +[features] +default = ["std"] +std = [ + "sp-std/std", + "rand_core/std", + "parity-scale-codec/std", + "sha3/std", + "tg-frost-core/std", + "ed448-goldilocks/std" +] diff --git a/frost/frost-ed448/src/lib.rs b/frost/frost-ed448/src/lib.rs new file mode 100644 index 00000000..671bd15c --- /dev/null +++ b/frost/frost-ed448/src/lib.rs @@ -0,0 +1,392 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + +use sp_std::collections::btree_map::BTreeMap; + +use ed448_goldilocks::{ + elliptic_curve::generic_array::{typenum::U114, GenericArray}, + CompressedEdwardsY, EdwardsPoint, Scalar, ScalarBytes, +}; +use rand_core::{CryptoRng, RngCore}; +use sha3::{ + digest::{ExtendableOutput, Update, XofReader}, + Shake256, +}; + +use frost_core as frost; +use tg_frost_core as frost_core; + +// Re-exports in our public API +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; +pub use rand_core; + +/// An error. +pub type Error = frost_core::Error; + +/// An implementation of the FROST(Ed448, SHAKE256) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct Ed448ScalarField; + +impl Field for Ed448ScalarField { + type Scalar = Scalar; + + type Serialization = [u8; 57]; + + fn zero() -> Self::Scalar { + Scalar::ZERO + } + + fn one() -> Self::Scalar { + Scalar::ONE + } + + fn invert(scalar: &Self::Scalar) -> Result { + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(scalar.invert()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes_rfc_8032().into() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let buffer = ScalarBytes::clone_from_slice(buf); + Option::::from(Scalar::from_canonical_bytes(&buffer)) + .ok_or(FieldError::MalformedScalar) + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + Self::serialize(scalar) + } +} + +/// An implementation of the FROST(Ed448, SHAKE256) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Ed448Group; + +impl Group for Ed448Group { + type Field = Ed448ScalarField; + + type Element = EdwardsPoint; + + type Serialization = [u8; 57]; + + fn cofactor() -> ::Scalar { + Scalar::ONE + } + + fn identity() -> Self::Element { + Self::Element::IDENTITY + } + + fn generator() -> Self::Element { + Self::Element::GENERATOR + } + + fn serialize(element: &Self::Element) -> Result { + Ok(element.compress().0) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let compressed = CompressedEdwardsY(*buf); + match Option::::from(compressed.decompress()) { + Some(point) => { + if point == EdwardsPoint::IDENTITY { + Err(GroupError::InvalidIdentityElement) + } else if point.is_torsion_free().into() { + // decompress() does not check for canonicality, so we + // check by recompressing and comparing + if point.compress().0 != compressed.0 { + Err(GroupError::MalformedElement) + } else { + Ok(point) + } + } else { + Err(GroupError::InvalidNonPrimeOrderElement) + } + }, + None => Err(GroupError::MalformedElement), + } + } +} + +fn hash_to_array(inputs: &[&[u8]]) -> [u8; 114] { + let mut h = Shake256::default(); + for i in inputs { + h.update(i); + } + let mut reader = h.finalize_xof(); + let mut output = [0u8; 114]; + reader.read(&mut output); + output +} + +fn hash_to_scalar(inputs: &[&[u8]]) -> Scalar { + let output = GenericArray::::clone_from_slice(&hash_to_array(inputs)); + Scalar::from_bytes_mod_order_wide(&output) +} + +/// Context string from the ciphersuite in the [spec] +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-1 +const CONTEXT_STRING: &str = "FROST-ED448-SHAKE256-v1"; + +/// An implementation of the FROST(Ed448, SHAKE256) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Ed448Shake256; + +impl Ciphersuite for Ed448Shake256 { + const ID: &'static str = CONTEXT_STRING; + + type Group = Ed448Group; + + type HashOutput = [u8; 114]; + + type SignatureSerialization = [u8; 114]; + + /// H1 for FROST(Ed448, SHAKE256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.1 + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho", m]) + } + + /// H2 for FROST(Ed448, SHAKE256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.2 + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[b"SigEd448\0\0", m]) + } + + /// H3 for FROST(Ed448, SHAKE256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.3 + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce", m]) + } + + /// H4 for FROST(Ed448, SHAKE256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.4 + fn H4(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) + } + + /// H5 for FROST(Ed448, SHAKE256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.3-2.2.2.5 + fn H5(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) + } + + /// HDKG for FROST(Ed448, SHAKE256) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg", m])) + } + + /// HID for FROST(Ed448, SHAKE256) + fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id", m])) + } +} + +type E = Ed448Shake256; + +/// A FROST(Ed448, SHAKE256) participant identifier. +pub type Identifier = frost::Identifier; + +/// FROST(Ed448, SHAKE256) keys, key generation, key shares. +pub mod keys { + use super::*; + use sp_std::collections::btree_map::BTreeMap; + + /// The identifier list to use when generating key shares. + pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, E>; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + mut rng: RNG, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) + } + + /// Splits an existing key into FROST shares. + /// + /// This is identical to [`generate_with_dealer`] but receives an existing key + /// instead of generating a fresh one. This is useful in scenarios where + /// the key needs to be generated externally or must be derived from e.g. a + /// seed phrase. + pub fn split( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::split(secret, max_signers, min_signers, identifiers, rng) + } + + /// Recompute the secret from t-of-n secret shares using Lagrange interpolation. + /// + /// This can be used if for some reason the original key must be restored; e.g. + /// if threshold signing is not required anymore. + /// + /// This is NOT required to sign with FROST; the whole point of FROST is being + /// able to generate signatures only using the shares, without having to + /// reconstruct the original key. + /// + /// The caller is responsible for providing at least `min_signers` shares; + /// if less than that is provided, a different key will be returned. + pub fn reconstruct(secret_shares: &[KeyPackage]) -> Result { + frost::keys::reconstruct(secret_shares) + } + + /// Secret and public key material generated by a dealer performing + /// [`generate_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(Ed448, SHAKE256) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare; + + /// A secret scalar value representing a signer's share of the group secret. + pub type SigningShare = frost::keys::SigningShare; + + /// A public group element that represents a single signer's public verification share. + pub type VerifyingShare = frost::keys::VerifyingShare; + + /// A FROST(Ed448, SHAKE256) keypair, which can be generated either by a trusted dealer or using + /// a DKG. + /// + /// When using a central dealer, [`SecretShare`]s are distributed to + /// participants, who then perform verification, before deriving + /// [`KeyPackage`]s, which they store to later use during signing. + pub type KeyPackage = frost::keys::KeyPackage; + + /// Public data that contains all the signers' public keys as well as the + /// group public key. + /// + /// Used for verification purposes before publishing a signature. + pub type PublicKeyPackage = frost::keys::PublicKeyPackage; + + /// Contains the commitments to the coefficients for our secret polynomial _f_, + /// used to generate participants' key shares. + /// + /// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which + /// themselves are scalars) for a secret polynomial f, where f is used to + /// generate each ith participant's key share f(i). Participants use this set of + /// commitments to perform verifiable secret sharing. + /// + /// Note that participants MUST be assured that they have the *same* + /// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using + /// some agreed-upon public location for publication, where each participant can + /// ensure that they received the correct (and same) value. + pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; +} + +/// FROST(Ed448, SHAKE256) Round 1 functionality and types. +pub mod round1 { + use crate::keys::SigningShare; + + use super::*; + + /// Comprised of FROST(Ed448, SHAKE256) hiding and binding nonces. + /// + /// Note that [`SigningNonces`] must be used *only once* for a signing + /// operation; re-using nonces will result in leakage of a signer's long-lived + /// signing key. + pub type SigningNonces = frost::round1::SigningNonces; + + /// Published by each participant in the first round of the signing protocol. + /// + /// This step can be batched if desired by the implementation. Each + /// SigningCommitment can be used for exactly *one* signature. + pub type SigningCommitments = frost::round1::SigningCommitments; + + /// A commitment to a signing nonce share. + pub type NonceCommitment = frost::round1::NonceCommitment; + + /// Performed once by each participant selected for the signing operation. + /// + /// Generates the signing nonces and commitments to be used in the signing + /// operation. + pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage; + +/// FROST(Ed448, SHAKE256) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use super::*; + + /// A FROST(Ed448, SHAKE256) participant's signature share, which the Coordinator will aggregate with all other signer's + /// shares into the joint signature. + pub type SignatureShare = frost::round2::SignatureShare; + + /// Performed once by each participant selected for the signing operation. + /// + /// Receives the message to be signed and a set of signing commitments and a set + /// of randomizing commitments to be used in that signing operation, including + /// that for this participant. + /// + /// Assumes the participant has already determined which nonce corresponds with + /// the commitment that was assigned by the coordinator in the SigningPackage. + pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + ) -> Result { + frost::round2::sign(signing_package, signer_nonces, key_package) + } +} + +/// A Schnorr signature on FROST(Ed448, SHAKE256). +pub type Signature = frost_core::Signature; + +/// Verifies each FROST(Ed448, SHAKE256) participant's signature share, and if all are valid, +/// aggregates the shares into a signature to publish. +/// +/// Resulting signature is compatible with verification of a plain Schnorr +/// signature. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, +) -> Result { + frost::aggregate(signing_package, signature_shares, pubkeys) +} + +/// A signing key for a Schnorr signature on FROST(Ed448, SHAKE256). +pub type SigningKey = frost_core::SigningKey; + +/// A valid verifying key for Schnorr signatures on FROST(Ed448, SHAKE256). +pub type VerifyingKey = frost_core::VerifyingKey; diff --git a/frost/frost-ed448/src/types.rs b/frost/frost-ed448/src/types.rs new file mode 100644 index 00000000..1d177083 --- /dev/null +++ b/frost/frost-ed448/src/types.rs @@ -0,0 +1,146 @@ +use core::ops::{Add, Mul, Neg, Sub}; + +use ed448_goldilocks::{CompressedEdwardsY, EdwardsPoint, Scalar, ScalarBytes}; +use parity_scale_codec::{Decode, Encode}; +use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable}; + +/// A wrapper around a [`ed448_goldilocks::Scalar`] to implement the [`Encode`,`Decode`] +/// traits. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct WrappedScalar(pub Scalar); + +impl Encode for WrappedScalar { + fn size_hint(&self) -> usize { + 56 + } + + fn encode_to(&self, dest: &mut W) { + dest.write(self.0.to_bytes().as_ref()); + } +} + +impl Decode for WrappedScalar { + fn decode( + input: &mut I, + ) -> Result { + let mut bytes = [0u8; 56]; + input.read(&mut bytes)?; + let buffer = ScalarBytes::from_slice(&bytes); + Ok(WrappedScalar(Scalar::from_canonical_bytes(buffer).unwrap_or(Scalar::ZERO))) + } +} + +impl Sub for WrappedScalar { + type Output = WrappedScalar; + + fn sub(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 - rhs.0) + } +} + +impl Add for WrappedScalar { + type Output = WrappedScalar; + + fn add(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 + rhs.0) + } +} + +impl Mul for WrappedScalar { + type Output = WrappedScalar; + + fn mul(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 * rhs.0) + } +} + +impl Neg for WrappedScalar { + type Output = WrappedScalar; + + fn neg(self) -> WrappedScalar { + WrappedScalar(-self.0) + } +} + +impl ConditionallySelectable for WrappedScalar { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + WrappedScalar(Scalar::conditional_select(&a.0, &b.0, choice)) + } +} + +impl ConditionallyNegatable for WrappedScalar { + fn conditional_negate(&mut self, choice: Choice) { + self.0.conditional_negate(choice); + } +} + +/// A wrapper around a [`curve25519_dalek::edwards::EdwardsPoint`] to implement the +/// [`Encode`,`Decode`] traits. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct WrappedEdwardsPoint(pub EdwardsPoint); + +impl Encode for WrappedEdwardsPoint { + fn size_hint(&self) -> usize { + 57 + } + + fn encode_to(&self, dest: &mut W) { + dest.write(self.0.compress().as_bytes()); + } +} + +impl Decode for WrappedEdwardsPoint { + fn decode( + input: &mut I, + ) -> Result { + let mut bytes = [0u8; 57]; + input.read(&mut bytes)?; + Ok(WrappedEdwardsPoint( + CompressedEdwardsY(bytes).decompress().unwrap_or(EdwardsPoint::default()), + )) + } +} + +impl Sub for WrappedEdwardsPoint { + type Output = WrappedEdwardsPoint; + + fn sub(self, rhs: WrappedEdwardsPoint) -> WrappedEdwardsPoint { + WrappedEdwardsPoint(self.0 - rhs.0) + } +} + +impl Add for WrappedEdwardsPoint { + type Output = WrappedEdwardsPoint; + + fn add(self, rhs: WrappedEdwardsPoint) -> WrappedEdwardsPoint { + WrappedEdwardsPoint(self.0 + rhs.0) + } +} + +impl Mul for WrappedEdwardsPoint { + type Output = WrappedEdwardsPoint; + + fn mul(self, rhs: WrappedScalar) -> WrappedEdwardsPoint { + WrappedEdwardsPoint(self.0 * rhs.0) + } +} + +impl Neg for WrappedEdwardsPoint { + type Output = WrappedEdwardsPoint; + + fn neg(self) -> WrappedEdwardsPoint { + WrappedEdwardsPoint(-self.0) + } +} + +impl ConditionallySelectable for WrappedEdwardsPoint { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + WrappedEdwardsPoint(EdwardsPoint::conditional_select(&a.0, &b.0, choice)) + } +} + +impl ConditionallyNegatable for WrappedEdwardsPoint { + fn conditional_negate(&mut self, choice: Choice) { + self.0.conditional_negate(choice); + } +} diff --git a/frost/frost-p384/Cargo.toml b/frost/frost-p384/Cargo.toml new file mode 100644 index 00000000..a9c7027f --- /dev/null +++ b/frost/frost-p384/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "frost-p384" +edition = "2021" +# When releasing to crates.io: +# - Update html_root_url +# - Update CHANGELOG.md +# - Create git tag. +version.workspace = true +authors = [ + "Deirdre Connolly ", + "Chelsea Komlo ", + "Conrado Gouvea " +] +readme = "README.md" +license = "MIT OR Apache-2.0" +categories = ["cryptography"] +keywords = ["cryptography", "crypto", "ed25519", "threshold", "signature"] +description = "A Schnorr signature scheme over Ed25519 that supports FROST." + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +parity-scale-codec = { workspace = true } +p384 = { version = "0.13.0", features = ["hash2curve", "alloc"], git = "https://github.com/LIT-Protocol/elliptic-curves.git", default-features = false } +tg-frost-core = { workspace = true, features = ["internals"] } +sp-std = { workspace = true } +rand_core = { workspace = true } +sha2 = { workspace = true } +subtle = { workspace = true } + +[features] +default = ["std"] +std = [ + "sp-std/std", + "tg-frost-core/std", + "parity-scale-codec/std", + "rand_core/std", + "p384/std" +] \ No newline at end of file diff --git a/frost/frost-p384/src/lib.rs b/frost/frost-p384/src/lib.rs new file mode 100644 index 00000000..9a894fc7 --- /dev/null +++ b/frost/frost-p384/src/lib.rs @@ -0,0 +1,422 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + +use sp_std::collections::btree_map::BTreeMap; + +use p384::{ + elliptic_curve::{ + hash2curve::{hash_to_field, ExpandMsgXmd}, + sec1::{FromEncodedPoint, ToEncodedPoint}, + Field as FFField, PrimeField, + }, + AffinePoint, ProjectivePoint, Scalar, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha384}; +use sp_std::prelude::ToOwned; + +use frost_core as frost; +use tg_frost_core as frost_core; + +// Re-exports in our public API +pub use frost_core::{Ciphersuite, Field, FieldError, Group, GroupError}; +pub use rand_core; + +/// An error. +pub type Error = frost_core::Error; + +/// An implementation of the FROST(P-384, SHA-384) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct P384ScalarField; + +impl Field for P384ScalarField { + type Scalar = Scalar; + + type Serialization = [u8; 48]; + + fn zero() -> Self::Scalar { + Scalar::ZERO + } + + fn one() -> Self::Scalar { + Scalar::ONE + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`p384::Scalar`]'s Eq/PartialEq does a constant-time comparison using + // `ConstantTimeEq` + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(scalar.invert().unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes().into() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let field_bytes: &p384::FieldBytes = buf.into(); + match Scalar::from_repr(*field_bytes).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + let mut array = Self::serialize(scalar); + array.reverse(); + array + } +} + +/// An implementation of the FROST(P-384, SHA-384) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct P384Group; + +impl Group for P384Group { + type Field = P384ScalarField; + + type Element = ProjectivePoint; + + /// [SEC 1][1] serialization of a compressed point in P-384 takes 49 bytes + /// (1-byte prefix and 48 bytes for the coordinate). + /// + /// Note that, in the P-384 spec, the identity is encoded as a single null byte; + /// but here we pad with zeroes. This is acceptable as the identity _should_ never + /// be serialized in FROST, else we error. + /// + /// [1]: https://secg.org/sec1-v2.pdf + type Serialization = [u8; 49]; + + fn cofactor() -> ::Scalar { + Scalar::ONE + } + + fn identity() -> Self::Element { + ProjectivePoint::IDENTITY + } + + fn generator() -> Self::Element { + ProjectivePoint::GENERATOR + } + + fn serialize(element: &Self::Element) -> Result { + let mut fixed_serialized = [0; 49]; + let serialized_point = element.to_encoded_point(true); + let serialized = serialized_point.as_bytes(); + // Sanity check; either it takes all bytes or a single byte (identity). + if !(serialized.len() == fixed_serialized.len() || serialized.len() == 1) { + return Err(GroupError::MalformedElement); + } + // Copy to the left of the buffer (i.e. pad the identity with zeroes). + // Note that identity elements shouldn't be serialized in FROST, but we + // do this padding so that this function doesn't have to return an error. + // If this encodes the identity, it will fail when deserializing. + { + let (left, _right) = fixed_serialized.split_at_mut(serialized.len()); + left.copy_from_slice(serialized); + } + Ok(fixed_serialized) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let encoded_point = + p384::EncodedPoint::from_bytes(buf).map_err(|_| GroupError::MalformedElement)?; + + match Option::::from(AffinePoint::from_encoded_point(&encoded_point)) { + Some(point) => { + if point.is_identity().into() { + // This is actually impossible since the identity is encoded in a single byte + // which will never happen since we receive a 33-byte buffer. + // We leave the check for consistency. + Err(GroupError::InvalidIdentityElement) + } else { + Ok(ProjectivePoint::from(point)) + } + }, + None => Err(GroupError::MalformedElement), + } + } +} + +fn hash_to_array(inputs: &[&[u8]]) -> [u8; 48] { + let mut h = Sha384::new(); + for i in inputs { + h.update(i); + } + let mut output = [0u8; 48]; + output.copy_from_slice(h.finalize().as_slice()); + output +} + +fn hash_to_scalar(domain: &[u8], msg: &[u8]) -> Scalar { + let mut u = [P384ScalarField::zero()]; + hash_to_field::, Scalar>(&[msg], &[domain], &mut u) + .expect("should never return error according to error cases described in ExpandMsgXmd"); + u[0] +} + +/// Context string from the ciphersuite in the [spec] +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-1 +const CONTEXT_STRING: &str = "FROST-P384-SHA384-v1"; + +/// An implementation of the FROST(P-384, SHA-384) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct P384Sha384; + +impl Ciphersuite for P384Sha384 { + const ID: &'static str = CONTEXT_STRING; + + type Group = P384Group; + + type HashOutput = [u8; 48]; + + type SignatureSerialization = [u8; 97]; + + /// H1 for FROST(P-384, SHA-384) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.1 + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar((CONTEXT_STRING.to_owned() + "rho").as_bytes(), m) + } + + /// H2 for FROST(P-384, SHA-384) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.2 + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar((CONTEXT_STRING.to_owned() + "chal").as_bytes(), m) + } + + /// H3 for FROST(P-384, SHA-384) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.3 + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar((CONTEXT_STRING.to_owned() + "nonce").as_bytes(), m) + } + + /// H4 for FROST(P-384, SHA-384) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.4 + fn H4(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) + } + + /// H5 for FROST(P-384, SHA-384) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.4-2.2.2.5 + fn H5(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) + } + + /// HDKG for FROST(P-384, SHA-384) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar((CONTEXT_STRING.to_owned() + "dkg").as_bytes(), m)) + } + + /// HID for FROST(P-384, SHA-384) + fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar((CONTEXT_STRING.to_owned() + "id").as_bytes(), m)) + } +} + +// Shorthand alias for the ciphersuite +type P = P384Sha384; + +/// A FROST(P-384, SHA-384) participant identifier. +pub type Identifier = frost::Identifier

; +/// FROST(P-384, SHA-384) keys, key generation, key shares. +pub mod keys { + use sp_std::collections::btree_map::BTreeMap; + + use super::*; + + /// The identifier list to use when generating key shares. + pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, P>; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + mut rng: RNG, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) + } + + /// Splits an existing key into FROST shares. + /// + /// This is identical to [`generate_with_dealer`] but receives an existing key + /// instead of generating a fresh one. This is useful in scenarios where + /// the key needs to be generated externally or must be derived from e.g. a + /// seed phrase. + pub fn split( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::split(secret, max_signers, min_signers, identifiers, rng) + } + + /// Recompute the secret from t-of-n secret shares using Lagrange interpolation. + /// + /// This can be used if for some reason the original key must be restored; e.g. + /// if threshold signing is not required anymore. + /// + /// This is NOT required to sign with FROST; the whole point of FROST is being + /// able to generate signatures only using the shares, without having to + /// reconstruct the original key. + /// + /// The caller is responsible for providing at least `min_signers` shares; + /// if less than that is provided, a different key will be returned. + pub fn reconstruct(secret_shares: &[KeyPackage]) -> Result { + frost::keys::reconstruct(secret_shares) + } + + /// Secret and public key material generated by a dealer performing + /// [`generate_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(P-384, SHA-384) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare

; + + /// A secret scalar value representing a signer's share of the group secret. + pub type SigningShare = frost::keys::SigningShare

; + + /// A public group element that represents a single signer's public verification share. + pub type VerifyingShare = frost::keys::VerifyingShare

; + + /// A FROST(P-384, SHA-384) keypair, which can be generated either by a trusted dealer or using + /// a DKG. + /// + /// When using a central dealer, [`SecretShare`]s are distributed to + /// participants, who then perform verification, before deriving + /// [`KeyPackage`]s, which they store to later use during signing. + pub type KeyPackage = frost::keys::KeyPackage

; + + /// Public data that contains all the signers' public keys as well as the + /// group public key. + /// + /// Used for verification purposes before publishing a signature. + pub type PublicKeyPackage = frost::keys::PublicKeyPackage

; + + /// Contains the commitments to the coefficients for our secret polynomial _f_, + /// used to generate participants' key shares. + /// + /// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which + /// themselves are scalars) for a secret polynomial f, where f is used to + /// generate each ith participant's key share f(i). Participants use this set of + /// commitments to perform verifiable secret sharing. + /// + /// Note that participants MUST be assured that they have the *same* + /// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using + /// some agreed-upon public location for publication, where each participant can + /// ensure that they received the correct (and same) value. + pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment

; +} + +/// FROST(P-384, SHA-384) Round 1 functionality and types. +pub mod round1 { + use crate::keys::SigningShare; + + use super::*; + + /// Comprised of FROST(P-384, SHA-384) hiding and binding nonces. + /// + /// Note that [`SigningNonces`] must be used *only once* for a signing + /// operation; re-using nonces will result in leakage of a signer's long-lived + /// signing key. + pub type SigningNonces = frost::round1::SigningNonces

; + + /// Published by each participant in the first round of the signing protocol. + /// + /// This step can be batched if desired by the implementation. Each + /// SigningCommitment can be used for exactly *one* signature. + pub type SigningCommitments = frost::round1::SigningCommitments

; + + /// A commitment to a signing nonce share. + pub type NonceCommitment = frost::round1::NonceCommitment

; + + /// Performed once by each participant selected for the signing operation. + /// + /// Generates the signing nonces and commitments to be used in the signing + /// operation. + pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage

; + +/// FROST(P-384, SHA-384) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use super::*; + + /// A FROST(P-384, SHA-384) participant's signature share, which the Coordinator will aggregate with all other signer's + /// shares into the joint signature. + pub type SignatureShare = frost::round2::SignatureShare

; + + /// Performed once by each participant selected for the signing operation. + /// + /// Receives the message to be signed and a set of signing commitments and a set + /// of randomizing commitments to be used in that signing operation, including + /// that for this participant. + /// + /// Assumes the participant has already determined which nonce corresponds with + /// the commitment that was assigned by the coordinator in the SigningPackage. + pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + ) -> Result { + frost::round2::sign(signing_package, signer_nonces, key_package) + } +} + +/// A Schnorr signature on FROST(P-384, SHA-384). +pub type Signature = frost_core::Signature

; + +/// Verifies each FROST(P-384, SHA-384) participant's signature share, and if all are valid, +/// aggregates the shares into a signature to publish. +/// +/// Resulting signature is compatible with verification of a plain Schnorr +/// signature. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + pubkeys: &keys::PublicKeyPackage, +) -> Result { + frost::aggregate(signing_package, signature_shares, pubkeys) +} + +/// A signing key for a Schnorr signature on FROST(P-384, SHA-384). +pub type SigningKey = frost_core::SigningKey

; + +/// A valid verifying key for Schnorr signatures on FROST(P-384, SHA-384). +pub type VerifyingKey = frost_core::VerifyingKey

; diff --git a/frost/frost-p384/src/types.rs b/frost/frost-p384/src/types.rs new file mode 100644 index 00000000..ef86092f --- /dev/null +++ b/frost/frost-p384/src/types.rs @@ -0,0 +1,155 @@ +use core::ops::{Add, Mul, Neg, Sub}; + +use p384::{ + elliptic_curve::{ + sec1::{FromEncodedPoint, ToEncodedPoint}, + PrimeField, + }, + EncodedPoint, FieldBytes, ProjectivePoint, Scalar, +}; +use parity_scale_codec::{Decode, Encode}; +use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable}; + +/// A wrapper around a [`p384::Scalar`] to implement the [`Encode`,`Decode`] +/// traits. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct WrappedScalar(pub Scalar); + +impl Encode for WrappedScalar { + fn size_hint(&self) -> usize { + 48 + } + + fn encode_to(&self, dest: &mut W) { + dest.write(self.0.to_repr().encode().as_ref()); + } +} + +impl Decode for WrappedScalar { + fn decode( + input: &mut I, + ) -> Result { + let mut bytes = [0u8; 32]; + input.read(&mut bytes)?; + Ok(WrappedScalar( + Scalar::from_repr(*FieldBytes::from_slice(&bytes)).unwrap_or(Scalar::ZERO), + )) + } +} + +impl Sub for WrappedScalar { + type Output = WrappedScalar; + + fn sub(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 - rhs.0) + } +} + +impl Add for WrappedScalar { + type Output = WrappedScalar; + + fn add(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 + rhs.0) + } +} + +impl Mul for WrappedScalar { + type Output = WrappedScalar; + + fn mul(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 * rhs.0) + } +} + +impl Neg for WrappedScalar { + type Output = WrappedScalar; + + fn neg(self) -> WrappedScalar { + WrappedScalar(-self.0) + } +} + +impl ConditionallySelectable for WrappedScalar { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + WrappedScalar(Scalar::conditional_select(&a.0, &b.0, choice)) + } +} + +impl ConditionallyNegatable for WrappedScalar { + fn conditional_negate(&mut self, choice: Choice) { + self.0.conditional_negate(choice); + } +} + +/// A wrapper around a [`p384::ProjectivePoint`] to implement the +/// [`Encode`,`Decode`] traits. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct WrappedProjectivePoint(pub ProjectivePoint); + +impl Encode for WrappedProjectivePoint { + fn size_hint(&self) -> usize { + 49 + } + + fn encode_to(&self, dest: &mut W) { + dest.write(self.0.to_encoded_point(true).as_bytes()); + } +} + +impl Decode for WrappedProjectivePoint { + fn decode( + input: &mut I, + ) -> Result { + let mut bytes = [0u8; 32]; + input.read(&mut bytes)?; + let pt = ProjectivePoint::from_encoded_point( + &EncodedPoint::from_bytes(bytes).unwrap_or_default(), + ) + .unwrap_or(ProjectivePoint::default()); + Ok(WrappedProjectivePoint(pt)) + } +} + +impl Sub for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn sub(self, rhs: WrappedProjectivePoint) -> WrappedProjectivePoint { + WrappedProjectivePoint(self.0 - rhs.0) + } +} + +impl Add for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn add(self, rhs: WrappedProjectivePoint) -> WrappedProjectivePoint { + WrappedProjectivePoint(self.0 + rhs.0) + } +} + +impl Mul for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn mul(self, rhs: WrappedScalar) -> WrappedProjectivePoint { + WrappedProjectivePoint(self.0 * rhs.0) + } +} + +impl Neg for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn neg(self) -> WrappedProjectivePoint { + WrappedProjectivePoint(-self.0) + } +} + +impl ConditionallySelectable for WrappedProjectivePoint { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + WrappedProjectivePoint(ProjectivePoint::conditional_select(&a.0, &b.0, choice)) + } +} + +impl ConditionallyNegatable for WrappedProjectivePoint { + fn conditional_negate(&mut self, choice: Choice) { + self.0.conditional_negate(choice); + } +} diff --git a/frost/frost-secp256k1-tr/Cargo.toml b/frost/frost-secp256k1-tr/Cargo.toml new file mode 100644 index 00000000..4b12fd3a --- /dev/null +++ b/frost/frost-secp256k1-tr/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "frost-secp256k1-tr" +edition = "2021" +# When releasing to crates.io: +# - Update html_root_url +# - Update CHANGELOG.md +# - Create git tag. +version.workspace = true +authors = [ + "Deirdre Connolly ", + "Chelsea Komlo ", + "Conrado Gouvea " +] +readme = "README.md" +license = "MIT OR Apache-2.0" +categories = ["cryptography"] +keywords = ["cryptography", "crypto", "ed25519", "threshold", "signature"] +description = "A Schnorr signature scheme over secp256k1 that supports FROST." + +[package.metadata.docs.rs] +features = ["serde"] +rustdoc-args = ["--cfg", "docsrs"] + +[dependencies] +parity-scale-codec = { workspace = true } +k256 = { workspace = true, default-features = false, features = ["arithmetic", "schnorr", "expose-field", "hash2curve", "alloc", "pkcs8"] } +tg-frost-core = { workspace = true, features = ["internals"] } +sp-std = { workspace = true } +rand_core = { workspace = true } +sha2 = { workspace = true } +signature = { workspace = true } +subtle = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "rand_core/std", + "tg-frost-core/std", + "sp-std/std", + "k256/std", + "sha2/std", +] diff --git a/frost/frost-secp256k1-tr/src/lib.rs b/frost/frost-secp256k1-tr/src/lib.rs new file mode 100644 index 00000000..987aec8e --- /dev/null +++ b/frost/frost-secp256k1-tr/src/lib.rs @@ -0,0 +1,877 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] + +use sp_std::vec; +use sp_std::{borrow::Cow, collections::btree_map::BTreeMap, vec::Vec}; + +use k256::elliptic_curve::ops::Reduce; +use k256::{ + elliptic_curve::{ + bigint::U256, + group::prime::PrimeCurveAffine, + hash2curve::{hash_to_field, ExpandMsgXmd}, + point::AffineCoordinates, + sec1::{FromEncodedPoint, ToEncodedPoint}, + Field as FFField, PrimeField, + }, + AffinePoint, ProjectivePoint, Scalar, +}; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +use tg_frost_core as frost_core; + +use frost_core::{self as frost, random_nonzero}; + +use keys::EvenY; +use keys::Tweak; + +// Re-exports in our public API +pub use frost_core::{ + Challenge, Ciphersuite, Element, Field, FieldError, Group, GroupCommitment, GroupError, +}; +pub use rand_core; + +/// An error. +pub type Error = frost_core::Error; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite scalar field. +#[derive(Clone, Copy)] +pub struct Secp256K1ScalarField; + +impl Field for Secp256K1ScalarField { + type Scalar = Scalar; + + type Serialization = [u8; 32]; + + fn zero() -> Self::Scalar { + Scalar::ZERO + } + + fn one() -> Self::Scalar { + Scalar::ONE + } + + fn invert(scalar: &Self::Scalar) -> Result { + // [`Scalar`]'s Eq/PartialEq does a constant-time comparison + if *scalar == ::zero() { + Err(FieldError::InvalidZeroScalar) + } else { + Ok(scalar.invert().unwrap()) + } + } + + fn random(rng: &mut R) -> Self::Scalar { + Scalar::random(rng) + } + + fn serialize(scalar: &Self::Scalar) -> Self::Serialization { + scalar.to_bytes().into() + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let field_bytes: &k256::FieldBytes = buf.into(); + match Scalar::from_repr(*field_bytes).into() { + Some(s) => Ok(s), + None => Err(FieldError::MalformedScalar), + } + } + + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization { + let mut array = Self::serialize(scalar); + array.reverse(); + array + } +} + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite group. +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct Secp256K1Group; + +impl Group for Secp256K1Group { + type Field = Secp256K1ScalarField; + + type Element = ProjectivePoint; + + /// [SEC 1][1] serialization of a compressed point in secp256k1 takes 33 bytes + /// (1-byte prefix and 32 bytes for the coordinate). + /// + /// Note that, in the SEC 1 spec, the identity is encoded as a single null byte; + /// but here we pad with zeroes. This is acceptable as the identity _should_ never + /// be serialized in FROST, else we error. + /// + /// [1]: https://secg.org/sec1-v2.pdf + type Serialization = [u8; 33]; + + fn cofactor() -> ::Scalar { + Scalar::ONE + } + + fn identity() -> Self::Element { + ProjectivePoint::IDENTITY + } + + fn generator() -> Self::Element { + ProjectivePoint::GENERATOR + } + + fn serialize(element: &Self::Element) -> Result { + if *element == Self::identity() { + return Err(GroupError::InvalidIdentityElement); + } + let mut fixed_serialized = [0; 33]; + let serialized_point = element.to_affine().to_encoded_point(true); + let serialized = serialized_point.as_bytes(); + fixed_serialized.copy_from_slice(serialized); + Ok(fixed_serialized) + } + + fn deserialize(buf: &Self::Serialization) -> Result { + let encoded_point = + k256::EncodedPoint::from_bytes(buf).map_err(|_| GroupError::MalformedElement)?; + + match Option::::from(AffinePoint::from_encoded_point(&encoded_point)) { + Some(point) => { + if point.is_identity().into() { + // This is actually impossible since the identity is encoded a a single byte + // which will never happen since we receive a 33-byte buffer. + // We leave the check for consistency. + Err(GroupError::InvalidIdentityElement) + } else { + Ok(ProjectivePoint::from(point)) + } + }, + None => Err(GroupError::MalformedElement), + } + } +} + +fn hash_to_array(inputs: &[&[u8]]) -> [u8; 32] { + let mut h = Sha256::new(); + for i in inputs { + h.update(i); + } + let mut output = [0u8; 32]; + output.copy_from_slice(h.finalize().as_slice()); + output +} + +fn hash_to_scalar(domain: &[&[u8]], msg: &[u8]) -> Scalar { + let mut u = [Secp256K1ScalarField::zero()]; + hash_to_field::, Scalar>(&[msg], domain, &mut u) + .expect("should never return error according to error cases described in ExpandMsgXmd"); + u[0] +} + +/// Context string from the ciphersuite in the [spec]. +/// +/// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-1 +const CONTEXT_STRING: &str = "FROST-secp256k1-SHA256-TR-v1"; + +/// An implementation of the FROST(secp256k1, SHA-256) ciphersuite. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct Secp256K1Sha256TR; + +/// Digest the hasher to a Scalar +fn hasher_to_scalar(hasher: Sha256) -> Scalar { + // This is acceptable because secp256k1 curve order is close to 2^256, + // and the input is uniformly random since it is a hash output, therefore + // the bias is negligibly small. + Scalar::reduce(U256::from_be_slice(&hasher.finalize())) +} + +/// Create a BIP340 compliant tagged hash +fn tagged_hash(tag: &str) -> Sha256 { + let mut hasher = Sha256::new(); + let mut tag_hasher = Sha256::new(); + tag_hasher.update(tag.as_bytes()); + let tag_hash = tag_hasher.finalize(); + hasher.update(tag_hash); + hasher.update(tag_hash); + hasher +} + +/// Create a BIP341 compliant taproot tweak +fn tweak>( + public_key: &<::Group as Group>::Element, + merkle_root: Option, +) -> Scalar { + match merkle_root { + None => Secp256K1ScalarField::zero(), + Some(root) => { + let mut hasher = tagged_hash("TapTweak"); + hasher.update(public_key.to_affine().x()); + hasher.update(root.as_ref()); + hasher_to_scalar(hasher) + }, + } +} + +// Negate a Nonce +fn negate_nonce(nonce: &frost_core::round1::Nonce) -> frost_core::round1::Nonce { + frost_core::round1::Nonce::::from_scalar(-nonce.to_scalar()) +} + +// Negate a SigningNonces +fn negate_nonces(signing_nonces: &round1::SigningNonces) -> round1::SigningNonces { + // TODO: this recomputes commitments which is expensive, and not needed. + // Create an `internals` SigningNonces::from_nonces_and_commitments or + // something similar. + round1::SigningNonces::from_nonces( + negate_nonce(signing_nonces.hiding()), + negate_nonce(signing_nonces.binding()), + ) +} + +impl Ciphersuite for Secp256K1Sha256TR { + const ID: &'static str = CONTEXT_STRING; + + type Group = Secp256K1Group; + + type HashOutput = [u8; 32]; + + type SignatureSerialization = [u8; 64]; + + /// H1 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.1 + fn H1(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"rho"], m) + } + + /// H2 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.2 + fn H2(m: &[u8]) -> <::Field as Field>::Scalar { + let mut hasher = tagged_hash("BIP0340/challenge"); + hasher.update(m); + hasher_to_scalar(hasher) + } + + /// H3 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.3 + fn H3(m: &[u8]) -> <::Field as Field>::Scalar { + hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"nonce"], m) + } + + /// H4 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.4 + fn H4(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"msg", m]) + } + + /// H5 for FROST(secp256k1, SHA-256) + /// + /// [spec]: https://www.ietf.org/archive/id/draft-irtf-cfrg-frost-14.html#section-6.5-2.2.2.5 + fn H5(m: &[u8]) -> Self::HashOutput { + hash_to_array(&[CONTEXT_STRING.as_bytes(), b"com", m]) + } + + /// HDKG for FROST(secp256k1, SHA-256) + fn HDKG(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"dkg"], m)) + } + + /// HID for FROST(secp256k1, SHA-256) + fn HID(m: &[u8]) -> Option<<::Field as Field>::Scalar> { + Some(hash_to_scalar(&[CONTEXT_STRING.as_bytes(), b"id"], m)) + } + + // Sign, negating the key if required by BIP-340. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + let signing_key = signing_key.into_even_y(None); + signing_key.default_sign(rng, message) + } + + // Preprocess sign inputs, negating the keys in the KeyPackage if required + // by BIP-340. + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a keys::KeyPackage, + ) -> Result< + (Cow<'a, SigningPackage>, Cow<'a, round1::SigningNonces>, Cow<'a, keys::KeyPackage>), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Owned(key_package.clone().into_even_y(None)), + )) + } + + // Preprocess sign inputs, negating the keys in the PublicKeyPackage if + // required by BIP-340. + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, + public_key_package: &'a keys::PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap>, + Cow<'a, keys::PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Owned(public_key_package.clone().into_even_y(None)), + )) + } + + // Preprocess verify inputs, negating the VerifyingKey and `signature.R` if required by + // BIP-340. + fn pre_verify<'a>( + message: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result<(Cow<'a, [u8]>, Cow<'a, Signature>, Cow<'a, VerifyingKey>), Error> { + let public_key = public_key.into_even_y(None); + let signature = signature.into_even_y(None); + Ok((Cow::Borrowed(message), Cow::Owned(signature), Cow::Owned(public_key))) + } + + // Generate a nonce, negating it if required by BIP-340. + fn generate_nonce( + rng: &mut R, + ) -> (<::Field as Field>::Scalar, ::Element) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + if R.to_affine().y_is_odd().into() { + (-k, -R) + } else { + (k, R) + } + } + + // Compute the challenge. Per BIP-340, only the X coordinate of R and + // verifying_key are hashed, unlike vanilla FROST. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + let mut preimage = vec![]; + preimage.extend_from_slice(&R.to_affine().x()); + preimage.extend_from_slice(&verifying_key.to_element().to_affine().x()); + preimage.extend_from_slice(message); + Ok(Challenge::from_scalar(S::H2(&preimage[..]))) + } + + /// Compute a signature share, negating the nonces if required by BIP-340. + fn compute_signature_share( + group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: frost::BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &frost::keys::KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + let signer_nonces = if !group_commitment.has_even_y() { + negate_nonces(signer_nonces) + } else { + signer_nonces.clone() + }; + + frost::round2::compute_signature_share( + &signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Verify a signature share, negating the group commitment share if + /// required by BIP-340. + fn verify_share( + group_commitment: &GroupCommitment, + signature_share: &frost_core::round2::SignatureShare, + identifier: Identifier, + group_commitment_share: &frost_core::round1::GroupCommitmentShare, + verifying_share: &frost_core::keys::VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + let group_commitment_share = if !group_commitment.has_even_y() { + frost_core::round1::GroupCommitmentShare::from_element( + -group_commitment_share.to_element(), + ) + } else { + *group_commitment_share + }; + signature_share.verify( + identifier, + &group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Serialize a signature in compact BIP340 format, with an x-only R point. + fn serialize_signature(signature: &Signature) -> Result, Error> { + let R_bytes = Self::Group::serialize(signature.R())?; + let z_bytes = ::Field::serialize(signature.z()); + + let mut bytes = vec![0u8; 64]; + bytes[..32].copy_from_slice(&R_bytes[1..]); + bytes[32..].copy_from_slice(&z_bytes); + Ok(bytes) + } + + /// Deserialize a signature in compact BIP340 format, with an x-only R point. + fn deserialize_signature(bytes: &[u8]) -> Result { + if bytes.len() != 64 { + return Err(Error::MalformedSignature); + } + + let mut R_bytes = [0u8; 33]; + R_bytes[0] = 0x02; // taproot signatures always have an even R point + R_bytes[1..].copy_from_slice(&bytes[..32]); + + let mut z_bytes = [0u8; 32]; + z_bytes.copy_from_slice(&bytes[32..]); + + let R = Self::Group::deserialize(&R_bytes)?; + let z = ::Field::deserialize(&z_bytes)?; + + Ok(Signature::new(R, z)) + } + + /// Post-process the DKG output. We add an unusable taproot tweak to the + /// group key computed by a DKG run, to prevent peers from inserting rogue + /// tapscript tweaks into the group's joint public key. + fn post_dkg( + key_package: keys::KeyPackage, + public_key_package: keys::PublicKeyPackage, + ) -> Result<(keys::KeyPackage, keys::PublicKeyPackage), Error> { + // From BIP-341: + // > If the spending conditions do not require a script path, the output + // > key should commit to an unspendable script path instead of having + // > no script path. This can be achieved by computing the output key + // > point as Q = P + int(hashTapTweak(bytes(P)))G. + let merkle_root = [0u8; 0]; + Ok((key_package.tweak(Some(&merkle_root)), public_key_package.tweak(Some(&merkle_root)))) + } +} + +type S = Secp256K1Sha256TR; + +/// A FROST(secp256k1, SHA-256) participant identifier. +pub type Identifier = frost::Identifier; + +/// FROST(secp256k1, SHA-256) keys, key generation, key shares. +pub mod keys { + use super::*; + + /// The identifier list to use when generating key shares. + pub type IdentifierList<'a> = frost::keys::IdentifierList<'a, S>; + + /// Allows all participants' keys to be generated using a central, trusted + /// dealer. + pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + mut rng: RNG, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::generate_with_dealer(max_signers, min_signers, identifiers, &mut rng) + } + + /// Splits an existing key into FROST shares. + /// + /// This is identical to [`generate_with_dealer`] but receives an existing key + /// instead of generating a fresh one. This is useful in scenarios where + /// the key needs to be generated externally or must be derived from e.g. a + /// seed phrase. + pub fn split( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, + ) -> Result<(BTreeMap, PublicKeyPackage), Error> { + frost::keys::split(secret, max_signers, min_signers, identifiers, rng) + } + + /// Recompute the secret from t-of-n secret shares using Lagrange interpolation. + /// + /// This can be used if for some reason the original key must be restored; e.g. + /// if threshold signing is not required anymore. + /// + /// This is NOT required to sign with FROST; the whole point of FROST is being + /// able to generate signatures only using the shares, without having to + /// reconstruct the original key. + /// + /// The caller is responsible for providing at least `min_signers` shares; + /// if less than that is provided, a different key will be returned. + pub fn reconstruct(secret_shares: &[KeyPackage]) -> Result { + frost::keys::reconstruct(secret_shares) + } + + /// Secret and public key material generated by a dealer performing + /// [`generate_with_dealer`]. + /// + /// # Security + /// + /// To derive a FROST(secp256k1, SHA-256) keypair, the receiver of the [`SecretShare`] *must* call + /// .into(), which under the hood also performs validation. + pub type SecretShare = frost::keys::SecretShare; + + /// A secret scalar value representing a signer's share of the group secret. + pub type SigningShare = frost::keys::SigningShare; + + /// A public group element that represents a single signer's public verification share. + pub type VerifyingShare = frost::keys::VerifyingShare; + + /// A FROST(secp256k1, SHA-256) keypair, which can be generated either by a trusted dealer or using + /// a DKG. + /// + /// When using a central dealer, [`SecretShare`]s are distributed to + /// participants, who then perform verification, before deriving + /// [`KeyPackage`]s, which they store to later use during signing. + pub type KeyPackage = frost::keys::KeyPackage; + + /// Public data that contains all the signers' public keys as well as the + /// group public key. + /// + /// Used for verification purposes before publishing a signature. + pub type PublicKeyPackage = frost::keys::PublicKeyPackage; + + /// Contains the commitments to the coefficients for our secret polynomial _f_, + /// used to generate participants' key shares. + /// + /// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which + /// themselves are scalars) for a secret polynomial f, where f is used to + /// generate each ith participant's key share f(i). Participants use this set of + /// commitments to perform verifiable secret sharing. + /// + /// Note that participants MUST be assured that they have the *same* + /// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using + /// some agreed-upon public location for publication, where each participant can + /// ensure that they received the correct (and same) value. + pub type VerifiableSecretSharingCommitment = frost::keys::VerifiableSecretSharingCommitment; + + /// Trait for ensuring the group public key has an even Y coordinate. + /// + /// In BIP-320, public keys are encoded with only the X coordinate, which + /// means that two Y coordinates are possible. The specification says that + /// the coordinate which is even must be used. Alternatively, something + /// equivalent can be accomplished by simply converting any existing + /// (non-encoded) public key to have an even Y coordinate. + /// + /// This trait is used to enable this procedure, by changing the private and + /// public keys to ensure that the public key has a even Y coordinate. This + /// is done by simply negating both keys if Y is even (in a field, negating + /// is equivalent to computing p - x where p is the prime modulus. Since p + /// is odd, if x is odd then the result will be even). Fortunately this + /// works even after Shamir secret sharing, in the individual signing and + /// verifying shares, since it's linear. + pub trait EvenY { + /// Return if the given type has a group public key with an even Y + /// coordinate. + fn has_even_y(&self) -> bool; + + /// Convert the given type to make sure the group public key has an even + /// Y coordinate. `is_even` can be specified if evenness was already + /// determined beforehand. + fn into_even_y(self, is_even: Option) -> Self; + } + + impl EvenY for PublicKeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate verifying key + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = self + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(-vs.to_element()); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new(verifying_shares, verifying_key) + } else { + self + } + } + } + + impl EvenY for KeyPackage { + fn has_even_y(&self) -> bool { + let verifying_key = self.verifying_key(); + (!verifying_key.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + // Negate all components + let verifying_key = VerifyingKey::new(-self.verifying_key().to_element()); + let signing_share = SigningShare::new(-self.signing_share().to_scalar()); + let verifying_share = VerifyingShare::new(-self.verifying_share().to_element()); + KeyPackage::new( + *self.identifier(), + signing_share, + verifying_share, + verifying_key, + *self.min_signers(), + ) + } else { + self + } + } + } + + impl EvenY for VerifyingKey { + fn has_even_y(&self) -> bool { + (!self.to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + VerifyingKey::new(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for GroupCommitment { + fn has_even_y(&self) -> bool { + (!self.clone().to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::from_element(-self.to_element()) + } else { + self + } + } + } + + impl EvenY for Signature { + fn has_even_y(&self) -> bool { + (!self.R().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + Self::new(-*self.R(), *self.z()) + } else { + self + } + } + } + + impl EvenY for SigningKey { + fn has_even_y(&self) -> bool { + (!Into::::into(self).to_element().to_affine().y_is_odd()).into() + } + + fn into_even_y(self, is_even: Option) -> Self { + let is_even = is_even.unwrap_or_else(|| self.has_even_y()); + if !is_even { + SigningKey::from_scalar(-self.to_scalar()) + .expect("the original SigningKey must be nonzero") + } else { + self + } + } + } + + /// Trait for tweaking a key component following BIP-341 + pub trait Tweak: EvenY { + /// Convert the given type to add a tweak. + fn tweak>(self, merkle_root: Option) -> Self; + } + + impl Tweak for PublicKeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let public_key_package = self.into_even_y(None); + let verifying_key = + VerifyingKey::new(public_key_package.verifying_key().to_element() + tp); + // Recreate verifying share map with negated VerifyingShares + // values. + let verifying_shares: BTreeMap<_, _> = public_key_package + .verifying_shares() + .iter() + .map(|(i, vs)| { + let vs = VerifyingShare::new(vs.to_element() + tp); + (*i, vs) + }) + .collect(); + PublicKeyPackage::new(verifying_shares, verifying_key) + } + } + + impl Tweak for KeyPackage { + fn tweak>(self, merkle_root: Option) -> Self { + let t = tweak(&self.verifying_key().to_element(), merkle_root); + let tp = ProjectivePoint::GENERATOR * t; + let key_package = self.into_even_y(None); + let verifying_key = VerifyingKey::new(key_package.verifying_key().to_element() + tp); + let signing_share = SigningShare::new(key_package.signing_share().to_scalar() + t); + let verifying_share = + VerifyingShare::new(key_package.verifying_share().to_element() + tp); + KeyPackage::new( + *key_package.identifier(), + signing_share, + verifying_share, + verifying_key, + *key_package.min_signers(), + ) + } + } +} + +/// FROST(secp256k1, SHA-256) Round 1 functionality and types. +pub mod round1 { + use crate::keys::SigningShare; + + use super::*; + + /// Comprised of FROST(secp256k1, SHA-256) hiding and binding nonces. + /// + /// Note that [`SigningNonces`] must be used *only once* for a signing + /// operation; re-using nonces will result in leakage of a signer's long-lived + /// signing key. + pub type SigningNonces = frost::round1::SigningNonces; + + /// Published by each participant in the first round of the signing protocol. + /// + /// This step can be batched if desired by the implementation. Each + /// SigningCommitment can be used for exactly *one* signature. + pub type SigningCommitments = frost::round1::SigningCommitments; + + /// A commitment to a signing nonce share. + pub type NonceCommitment = frost::round1::NonceCommitment; + + /// Performed once by each participant selected for the signing operation. + /// + /// Generates the signing nonces and commitments to be used in the signing + /// operation. + pub fn commit(secret: &SigningShare, rng: &mut RNG) -> (SigningNonces, SigningCommitments) + where + RNG: CryptoRng + RngCore, + { + frost::round1::commit::(secret, rng) + } +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party. +pub type SigningPackage = frost::SigningPackage; + +/// FROST(secp256k1, SHA-256) Round 2 functionality and types, for signature share generation. +pub mod round2 { + use keys::Tweak; + + use super::*; + + /// A FROST(secp256k1, SHA-256) participant's signature share, which the Coordinator will aggregate with all other signer's + /// shares into the joint signature. + pub type SignatureShare = frost::round2::SignatureShare; + + /// Performed once by each participant selected for the signing operation. + /// + /// Receives the message to be signed and a set of signing commitments and a set + /// of randomizing commitments to be used in that signing operation, including + /// that for this participant. + /// + /// Assumes the participant has already determined which nonce corresponds with + /// the commitment that was assigned by the coordinator in the SigningPackage. + pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + ) -> Result { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + + /// Same as [`sign()`], but using a Taproot tweak as specified in BIP-341. + pub fn sign_with_tweak( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &keys::KeyPackage, + merkle_root: Option<&[u8]>, + ) -> Result { + if merkle_root.is_some() { + let key_package = key_package.clone().tweak(merkle_root); + frost::round2::sign(signing_package, signer_nonces, &key_package) + } else { + frost::round2::sign(signing_package, signer_nonces, key_package) + } + } +} + +/// A Schnorr signature on FROST(secp256k1, SHA-256). +pub type Signature = frost_core::Signature; + +/// Verifies each FROST(secp256k1, SHA-256) participant's signature share, and if all are valid, +/// aggregates the shares into a signature to publish. +/// +/// Resulting signature is compatible with verification of a plain Schnorr +/// signature. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, +) -> Result { + frost::aggregate(signing_package, signature_shares, public_key_package) +} + +/// Same as [`aggregate()`], but using a Taproot tweak as specified in BIP-341. +pub fn aggregate_with_tweak( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, + public_key_package: &keys::PublicKeyPackage, + merkle_root: Option<&[u8]>, +) -> Result { + if merkle_root.is_some() { + let public_key_package = public_key_package.clone().tweak(merkle_root); + frost::aggregate(signing_package, signature_shares, &public_key_package) + } else { + frost::aggregate(signing_package, signature_shares, public_key_package) + } +} + +/// A signing key for a Schnorr signature on FROST(secp256k1, SHA-256). +pub type SigningKey = frost_core::SigningKey; + +/// A valid verifying key for Schnorr signatures on FROST(secp256k1, SHA-256). +pub type VerifyingKey = frost_core::VerifyingKey; diff --git a/frost/frost-secp256k1-tr/src/types.rs b/frost/frost-secp256k1-tr/src/types.rs new file mode 100644 index 00000000..558b4e74 --- /dev/null +++ b/frost/frost-secp256k1-tr/src/types.rs @@ -0,0 +1,150 @@ +use core::ops::{Add, Mul, Neg, Sub}; + +use k256::{ + elliptic_curve::{group::GroupEncoding, sec1::ToEncodedPoint, PrimeField}, + CompressedPoint, FieldBytes, ProjectivePoint, Scalar, +}; +use parity_scale_codec::{Decode, Encode}; +use subtle::{Choice, ConditionallyNegatable, ConditionallySelectable}; + +/// A wrapper around a [`curve25519_dalek::scalar::Scalar`] to implement the [`Encode`,`Decode`] +/// traits. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct WrappedScalar(pub Scalar); + +impl Encode for WrappedScalar { + fn size_hint(&self) -> usize { + 32 + } + + fn encode_to(&self, dest: &mut W) { + dest.write(self.0.to_bytes().as_ref()); + } +} + +impl Decode for WrappedScalar { + fn decode( + input: &mut I, + ) -> Result { + let mut bytes = [0u8; 32]; + input.read(&mut bytes)?; + let buffer = FieldBytes::from_slice(&bytes); + Ok(WrappedScalar(Scalar::from_repr(*buffer).unwrap_or(Scalar::ZERO))) + } +} + +impl Sub for WrappedScalar { + type Output = WrappedScalar; + + fn sub(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 - rhs.0) + } +} + +impl Add for WrappedScalar { + type Output = WrappedScalar; + + fn add(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 + rhs.0) + } +} + +impl Mul for WrappedScalar { + type Output = WrappedScalar; + + fn mul(self, rhs: WrappedScalar) -> WrappedScalar { + WrappedScalar(self.0 * rhs.0) + } +} + +impl Neg for WrappedScalar { + type Output = WrappedScalar; + + fn neg(self) -> WrappedScalar { + WrappedScalar(-self.0) + } +} + +impl ConditionallySelectable for WrappedScalar { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + WrappedScalar(Scalar::conditional_select(&a.0, &b.0, choice)) + } +} + +impl ConditionallyNegatable for WrappedScalar { + fn conditional_negate(&mut self, choice: Choice) { + self.0.conditional_negate(choice); + } +} + +/// A wrapper around a [`k256::ProjectivePoint`] to implement the +/// [`Encode`,`Decode`] traits. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub struct WrappedProjectivePoint(pub ProjectivePoint); + +impl Encode for WrappedProjectivePoint { + fn size_hint(&self) -> usize { + 33 + } + + fn encode_to(&self, dest: &mut W) { + dest.write(self.0.to_encoded_point(true).as_bytes()); + } +} + +impl Decode for WrappedProjectivePoint { + fn decode( + input: &mut I, + ) -> Result { + let mut bytes = [0u8; 33]; + input.read(&mut bytes)?; + let pt = CompressedPoint::from_slice(&bytes); + Ok(WrappedProjectivePoint( + ProjectivePoint::from_bytes(pt).unwrap_or(ProjectivePoint::default()), + )) + } +} + +impl Sub for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn sub(self, rhs: WrappedProjectivePoint) -> WrappedProjectivePoint { + WrappedProjectivePoint(self.0 - rhs.0) + } +} + +impl Add for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn add(self, rhs: WrappedProjectivePoint) -> WrappedProjectivePoint { + WrappedProjectivePoint(self.0 + rhs.0) + } +} + +impl Mul for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn mul(self, rhs: WrappedScalar) -> WrappedProjectivePoint { + WrappedProjectivePoint(self.0 * rhs.0) + } +} + +impl Neg for WrappedProjectivePoint { + type Output = WrappedProjectivePoint; + + fn neg(self) -> WrappedProjectivePoint { + WrappedProjectivePoint(-self.0) + } +} + +impl ConditionallySelectable for WrappedProjectivePoint { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + WrappedProjectivePoint(ProjectivePoint::conditional_select(&a.0, &b.0, choice)) + } +} + +impl ConditionallyNegatable for WrappedProjectivePoint { + fn conditional_negate(&mut self, choice: Choice) { + self.0.conditional_negate(choice); + } +} diff --git a/frost/src/batch.rs b/frost/src/batch.rs new file mode 100644 index 00000000..9678f2e2 --- /dev/null +++ b/frost/src/batch.rs @@ -0,0 +1,160 @@ +//! Performs batch Schnorr signature verification. +//! +//! Batch verification asks whether *all* signatures in some set are valid, +//! rather than asking whether *each* of them is valid. This allows sharing +//! computations among all signature verifications, performing less work overall +//! at the cost of higher latency (the entire batch must complete), complexity +//! of caller code (which must assemble a batch of signatures across +//! work-items), and loss of the ability to easily pinpoint failing signatures. + +use rand_core::{CryptoRng, RngCore}; + +use crate::{scalar_mul::VartimeMultiscalarMul, Ciphersuite, Element, *}; + +/// A batch verification item. +/// +/// This struct exists to allow batch processing to be decoupled from the +/// lifetime of the message. This is useful when using the batch verification +/// API in an async context. +#[derive(Clone, Debug)] +pub struct Item { + vk: VerifyingKey, + sig: Signature, + c: Challenge, +} + +impl Item +where + C: Ciphersuite, +{ + /// Create a new batch [`Item`] from a [`VerifyingKey`], [`Signature`] + /// and a message to be verified. + pub fn new(vk: VerifyingKey, sig: Signature, msg: M) -> Result> + where + M: AsRef<[u8]>, + { + let (msg, sig, vk) = ::pre_verify(msg.as_ref(), &sig, &vk)?; + let c = ::challenge(&sig.R, &vk, &msg)?; + + Ok(Self { vk: *vk, sig: *sig, c }) + } +} + +impl Item +where + C: Ciphersuite, +{ + /// Perform non-batched verification of this `Item`. + /// + /// This is useful (in combination with `Item::clone`) for implementing + /// fallback logic when batch verification fails. In contrast to + /// [`VerifyingKey::verify`](crate::VerifyingKey::verify), which + /// requires borrowing the message data, the `Item` type is unlinked + /// from the lifetime of the message. + pub fn verify_single(self) -> Result<(), Error> { + self.vk.verify_prehashed(self.c, &self.sig) + } +} + +/// A batch verification context. +pub struct Verifier { + /// Signature data queued for verification. + signatures: Vec>, +} + +impl Verifier +where + C: Ciphersuite, +{ + /// Constructs a new batch verifier. + pub fn new() -> Verifier { + Verifier::default() + } + + /// Queues an Item for verification. + pub fn queue>>(&mut self, item: I) { + self.signatures.push(item.into()); + } + + /// Performs batch verification, returning `Ok(())` if all signatures were + /// valid and `Err` otherwise, or if the batch is empty. + /// + /// The batch verification equation is: + /// + /// h_G * -[sum(z_i * s_i)]P_G + sum(\[z_i\]R_i + [z_i * c_i]VK_i) = 0_G + /// + /// which we split out into: + /// + /// h_G * -[sum(z_i * s_i)]P_G + sum(\[z_i\]R_i) + sum([z_i * c_i]VK_i) = + /// 0_G + /// + /// so that we can use multiscalar multiplication speedups. + /// + /// where for each signature i, + /// - VK_i is the verification key; + /// - R_i is the signature's R value; + /// - s_i is the signature's s value; + /// - c_i is the hash of the message and other data; + /// - z_i is a random 128-bit Scalar; + /// - h_G is the cofactor of the group; + /// - P_G is the generator of the subgroup; + /// + /// As follows elliptic curve scalar multiplication convention, + /// scalar variables are lowercase and group point variables + /// are uppercase. This does not exactly match the RedDSA + /// notation in the [protocol specification §B.1][ps]. + /// + /// [ps]: https://zips.z.cash/protocol/protocol.pdf#reddsabatchverify + pub fn verify(self, mut rng: R) -> Result<(), Error> { + let n = self.signatures.len(); + + if n == 0 { + return Err(Error::InvalidSignature); + } + + let mut VK_coeffs = Vec::with_capacity(n); + let mut VKs = Vec::with_capacity(n); + let mut R_coeffs = Vec::with_capacity(n); + let mut Rs = Vec::with_capacity(n); + let mut P_coeff_acc = <::Field>::zero(); + + for item in self.signatures.iter() { + let z = item.sig.z; + let R = item.sig.R; + + let blind = <::Field>::random(&mut rng); + + let P_coeff = blind * z; + P_coeff_acc = P_coeff_acc - P_coeff; + + R_coeffs.push(blind); + Rs.push(R); + + VK_coeffs.push(<::Field>::zero() + (blind * item.c.0)); + VKs.push(item.vk.to_element()); + } + + let scalars = core::iter::once(&P_coeff_acc).chain(VK_coeffs.iter()).chain(R_coeffs.iter()); + + let basepoints = [C::Group::generator()]; + let points = basepoints.iter().chain(VKs.iter()).chain(Rs.iter()); + + let check: Element = + VartimeMultiscalarMul::::vartime_multiscalar_mul(scalars, points); + + if (check * ::cofactor()) == ::identity() { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } +} + +impl Default for Verifier +where + C: Ciphersuite, +{ + fn default() -> Self { + Self { signatures: Vec::new() } + } +} diff --git a/frost/src/benches.rs b/frost/src/benches.rs new file mode 100644 index 00000000..5a3577b9 --- /dev/null +++ b/frost/src/benches.rs @@ -0,0 +1,180 @@ +//! Ciphersuite-generic benchmark functions. +#![allow(clippy::unwrap_used)] + +use core::iter; + +use alloc::{collections::BTreeMap, format, vec::Vec}; +use rand_core::{CryptoRng, RngCore}; + +use criterion::{BenchmarkId, Criterion, Throughput}; + +use crate as frost; +use crate::{batch, Ciphersuite, Signature, SigningKey, VerifyingKey}; + +struct Item { + vk: VerifyingKey, + sig: Signature, +} + +fn sigs_with_distinct_keys( + rng: &mut R, +) -> impl Iterator> { + let mut rng = rng.clone(); + iter::repeat_with(move || { + let msg = b"Bench"; + let sk = SigningKey::new(&mut rng); + let vk = VerifyingKey::from(&sk); + let sig = sk.sign(&mut rng, &msg[..]); + Item { vk, sig } + }) +} + +/// Benchmark batched signature verification with the specified ciphersuite. +pub fn bench_batch_verify( + c: &mut Criterion, + name: &str, + rng: &mut R, +) { + let mut group = c.benchmark_group(format!("Batch Verification {name}")); + for &n in [8usize, 16, 24, 32, 40, 48, 56, 64].iter() { + group.throughput(Throughput::Elements(n as u64)); + + let sigs = sigs_with_distinct_keys::(rng).take(n).collect::>(); + + group.bench_with_input(BenchmarkId::new("Unbatched verification", n), &sigs, |b, sigs| { + b.iter(|| { + for item in sigs.iter() { + let msg = b"Bench"; + + let Item { vk, sig } = item; + let _ = vk.verify(msg, sig); + } + }) + }); + + group.bench_with_input(BenchmarkId::new("Batched verification", n), &sigs, |b, sigs| { + let mut rng = rng.clone(); + b.iter(|| { + let mut batch = batch::Verifier::new(); + for item in sigs.iter() { + let msg = b"Bench"; + + let Item { vk, sig } = item; + let item = batch::Item::::new(*vk, *sig, msg).unwrap(); + batch.queue(item); + } + batch.verify(&mut rng) + }) + }); + } + group.finish(); +} + +/// Benchmark FROST signing with the specified ciphersuite. +pub fn bench_sign( + c: &mut Criterion, + name: &str, + rng: &mut R, +) { + let mut group = c.benchmark_group(format!("FROST Signing {name}")); + for &n in [3u16, 10, 100, 1000].iter() { + let max_signers = n; + let min_signers = (n * 2 + 2) / 3; + + group.bench_with_input( + BenchmarkId::new("Key Generation with Dealer", max_signers), + &(max_signers, min_signers), + |b, (max_signers, min_signers)| { + let mut rng = rng.clone(); + b.iter(|| { + frost::keys::generate_with_dealer::( + *max_signers, + *min_signers, + frost::keys::IdentifierList::Default, + &mut rng, + ) + .unwrap(); + }) + }, + ); + + let (shares, pubkeys) = frost::keys::generate_with_dealer::( + max_signers, + min_signers, + frost::keys::IdentifierList::Default, + rng, + ) + .unwrap(); + + // Verifies the secret shares from the dealer + let mut key_packages: BTreeMap, frost::keys::KeyPackage> = + BTreeMap::new(); + + for (k, v) in shares { + key_packages.insert(k, frost::keys::KeyPackage::try_from(v).unwrap()); + } + + group.bench_with_input( + BenchmarkId::new("Round 1", min_signers), + &key_packages, + |b, key_packages| { + b.iter(|| { + let participant_identifier = 1u16.try_into().expect("should be nonzero"); + frost::round1::commit( + key_packages.get(&participant_identifier).unwrap().signing_share(), + rng, + ); + }) + }, + ); + + let mut nonces: BTreeMap<_, _> = BTreeMap::new(); + let mut commitments: BTreeMap<_, _> = BTreeMap::new(); + + for participant_index in 1..=min_signers { + let participant_identifier = participant_index.try_into().expect("should be nonzero"); + let (nonce, commitment) = frost::round1::commit( + key_packages.get(&participant_identifier).unwrap().signing_share(), + rng, + ); + nonces.insert(participant_identifier, nonce); + commitments.insert(participant_identifier, commitment); + } + + let message = "message to sign".as_bytes(); + let signing_package = frost::SigningPackage::new(commitments, message); + + group.bench_with_input( + BenchmarkId::new("Round 2", min_signers), + &(key_packages.clone(), nonces.clone(), signing_package.clone()), + |b, (key_packages, nonces, signing_package)| { + b.iter(|| { + let participant_identifier = 1u16.try_into().expect("should be nonzero"); + let key_package = key_packages.get(&participant_identifier).unwrap(); + let nonces_to_use = &nonces.get(&participant_identifier).unwrap(); + frost::round2::sign(signing_package, nonces_to_use, key_package).unwrap(); + }) + }, + ); + + let mut signature_shares = BTreeMap::new(); + for participant_identifier in nonces.keys() { + let key_package = key_packages.get(participant_identifier).unwrap(); + let nonces_to_use = &nonces.get(participant_identifier).unwrap(); + let signature_share = + frost::round2::sign(&signing_package, nonces_to_use, key_package).unwrap(); + signature_shares.insert(*key_package.identifier(), signature_share); + } + + group.bench_with_input( + BenchmarkId::new("Aggregate", min_signers), + &(signing_package.clone(), signature_shares.clone(), pubkeys), + |b, (signing_package, signature_shares, pubkeys)| { + b.iter(|| { + frost::aggregate(signing_package, signature_shares, pubkeys).unwrap(); + }) + }, + ); + } + group.finish(); +} diff --git a/frost/src/const_crc32.rs b/frost/src/const_crc32.rs new file mode 100644 index 00000000..44b7ae79 --- /dev/null +++ b/frost/src/const_crc32.rs @@ -0,0 +1,86 @@ +//! A `const fn` crc32 checksum implementation. +//! +//! # Examples +//! +//! ``` +//! const BYTES: &[u8] = "The quick brown fox jumps over the lazy dog".as_bytes(); +//! const CKSUM: u32 = frost_core::const_crc32::crc32(BYTES); +//! assert_eq!(CKSUM, 0x414fa339_u32); +//! ``` +/// used to generate up a [u32; 256] lookup table in `crc32`. this computes +/// the table on demand for a given "index" `i` +#[rustfmt::skip] +const fn table_fn(i: u32) -> u32 { + let mut out = i; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out = if out & 1 == 1 { 0xedb88320 ^ (out >> 1) } else { out >> 1 }; + out +} +const fn get_table() -> [u32; 256] { + let mut table: [u32; 256] = [0u32; 256]; + let mut i = 0; + while i < 256 { + table[i] = table_fn(i as u32); + i += 1; + } + table +} +const TABLE: [u32; 256] = get_table(); +/// A `const fn` crc32 checksum implementation. +/// +/// Note: this is a naive implementation that should be expected to have poor performance +/// if used on dynamic data at runtime. Usage should generally be restricted to declaring +/// `const` variables based on `static` or `const` data available at build time. +pub const fn crc32(buf: &[u8]) -> u32 { + crc32_seed(buf, 0) +} +/// Calculate crc32 checksum, using provided `seed` as the initial state, instead of the +/// default initial state of `0u32`. +/// +/// # Examples +/// +/// Calculating the checksum from several parts of a larger input: +/// +/// ``` +/// const BYTES: &[u8] = "The quick brown fox jumps over the lazy dog".as_bytes(); +/// +/// let mut cksum = 0u32; +/// +/// cksum = frost_core::const_crc32::crc32_seed(&BYTES[0..10], cksum); +/// cksum = frost_core::const_crc32::crc32_seed(&BYTES[10..15], cksum); +/// cksum = frost_core::const_crc32::crc32_seed(&BYTES[15..], cksum); +/// +/// assert_eq!(cksum, frost_core::const_crc32::crc32(BYTES)); +/// ``` +/// +/// Using separate seeds for different kinds of data, to produce different checksums depending +/// on what kind of data the bytes represent: +/// +/// ``` +/// const THING_ONE_SEED: u32 = 0xbaaaaaad_u32; +/// const THING_TWO_SEED: u32 = 0x2bad2bad_u32; +/// +/// let thing_one_bytes = "bump! thump!".as_bytes(); +/// let thing_two_bytes = "thump! bump!".as_bytes(); +/// +/// let thing_one_cksum = frost_core::const_crc32::crc32_seed(thing_one_bytes, THING_ONE_SEED); +/// let thing_two_cksum = frost_core::const_crc32::crc32_seed(thing_two_bytes, THING_TWO_SEED); +/// +/// assert_ne!(thing_one_cksum, thing_two_cksum); +/// ``` +#[inline] +pub const fn crc32_seed(buf: &[u8], seed: u32) -> u32 { + let mut out = !seed; + let mut i = 0usize; + while i < buf.len() { + out = (out >> 8) ^ TABLE[((out & 0xff) ^ (buf[i] as u32)) as usize]; + i += 1; + } + !out +} diff --git a/frost/src/error.rs b/frost/src/error.rs new file mode 100644 index 00000000..1d7ff4c9 --- /dev/null +++ b/frost/src/error.rs @@ -0,0 +1,188 @@ +//! FROST Error types + +use thiserror_nostd_notrait::Error; + +use crate::{Ciphersuite, Identifier}; + +#[derive(Error, Debug, Clone, Copy, Eq, PartialEq)] +pub struct ParticipantError(Identifier); + +/// An error related to FROST. +#[non_exhaustive] +#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)] +pub enum Error { + /// min_signers is invalid + #[error("min_signers must be at least 2 and not larger than max_signers")] + InvalidMinSigners, + /// max_signers is invalid + #[error("max_signers must be at least 2")] + InvalidMaxSigners, + /// max_signers is invalid + #[error("coefficients must have min_signers-1 elements")] + InvalidCoefficients, + /// This identifier is unserializable. + #[error("Malformed identifier is unserializable.")] + MalformedIdentifier, + /// This identifier is duplicated. + #[error("Duplicated identifier.")] + DuplicatedIdentifier, + /// This identifier does not belong to a participant in the signing process. + #[error("Unknown identifier.")] + UnknownIdentifier, + /// Incorrect number of identifiers. + #[error("Incorrect number of identifiers.")] + IncorrectNumberOfIdentifiers, + /// The encoding of a signing key was malformed. + #[error("Malformed signing key encoding.")] + MalformedSigningKey, + /// The encoding of a verifying key was malformed. + #[error("Malformed verifying key encoding.")] + MalformedVerifyingKey, + /// The encoding of a signature was malformed. + #[error("Malformed signature encoding.")] + MalformedSignature, + /// Signature verification failed. + #[error("Invalid signature.")] + InvalidSignature, + /// Duplicated shares provided + #[error("Duplicated shares provided.")] + DuplicatedShares, + /// Incorrect number of shares. + #[error("Incorrect number of shares.")] + IncorrectNumberOfShares, + /// Commitment equals the identity + #[error("Commitment equals the identity.")] + IdentityCommitment, + /// The participant's commitment is missing from the Signing Package + #[error("The Signing Package must contain the participant's Commitment.")] + MissingCommitment, + /// The participant's commitment is incorrect + #[error("The participant's commitment is incorrect.")] + IncorrectCommitment, + /// Incorrect number of commitments. + #[error("Incorrect number of commitments.")] + IncorrectNumberOfCommitments, + /// Signature share verification failed. + #[error("Invalid signature share.")] + InvalidSignatureShare { + /// The identifier of the signer whose share validation failed. + culprit: Identifier, + }, + /// Secret share verification failed. + #[error("Invalid secret share.")] + InvalidSecretShare { + /// The identifier of the signer whose secret share validation failed, + /// if possible to identify. + culprit: Option>, + }, + /// Round 1 package not found for Round 2 participant. + #[error("Round 1 package not found for Round 2 participant.")] + PackageNotFound, + /// Incorrect number of packages. + #[error("Incorrect number of packages.")] + IncorrectNumberOfPackages, + /// The incorrect package was specified. + #[error("The incorrect package was specified.")] + IncorrectPackage, + /// The ciphersuite does not support DKG. + #[error("The ciphersuite does not support DKG.")] + DKGNotSupported, + /// The proof of knowledge is not valid. + #[error("The proof of knowledge is not valid.")] + InvalidProofOfKnowledge { + /// The identifier of the signer whose share validation failed. + culprit: Identifier, + }, + /// Error in scalar Field. + #[error("Error in scalar Field.")] + FieldError(#[from] FieldError), + /// Error in elliptic curve Group. + #[error("Error in elliptic curve Group.")] + GroupError(#[from] GroupError), + /// Error in coefficient commitment deserialization. + #[error("Invalid coefficient")] + InvalidCoefficient, + /// The ciphersuite does not support deriving identifiers from strings. + #[error("The ciphersuite does not support deriving identifiers from strings.")] + IdentifierDerivationNotSupported, + /// Error serializing value. + #[error("Error serializing value.")] + SerializationError, + /// Error deserializing value. + #[error("Error deserializing value.")] + DeserializationError, +} + +impl Error +where + C: Ciphersuite, +{ + /// Return the identifier of the participant that caused the error. + /// Returns None if not applicable for the error. + /// + /// This can be used to penalize a participant that does not follow the + /// protocol correctly, e.g. removing them from further signings. + pub fn culprit(&self) -> Option> { + // Use an exhaustive match to make sure that if we add new enum items + // then we will explicitly check if they should be added here. + match self { + Error::InvalidSignatureShare { culprit: identifier } + | Error::InvalidProofOfKnowledge { culprit: identifier } => Some(*identifier), + Error::InvalidSecretShare { culprit: identifier } => *identifier, + Error::InvalidMinSigners + | Error::InvalidMaxSigners + | Error::InvalidCoefficients + | Error::MalformedIdentifier + | Error::MalformedSigningKey + | Error::MalformedVerifyingKey + | Error::MalformedSignature + | Error::InvalidSignature + | Error::DuplicatedShares + | Error::IncorrectNumberOfShares + | Error::IdentityCommitment + | Error::MissingCommitment + | Error::IncorrectCommitment + | Error::PackageNotFound + | Error::IncorrectNumberOfPackages + | Error::IncorrectPackage + | Error::DKGNotSupported + | Error::FieldError(_) + | Error::GroupError(_) + | Error::DuplicatedIdentifier + | Error::InvalidCoefficient + | Error::UnknownIdentifier + | Error::IncorrectNumberOfIdentifiers + | Error::IncorrectNumberOfCommitments + | Error::SerializationError + | Error::DeserializationError + | Error::IdentifierDerivationNotSupported => None, + } + } +} + +/// An error related to a scalar Field. +#[non_exhaustive] +#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)] +pub enum FieldError { + /// The encoding of a group scalar was malformed. + #[error("Malformed scalar encoding.")] + MalformedScalar, + /// This scalar MUST NOT be zero. + #[error("Invalid for this scalar to be zero.")] + InvalidZeroScalar, +} + +/// An error related to a Group (usually an elliptic curve or constructed from one) or one of its Elements. +#[non_exhaustive] +#[derive(Error, Debug, Copy, Clone, Eq, PartialEq)] +pub enum GroupError { + /// The encoding of a group element was malformed. + #[error("Malformed group element encoding.")] + MalformedElement, + /// This element MUST NOT be the identity. + #[error("Invalid for this element to be the identity.")] + InvalidIdentityElement, + /// This element MUST have (large) prime order. + #[error("Invalid for this element to not have large prime order.")] + InvalidNonPrimeOrderElement, +} diff --git a/frost/src/identifier.rs b/frost/src/identifier.rs new file mode 100644 index 00000000..aa8418dd --- /dev/null +++ b/frost/src/identifier.rs @@ -0,0 +1,171 @@ +//! FROST participant identifiers + +use core::{ + fmt::{self, Debug}, + hash::{Hash, Hasher}, +}; + +use alloc::vec::Vec; + +use crate::{ + serialization::SerializableScalar, Ciphersuite, Error, Field, FieldError, Group, Scalar, +}; + +/// A FROST participant identifier. +/// +/// The identifier is a field element in the scalar field that the secret polynomial is defined +/// over, corresponding to some x-coordinate for a polynomial f(x) = y. MUST NOT be zero in the +/// field, as f(0) = the shared secret. +#[derive(Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +// We use these to add a validation step since zero scalars should cause an +// error when deserializing. +#[cfg_attr(feature = "serde", serde(try_from = "SerializableScalar"))] +#[cfg_attr(feature = "serde", serde(into = "SerializableScalar"))] +pub struct Identifier(SerializableScalar); + +impl Identifier +where + C: Ciphersuite, +{ + /// Create a new Identifier from a scalar. For internal use only. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(scalar: Scalar) -> Result> { + if scalar == <::Field>::zero() { + Err(FieldError::InvalidZeroScalar.into()) + } else { + Ok(Self(SerializableScalar(scalar))) + } + } + + /// Get the inner scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar(&self) -> Scalar { + self.0 .0 + } + + /// Derive an Identifier from an arbitrary byte string. + /// + /// This feature is not part of the specification and is just a convenient + /// way of creating identifiers. + /// + /// Each possible byte string will map to an uniformly random identifier. + /// Returns an error if the ciphersuite does not support identifier derivation, + /// or if the mapped identifier is zero (which is unpredictable, but should happen + /// with negligible probability). + pub fn derive(s: &[u8]) -> Result> { + let scalar = C::HID(s).ok_or(Error::IdentifierDerivationNotSupported)?; + Self::new(scalar) + } + + /// Serialize the identifier using the ciphersuite encoding. + pub fn serialize(&self) -> Vec { + self.0.serialize() + } + + /// Deserialize an Identifier from a serialized buffer. + /// Returns an error if it attempts to deserialize zero. + pub fn deserialize(bytes: &[u8]) -> Result> { + Self::new(SerializableScalar::deserialize(bytes)?.0) + } +} + +#[cfg(feature = "serde")] +impl TryFrom> for Identifier +where + C: Ciphersuite, +{ + type Error = Error; + + fn try_from(s: SerializableScalar) -> Result { + Self::new(s.0) + } +} + +#[cfg(feature = "serde")] +impl From> for SerializableScalar +where + C: Ciphersuite, +{ + fn from(i: Identifier) -> Self { + i.0 + } +} + +impl Eq for Identifier where C: Ciphersuite {} + +impl Debug for Identifier +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("Identifier").field(&hex::encode(self.serialize())).finish() + } +} + +#[allow(clippy::derived_hash_with_manual_eq)] +impl Hash for Identifier +where + C: Ciphersuite, +{ + fn hash(&self, state: &mut H) { + self.serialize().hash(state) + } +} + +impl Ord for Identifier +where + C: Ciphersuite, +{ + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + let serialized_self = + <::Field>::little_endian_serialize(&self.to_scalar()); + let serialized_other = + <::Field>::little_endian_serialize(&other.to_scalar()); + // The default cmp uses lexicographic order; so we need the elements in big endian + serialized_self + .as_ref() + .iter() + .rev() + .cmp(serialized_other.as_ref().iter().rev()) + } +} + +impl PartialOrd for Identifier +where + C: Ciphersuite, +{ + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl TryFrom for Identifier +where + C: Ciphersuite, +{ + type Error = Error; + + fn try_from(n: u16) -> Result, Self::Error> { + if n == 0 { + Err(FieldError::InvalidZeroScalar.into()) + } else { + // Classic left-to-right double-and-add algorithm that skips the first bit 1 (since + // identifiers are never zero, there is always a bit 1), thus `sum` starts with 1 too. + let one = <::Field>::one(); + let mut sum = <::Field>::one(); + + let bits = (n.to_be_bytes().len() as u32) * 8; + for i in (0..(bits - n.leading_zeros() - 1)).rev() { + sum = sum + sum; + if n & (1 << i) != 0 { + sum = sum + one; + } + } + Self::new(sum) + } + } +} diff --git a/frost/src/keys.rs b/frost/src/keys.rs new file mode 100644 index 00000000..4ec8c235 --- /dev/null +++ b/frost/src/keys.rs @@ -0,0 +1,879 @@ +//! FROST keys, keygen, key shares +#![allow(clippy::type_complexity)] + +use core::iter; + +use alloc::{ + collections::{BTreeMap, BTreeSet}, + fmt::{self, Debug}, + string::ToString, + vec::Vec, +}; + +use derive_getters::Getters; +#[cfg(any(test, feature = "test-impl"))] +use hex::FromHex; + +use rand_core::{CryptoRng, RngCore}; +use zeroize::{DefaultIsZeroes, Zeroize}; + +use crate::{ + serialization::{SerializableElement, SerializableScalar}, + Ciphersuite, Element, Error, Field, Group, Header, Identifier, Scalar, SigningKey, + VerifyingKey, +}; + +#[cfg(feature = "serialization")] +use crate::serialization::{Deserialize, Serialize}; + +use super::compute_lagrange_coefficient; + +/// Sum the commitments from all participants in a distributed key generation +/// run into a single group commitment. +#[cfg_attr(feature = "internals", visibility::make(pub))] +pub(crate) fn sum_commitments( + commitments: &[&VerifiableSecretSharingCommitment], +) -> Result, Error> { + let mut group_commitment = + vec![ + CoefficientCommitment::new(::identity()); + commitments.first().ok_or(Error::IncorrectNumberOfCommitments)?.0.len() + ]; + for commitment in commitments { + for (i, c) in group_commitment.iter_mut().enumerate() { + *c = CoefficientCommitment::new( + c.value() + commitment.0.get(i).ok_or(Error::IncorrectNumberOfCommitments)?.value(), + ); + } + } + Ok(VerifiableSecretSharingCommitment(group_commitment)) +} + +/// Return a vector of randomly generated polynomial coefficients ([`Scalar`]s). +pub(crate) fn generate_coefficients( + size: usize, + rng: &mut R, +) -> Vec> { + iter::repeat_with(|| <::Field>::random(rng)) + .take(size) + .collect() +} + +/// Return a list of default identifiers (1 to max_signers, inclusive). +#[cfg_attr(feature = "internals", visibility::make(pub))] +pub(crate) fn default_identifiers(max_signers: u16) -> Vec> { + (1..=max_signers) + .map(|i| Identifier::::try_from(i).expect("nonzero")) + .collect::>() +} + +/// A secret scalar value representing a signer's share of the group secret. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct SigningShare(pub(crate) SerializableScalar); + +impl SigningShare +where + C: Ciphersuite, +{ + /// Create a new [`SigningShare`] from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(scalar: Scalar) -> Self { + Self(SerializableScalar(scalar)) + } + + /// Get the inner scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar(&self) -> Scalar { + self.0 .0 + } + + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(bytes)?)) + } + + /// Serialize to bytes + pub fn serialize(&self) -> Vec { + self.0.serialize() + } + + /// Computes the signing share from a list of coefficients. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn from_coefficients(coefficients: &[Scalar], peer: Identifier) -> Self { + Self::new(evaluate_polynomial(peer, coefficients)) + } +} + +impl Debug for SigningShare +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("SigningShare").field(&"").finish() + } +} + +impl Default for SigningShare +where + C: Ciphersuite, +{ + fn default() -> Self { + Self::new(<::Field>::zero()) + } +} + +// Implements [`Zeroize`] by overwriting a value with the [`Default::default()`] value +impl DefaultIsZeroes for SigningShare where C: Ciphersuite {} + +#[cfg(any(test, feature = "test-impl"))] +impl FromHex for SigningShare +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; + Self::deserialize(&v).map_err(|_| "malformed scalar") + } +} + +/// A public group element that represents a single signer's public verification share. +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct VerifyingShare(pub(super) SerializableElement) +where + C: Ciphersuite; + +impl VerifyingShare +where + C: Ciphersuite, +{ + /// Create a new [`VerifyingShare`] from a element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(element: Element) -> Self { + Self(SerializableElement(element)) + } + + /// Get the inner element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + #[allow(dead_code)] + pub(crate) fn to_element(&self) -> Element { + self.0 .0 + } + + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableElement::deserialize(bytes)?)) + } + + /// Serialize to bytes + pub fn serialize(&self) -> Result, Error> { + self.0.serialize() + } + + /// Computes a verifying share for a peer given the group commitment. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn from_commitment( + identifier: Identifier, + commitment: &VerifiableSecretSharingCommitment, + ) -> VerifyingShare { + // DKG Round 2, Step 4 + // + // > Any participant can compute the public verification share of any + // > other participant by calculating + // > Y_i = ∏_{j=1}^n ∏_{k=0}^{t−1} φ_{jk}^{i^k mod q}. + // + // Rewriting the equation by moving the product over j to further inside + // the equation: + // Y_i = ∏_{k=0}^{t−1} (∏_{j=1}^n φ_{jk})^{i^k mod q} + // i.e. we can operate on the sum of all φ_j commitments, which is + // what is passed to the functions. + VerifyingShare::new(evaluate_vss(identifier, commitment)) + } +} + +impl Debug for VerifyingShare +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("VerifyingShare") + .field(&self.serialize().map(hex::encode).unwrap_or("".to_string())) + .finish() + } +} + +impl From> for VerifyingShare +where + C: Ciphersuite, +{ + fn from(secret: SigningShare) -> VerifyingShare { + VerifyingShare::new(::generator() * secret.to_scalar()) + } +} + +/// A [`Group::Element`] newtype that is a commitment to one coefficient of our secret polynomial. +/// +/// This is a (public) commitment to one coefficient of a secret polynomial used for performing +/// verifiable secret sharing for a Shamir secret share. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +pub struct CoefficientCommitment(pub(crate) SerializableElement); + +impl CoefficientCommitment +where + C: Ciphersuite, +{ + /// Create a new CoefficientCommitment. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn new(value: Element) -> Self { + Self(SerializableElement(value)) + } + + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableElement::deserialize(bytes)?)) + } + + /// Serialize to bytes + pub fn serialize(&self) -> Result, Error> { + self.0.serialize() + } + + /// Returns inner element value + pub fn value(&self) -> Element { + self.0 .0 + } +} + +impl Debug for CoefficientCommitment +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("CoefficientCommitment") + .field(&self.serialize().map(hex::encode).unwrap_or("".to_string())) + .finish() + } +} + +/// Contains the commitments to the coefficients for our secret polynomial _f_, +/// used to generate participants' key shares. +/// +/// [`VerifiableSecretSharingCommitment`] contains a set of commitments to the coefficients (which +/// themselves are scalars) for a secret polynomial f, where f is used to +/// generate each ith participant's key share f(i). Participants use this set of +/// commitments to perform verifiable secret sharing. +/// +/// Note that participants MUST be assured that they have the *same* +/// [`VerifiableSecretSharingCommitment`], either by performing pairwise comparison, or by using +/// some agreed-upon public location for publication, where each participant can +/// ensure that they received the correct (and same) value. +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +pub struct VerifiableSecretSharingCommitment( + pub(crate) Vec>, +); + +impl VerifiableSecretSharingCommitment +where + C: Ciphersuite, +{ + /// Create a new VerifiableSecretSharingCommitment. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn new(coefficients: Vec>) -> Self { + Self(coefficients) + } + + /// Returns serialized coefficent commitments + pub fn serialize(&self) -> Result>, Error> { + self.0.iter().map(|cc| cc.serialize()).collect::>>() + } + + /// Returns VerifiableSecretSharingCommitment from an iterator of serialized + /// CoefficientCommitments (e.g. a [`Vec>`]). + pub fn deserialize(serialized_coefficient_commitments: I) -> Result> + where + I: IntoIterator, + V: AsRef<[u8]>, + { + let mut coefficient_commitments = Vec::new(); + for cc in serialized_coefficient_commitments.into_iter() { + coefficient_commitments.push(CoefficientCommitment::::deserialize(cc.as_ref())?); + } + + Ok(Self::new(coefficient_commitments)) + } + + /// Get the VerifyingKey matching this commitment vector (which is the first + /// element in the vector), or an error if the vector is empty. + pub(crate) fn verifying_key(&self) -> Result, Error> { + Ok(VerifyingKey::new(self.0.first().ok_or(Error::MissingCommitment)?.0 .0)) + } + + /// Returns the coefficient commitments. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn coefficients(&self) -> &[CoefficientCommitment] { + &self.0 + } +} + +/// A secret share generated by performing a (t-out-of-n) secret sharing scheme, +/// generated by a dealer performing [`generate_with_dealer`]. +/// +/// `n` is the total number of shares and `t` is the threshold required to reconstruct the secret; +/// in this case we use Shamir's secret sharing. +/// +/// As a solution to the secret polynomial _f_ (a 'point'), the `identifier` is the x-coordinate, and the +/// `value` is the y-coordinate. +/// +/// To derive a FROST keypair, the receiver of the [`SecretShare`] *must* call +/// .into(), which under the hood also performs validation. +#[derive(Clone, Debug, Zeroize, PartialEq, Eq, Getters)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SecretShare { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// The participant identifier of this [`SecretShare`]. + #[zeroize(skip)] + pub(crate) identifier: Identifier, + /// Secret Key. + pub(crate) signing_share: SigningShare, + #[zeroize(skip)] + /// The commitments to be distributed among signers. + pub(crate) commitment: VerifiableSecretSharingCommitment, +} + +impl SecretShare +where + C: Ciphersuite, +{ + /// Create a new [`SecretShare`] instance. + pub fn new( + identifier: Identifier, + signing_share: SigningShare, + commitment: VerifiableSecretSharingCommitment, + ) -> Self { + SecretShare { header: Header::default(), identifier, signing_share, commitment } + } + + /// Verifies that a secret share is consistent with a verifiable secret sharing commitment, + /// and returns the derived group info for the participant (their public verification share, + /// and the group public key) if successful. + /// + /// This ensures that this participant's share has been generated using the same + /// mechanism as all other signing participants. Note that participants *MUST* + /// ensure that they have the same view as all other participants of the + /// commitment! + /// + /// An implementation of `vss_verify()` from the [spec]. + /// This also implements `derive_group_info()` from the [spec] (which is very similar), + /// but only for this participant. + /// + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#appendix-C.2-3 + pub fn verify(&self) -> Result<(VerifyingShare, VerifyingKey), Error> { + let f_result = ::generator() * self.signing_share.to_scalar(); + let result = evaluate_vss(self.identifier, &self.commitment); + + if !(f_result == result) { + // The culprit needs to be identified by the caller if needed, + // because this function is called in two different contexts: + // - after trusted dealer key generation, by the participant who + // receives the SecretShare. In that case it does not make sense + // to identify themselves as the culprit, since the issue was with + // the Coordinator or in the communication. + // - during DKG, where a "fake" SecretShare is built just to reuse + // the verification logic and it does make sense to identify the + // culprit. Note that in this case, self.identifier is the caller's + // identifier and not the culprit's, so we couldn't identify + // the culprit inside this function anyway. + return Err(Error::InvalidSecretShare { culprit: None }); + } + + Ok((VerifyingShare::new(result), self.commitment.verifying_key()?)) + } +} + +#[cfg(feature = "serialization")] +impl SecretShare +where + C: Ciphersuite, +{ + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } +} + +/// The identifier list to use when generating key shares. +pub enum IdentifierList<'a, C: Ciphersuite> { + /// Use the default values (1 to max_signers, inclusive). + Default, + /// A user-provided list of identifiers. + Custom(&'a [Identifier]), +} + +/// Allows all participants' keys to be generated using a central, trusted +/// dealer. +/// +/// Under the hood, this performs verifiable secret sharing, which itself uses +/// Shamir secret sharing, from which each share becomes a participant's secret +/// key. The output from this function is a set of shares along with one single +/// commitment that participants use to verify the integrity of the share. +/// +/// Implements [`trusted_dealer_keygen`] from the spec. +/// +/// [`trusted_dealer_keygen`]: https://datatracker.ietf.org/doc/html/rfc9591#appendix-C +pub fn generate_with_dealer( + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, +) -> Result<(BTreeMap, SecretShare>, PublicKeyPackage), Error> { + let mut bytes = [0; 64]; + rng.fill_bytes(&mut bytes); + + let key = SigningKey::new(rng); + + split(&key, max_signers, min_signers, identifiers, rng) +} + +/// Splits an existing key into FROST shares. +/// +/// This is identical to [`generate_with_dealer`] but receives an existing key +/// instead of generating a fresh one. This is useful in scenarios where +/// the key needs to be generated externally or must be derived from e.g. a +/// seed phrase. +pub fn split( + key: &SigningKey, + max_signers: u16, + min_signers: u16, + identifiers: IdentifierList, + rng: &mut R, +) -> Result<(BTreeMap, SecretShare>, PublicKeyPackage), Error> { + validate_num_of_signers(min_signers, max_signers)?; + + if let IdentifierList::Custom(identifiers) = &identifiers { + if identifiers.len() != max_signers as usize { + return Err(Error::IncorrectNumberOfIdentifiers); + } + } + + let verifying_key = VerifyingKey::from(key); + + let coefficients = generate_coefficients::(min_signers as usize - 1, rng); + + let secret_shares = match identifiers { + IdentifierList::Default => { + let identifiers = default_identifiers(max_signers); + generate_secret_shares(key, max_signers, min_signers, coefficients, &identifiers)? + }, + IdentifierList::Custom(identifiers) => { + generate_secret_shares(key, max_signers, min_signers, coefficients, identifiers)? + }, + }; + let mut verifying_shares: BTreeMap, VerifyingShare> = BTreeMap::new(); + + let mut secret_shares_by_id: BTreeMap, SecretShare> = BTreeMap::new(); + + for secret_share in secret_shares { + let signer_public = secret_share.signing_share.into(); + verifying_shares.insert(secret_share.identifier, signer_public); + + secret_shares_by_id.insert(secret_share.identifier, secret_share); + } + + Ok(( + secret_shares_by_id, + PublicKeyPackage { header: Header::default(), verifying_shares, verifying_key }, + )) +} + +/// Evaluate the polynomial with the given coefficients (constant term first) +/// at the point x=identifier using Horner's method. +/// +/// Implements [`polynomial_evaluate`] from the spec. +/// +/// [`polynomial_evaluate`]: https://datatracker.ietf.org/doc/html/rfc9591#name-additional-polynomial-opera +fn evaluate_polynomial( + identifier: Identifier, + coefficients: &[Scalar], +) -> Scalar { + let mut value = <::Field>::zero(); + + let ell = identifier; + for coeff in coefficients.iter().skip(1).rev() { + value = value + *coeff; + value = value * ell.to_scalar(); + } + value = value + *coefficients.first().expect("coefficients must have at least one element"); + value +} + +/// Evaluates the right-hand side of the VSS verification equation, namely +/// ∏^{t−1}_{k=0} φ^{i^k mod q}_{ℓk} (multiplicative notation) using +/// `identifier` as `i` and the `commitment` as the commitment vector φ_ℓ. +/// +/// This is also used in Round 2, Step 4 of the DKG. +fn evaluate_vss( + identifier: Identifier, + commitment: &VerifiableSecretSharingCommitment, +) -> Element { + let i = identifier.to_scalar(); + + let (_, result) = commitment.0.iter().fold( + (<::Field>::one(), ::identity()), + |(i_to_the_k, sum_so_far), comm_k| { + (i * i_to_the_k, sum_so_far + comm_k.value() * i_to_the_k) + }, + ); + result +} + +/// A FROST keypair, which can be generated either by a trusted dealer or using +/// a DKG. +/// +/// When using a central dealer, [`SecretShare`]s are distributed to +/// participants, who then perform verification, before deriving +/// [`KeyPackage`]s, which they store to later use during signing. +#[derive(Clone, Debug, PartialEq, Eq, Getters, Zeroize)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct KeyPackage { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// Denotes the participant identifier each secret share key package is owned by. + #[zeroize(skip)] + pub(crate) identifier: Identifier, + /// This participant's signing share. This is secret. + pub(crate) signing_share: SigningShare, + /// This participant's public key. + #[zeroize(skip)] + pub(crate) verifying_share: VerifyingShare, + /// The public verifying key that represents the entire group. + #[zeroize(skip)] + pub(crate) verifying_key: VerifyingKey, + pub(crate) min_signers: u16, +} + +impl KeyPackage +where + C: Ciphersuite, +{ + /// Create a new [`KeyPackage`] instance. + pub fn new( + identifier: Identifier, + signing_share: SigningShare, + verifying_share: VerifyingShare, + verifying_key: VerifyingKey, + min_signers: u16, + ) -> Self { + Self { + header: Header::default(), + identifier, + signing_share, + verifying_share, + verifying_key, + min_signers, + } + } +} + +#[cfg(feature = "serialization")] +impl KeyPackage +where + C: Ciphersuite, +{ + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } +} + +impl TryFrom> for KeyPackage +where + C: Ciphersuite, +{ + type Error = Error; + + /// Tries to verify a share and construct a [`KeyPackage`] from it. + /// + /// When participants receive a [`SecretShare`] from the dealer, they + /// *MUST* verify the integrity of the share before continuing on to + /// transform it into a signing/verification keypair. Here, we assume that + /// every participant has the same view of the commitment issued by the + /// dealer, but implementations *MUST* make sure that all participants have + /// a consistent view of this commitment in practice. + fn try_from(secret_share: SecretShare) -> Result> { + let (verifying_share, verifying_key) = secret_share.verify()?; + + Ok(KeyPackage { + header: Header::default(), + identifier: secret_share.identifier, + signing_share: secret_share.signing_share, + verifying_share, + verifying_key, + min_signers: secret_share.commitment.0.len() as u16, + }) + } +} + +/// Public data that contains all the signers' verifying shares as well as the +/// group verifying key. +/// +/// Used for verification purposes before publishing a signature. +#[derive(Clone, Debug, PartialEq, Eq, Getters)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct PublicKeyPackage { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// The verifying shares for all participants. Used to validate signature + /// shares they generate. + pub(crate) verifying_shares: BTreeMap, VerifyingShare>, + /// The joint public key for the entire group. + pub(crate) verifying_key: VerifyingKey, +} + +impl PublicKeyPackage +where + C: Ciphersuite, +{ + /// Create a new [`PublicKeyPackage`] instance. + pub fn new( + verifying_shares: BTreeMap, VerifyingShare>, + verifying_key: VerifyingKey, + ) -> Self { + Self { header: Header::default(), verifying_shares, verifying_key } + } + + /// Computes the public key package given a list of participant identifiers + /// and a [`VerifiableSecretSharingCommitment`]. This is useful in scenarios + /// where the commitments are published somewhere and it's desirable to + /// recreate the public key package from them. + pub fn from_commitment( + identifiers: &BTreeSet>, + commitment: &VerifiableSecretSharingCommitment, + ) -> Result, Error> { + let verifying_keys: BTreeMap<_, _> = identifiers + .iter() + .map(|id| (*id, VerifyingShare::from_commitment(*id, commitment))) + .collect(); + Ok(PublicKeyPackage::new(verifying_keys, VerifyingKey::from_commitment(commitment)?)) + } + + /// Computes the public key package given a map of participant identifiers + /// and their [`VerifiableSecretSharingCommitment`] from a distributed key + /// generation process. This is useful in scenarios where the commitments + /// are published somewhere and it's desirable to recreate the public key + /// package from them. + pub fn from_dkg_commitments( + commitments: &BTreeMap, &VerifiableSecretSharingCommitment>, + ) -> Result, Error> { + let identifiers: BTreeSet<_> = commitments.keys().copied().collect(); + let commitments: Vec<_> = commitments.values().copied().collect(); + let group_commitment = sum_commitments(&commitments)?; + Self::from_commitment(&identifiers, &group_commitment) + } +} + +#[cfg(feature = "serialization")] +impl PublicKeyPackage +where + C: Ciphersuite, +{ + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } +} + +/// Validates the number of signers. +#[cfg_attr(feature = "internals", visibility::make(pub))] +fn validate_num_of_signers( + min_signers: u16, + max_signers: u16, +) -> Result<(), Error> { + if min_signers < 2 { + return Err(Error::InvalidMinSigners); + } + + if max_signers < 2 { + return Err(Error::InvalidMaxSigners); + } + + if min_signers > max_signers { + return Err(Error::InvalidMinSigners); + } + + Ok(()) +} + +/// Generate a secret polynomial to use in secret sharing, for the given +/// secret value. Also validates the given parameters. +/// +/// Returns the full vector of coefficients in little-endian order (including the +/// given secret, which is the first element) and a [`VerifiableSecretSharingCommitment`] +/// which contains commitments to those coefficients. +/// +/// Returns an error if the parameters (max_signers, min_signers) are inconsistent. +pub(crate) fn generate_secret_polynomial( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + mut coefficients: Vec>, +) -> Result<(Vec>, VerifiableSecretSharingCommitment), Error> { + validate_num_of_signers(min_signers, max_signers)?; + + if coefficients.len() != min_signers as usize - 1 { + return Err(Error::InvalidCoefficients); + } + + // Prepend the secret, which is the 0th coefficient + coefficients.insert(0, secret.scalar); + + // Create the vector of commitments + let commitment: Vec<_> = coefficients + .iter() + .map(|c| CoefficientCommitment::new(::generator() * *c)) + .collect(); + let commitment: VerifiableSecretSharingCommitment = + VerifiableSecretSharingCommitment(commitment); + + Ok((coefficients, commitment)) +} + +/// Creates secret shares for a given secret using the given coefficients. +/// +/// This function accepts a secret from which shares are generated, +/// and a list of threshold-1 coefficients. While in FROST this secret +/// and coefficients should always be generated randomly, we allow them +/// to be specified for this internal function for testability. +/// +/// Internally, [`generate_secret_shares`] performs verifiable secret sharing, which +/// generates shares via Shamir Secret Sharing, and then generates public +/// commitments to those shares. +/// +/// More specifically, [`generate_secret_shares`]: +/// - Interpret [secret, `coefficients[0]`, ...] as a secret polynomial f +/// - For each participant i, their secret share is f(i) +/// - The commitment to the secret polynomial f is [g^secret, `g^coefficients[0]`, ...] +/// +/// Implements [`secret_share_shard`] from the spec. +/// +/// [`secret_share_shard`]: https://datatracker.ietf.org/doc/html/rfc9591#name-shamir-secret-sharing +pub(crate) fn generate_secret_shares( + secret: &SigningKey, + max_signers: u16, + min_signers: u16, + coefficients: Vec>, + identifiers: &[Identifier], +) -> Result>, Error> { + let mut secret_shares: Vec> = Vec::with_capacity(max_signers as usize); + + let (coefficients, commitment) = + generate_secret_polynomial(secret, max_signers, min_signers, coefficients)?; + + let identifiers_set: BTreeSet<_> = identifiers.iter().collect(); + if identifiers_set.len() != identifiers.len() { + return Err(Error::DuplicatedIdentifier); + } + + for id in identifiers { + let signing_share = SigningShare::from_coefficients(&coefficients, *id); + + secret_shares.push(SecretShare { + header: Header::default(), + identifier: *id, + signing_share, + commitment: commitment.clone(), + }); + } + + Ok(secret_shares) +} + +/// Recompute the secret from at least `min_signers` secret shares (inside +/// [`KeyPackage`]s) using Lagrange interpolation. +/// +/// This can be used if for some reason the original key must be restored; e.g. +/// if threshold signing is not required anymore. +/// +/// This is NOT required to sign with FROST; the point of FROST is being +/// able to generate signatures only using the shares, without having to +/// reconstruct the original key. +/// +/// The caller is responsible for providing at least `min_signers` packages; +/// if less than that is provided, a different key will be returned. +pub fn reconstruct( + key_packages: &[KeyPackage], +) -> Result, Error> { + if key_packages.is_empty() { + return Err(Error::IncorrectNumberOfShares); + } + // There is no obvious way to get `min_signers` in order to validate the + // size of `secret_shares`. Since that is just a best-effort validation, + // we don't need to worry too much about adversarial situations where people + // lie about min_signers, so just get the minimum value out of all of them. + let min_signers = key_packages + .iter() + .map(|k| k.min_signers) + .min() + .expect("should not be empty since that was just tested"); + if key_packages.len() < min_signers as usize { + return Err(Error::IncorrectNumberOfShares); + } + + let mut secret = <::Field>::zero(); + + let identifiers: BTreeSet<_> = key_packages.iter().map(|s| s.identifier()).cloned().collect(); + + if identifiers.len() != key_packages.len() { + return Err(Error::DuplicatedIdentifier); + } + + // Compute the Lagrange coefficients + for key_package in key_packages.iter() { + let lagrange_coefficient = + compute_lagrange_coefficient(&identifiers, None, key_package.identifier)?; + + // Compute y = f(0) via polynomial interpolation of these t-of-n solutions ('points) of f + secret = secret + (lagrange_coefficient * key_package.signing_share().to_scalar()); + } + + Ok(SigningKey { scalar: secret }) +} diff --git a/frost/src/lib.rs b/frost/src/lib.rs new file mode 100644 index 00000000..7a3a7f4b --- /dev/null +++ b/frost/src/lib.rs @@ -0,0 +1,738 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(non_snake_case)] +#[macro_use] +extern crate alloc; + +use core::marker::PhantomData; + +use alloc::{ + collections::{BTreeMap, BTreeSet}, + fmt::{self, Debug}, + vec::Vec, +}; + +use derive_getters::Getters; +#[cfg(any(test, feature = "test-impl"))] +use hex::FromHex; +use keys::PublicKeyPackage; +use rand_core::{CryptoRng, RngCore}; +use serialization::SerializableScalar; +use zeroize::Zeroize; + +pub mod batch; +#[cfg(any(test, feature = "test-impl"))] +pub mod benches; +mod error; +mod identifier; +pub mod keys; +pub mod round1; +pub mod round2; +mod scalar_mul; +// We'd like to make this conditionally pub but the attribute below does +// not work yet (https://github.com/rust-lang/rust/issues/54727) +// #[cfg_attr(feature = "internals", visibility::make(pub))] +pub mod serialization; +mod signature; +mod signing_key; +#[cfg(any(test, feature = "test-impl"))] +pub mod tests; +mod traits; +mod verifying_key; + +pub use error::{Error, FieldError, GroupError}; +pub use identifier::Identifier; +use scalar_mul::VartimeMultiscalarMul; +// Re-export serde +#[cfg(feature = "serde")] +pub use serde; +pub use signature::Signature; +pub use signing_key::SigningKey; +pub use traits::{Ciphersuite, Element, Field, Group, Scalar}; +pub use verifying_key::VerifyingKey; + +/// A type refinement for the scalar field element representing the per-message _[challenge]_. +/// +/// [challenge]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-challenge-computa +#[derive(Copy, Clone)] +pub struct Challenge(pub(crate) <::Field as Field>::Scalar); + +impl Challenge +where + C: Ciphersuite, +{ + /// Creates a challenge from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + #[allow(dead_code)] + pub(crate) fn from_scalar( + scalar: <<::Group as Group>::Field as Field>::Scalar, + ) -> Self { + Self(scalar) + } + + /// Return the underlying scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar( + self, + ) -> <<::Group as Group>::Field as Field>::Scalar { + self.0 + } +} + +impl Debug for Challenge +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("Secret") + .field(&hex::encode(<::Field>::serialize(&self.0))) + .finish() + } +} + +/// Generates the challenge as is required for Schnorr signatures. +/// +/// Deals in bytes, so that [FROST] and singleton signing and verification can use it with different +/// types. +/// +/// This is the only invocation of the H2 hash function from the [RFC]. +/// +/// [FROST]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-challenge-computa +/// [RFC]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + msg: &[u8], +) -> Result, Error> +where + C: Ciphersuite, +{ + let mut preimage = Vec::new(); + + preimage.extend_from_slice(::serialize(R)?.as_ref()); + preimage.extend_from_slice(::serialize(&verifying_key.to_element())?.as_ref()); + preimage.extend_from_slice(msg); + + Ok(Challenge(C::H2(&preimage[..]))) +} + +/// Generates a random nonzero scalar. +/// +/// It assumes that the Scalar Eq/PartialEq implementation is constant-time. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +pub(crate) fn random_nonzero(rng: &mut R) -> Scalar { + loop { + let scalar = <::Field>::random(rng); + + if scalar != <::Field>::zero() { + return scalar; + } + } +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Zeroize)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +struct Header { + /// Format version + #[cfg_attr( + feature = "serde", + serde(deserialize_with = "crate::serialization::version_deserialize::<_>") + )] + version: u8, + /// Ciphersuite ID + #[cfg_attr( + feature = "serde", + serde(serialize_with = "crate::serialization::ciphersuite_serialize::<_, C>") + )] + #[cfg_attr( + feature = "serde", + serde(deserialize_with = "crate::serialization::ciphersuite_deserialize::<_, C>") + )] + ciphersuite: (), + #[cfg_attr(feature = "serde", serde(skip))] + phantom: PhantomData, +} + +impl Default for Header +where + C: Ciphersuite, +{ + fn default() -> Self { + Self { + version: Default::default(), + ciphersuite: Default::default(), + phantom: Default::default(), + } + } +} + +/// The binding factor, also known as _rho_ (ρ) +/// +/// Ensures each signature share is strongly bound to a signing set, specific set +/// of commitments, and a specific message. +/// +/// +#[derive(Clone, PartialEq, Eq)] +pub struct BindingFactor(Scalar); + +impl BindingFactor +where + C: Ciphersuite, +{ + /// Serializes [`BindingFactor`] to bytes. + pub fn serialize(&self) -> Vec { + SerializableScalar::(self.0).serialize() + } +} + +impl Debug for BindingFactor +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("BindingFactor").field(&hex::encode(self.serialize())).finish() + } +} + +/// A list of binding factors and their associated identifiers. +#[derive(Clone)] +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +pub(crate) struct BindingFactorList(BTreeMap, BindingFactor>); + +impl BindingFactorList +where + C: Ciphersuite, +{ + /// Create a new [`BindingFactorList`] from a map of identifiers to binding factors. + #[cfg(feature = "internals")] + pub fn new(binding_factors: BTreeMap, BindingFactor>) -> Self { + Self(binding_factors) + } + + /// Get the [`BindingFactor`] for the given identifier, or None if not found. + pub fn get(&self, key: &Identifier) -> Option<&BindingFactor> { + self.0.get(key) + } +} + +/// [`compute_binding_factors`] in the spec +/// +/// [`compute_binding_factors`]: https://datatracker.ietf.org/doc/html/rfc9591#name-binding-factors-computation +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +pub(crate) fn compute_binding_factor_list( + signing_package: &SigningPackage, + verifying_key: &VerifyingKey, + additional_prefix: &[u8], +) -> Result, Error> +where + C: Ciphersuite, +{ + let preimages = signing_package.binding_factor_preimages(verifying_key, additional_prefix)?; + + Ok(BindingFactorList( + preimages + .iter() + .map(|(identifier, preimage)| { + let binding_factor = C::H1(preimage); + (*identifier, BindingFactor(binding_factor)) + }) + .collect(), + )) +} + +#[cfg(any(test, feature = "test-impl"))] +impl FromHex for BindingFactor +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; + + match v.try_into() { + Ok(bytes) => <::Field>::deserialize(&bytes) + .map(|scalar| Self(scalar)) + .map_err(|_| "malformed scalar encoding"), + Err(_) => Err("malformed scalar encoding"), + } + } +} + +/// Generates a lagrange coefficient. +/// +/// The Lagrange polynomial for a set of points (x_j, y_j) for 0 <= j <= k +/// is ∑_{i=0}^k y_i.ℓ_i(x), where ℓ_i(x) is the Lagrange basis polynomial: +/// +/// ℓ_i(x) = ∏_{0≤j≤k; j≠i} (x - x_j) / (x_i - x_j). +/// +/// This computes ℓ_j(x) for the set of points `xs` and for the j corresponding +/// to the given xj. +/// +/// If `x` is None, it uses 0 for it (since Identifiers can't be 0) +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn compute_lagrange_coefficient( + x_set: &BTreeSet>, + x: Option>, + x_i: Identifier, +) -> Result, Error> { + if x_set.is_empty() { + return Err(Error::IncorrectNumberOfIdentifiers); + } + let mut num = <::Field>::one(); + let mut den = <::Field>::one(); + + let mut x_i_found = false; + + for x_j in x_set.iter() { + if x_i == *x_j { + x_i_found = true; + continue; + } + + if let Some(x) = x { + num = num * (x.to_scalar() - x_j.to_scalar()); + den = den * (x_i.to_scalar() - x_j.to_scalar()); + } else { + // Both signs inverted just to avoid requiring Neg (-*xj) + num = num * x_j.to_scalar(); + den = den * (x_j.to_scalar() - x_i.to_scalar()); + } + } + if !x_i_found { + return Err(Error::UnknownIdentifier); + } + + Ok(num * <::Field>::invert(&den).map_err(|_| Error::DuplicatedIdentifier)?) +} + +/// Generates the lagrange coefficient for the i'th participant (for `signer_id`). +/// +/// Implements [`derive_interpolating_value()`] from the spec. +/// +/// [`derive_interpolating_value()`]: https://datatracker.ietf.org/doc/html/rfc9591#name-polynomials +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn derive_interpolating_value( + signer_id: &Identifier, + signing_package: &SigningPackage, +) -> Result, Error> { + compute_lagrange_coefficient( + &signing_package.signing_commitments().keys().cloned().collect(), + None, + *signer_id, + ) +} + +/// Generated by the coordinator of the signing operation and distributed to +/// each signing party +#[derive(Clone, Debug, PartialEq, Eq, Getters)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SigningPackage { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// The set of commitments participants published in the first round of the + /// protocol. + signing_commitments: BTreeMap, round1::SigningCommitments>, + /// Message which each participant will sign. + /// + /// Each signer should perform protocol-specific verification on the + /// message. + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "serdect::slice::serialize_hex_lower_or_bin", + deserialize_with = "serdect::slice::deserialize_hex_or_bin_vec" + ) + )] + message: Vec, +} + +impl SigningPackage +where + C: Ciphersuite, +{ + /// Create a new `SigningPackage` + /// + /// The `signing_commitments` are sorted by participant `identifier`. + pub fn new( + signing_commitments: BTreeMap, round1::SigningCommitments>, + message: &[u8], + ) -> SigningPackage { + SigningPackage { header: Header::default(), signing_commitments, message: message.to_vec() } + } + + /// Get a signing commitment by its participant identifier, or None if not found. + pub fn signing_commitment( + &self, + identifier: &Identifier, + ) -> Option> { + self.signing_commitments.get(identifier).copied() + } + + /// Compute the preimages to H1 to compute the per-signer binding factors + // We separate this out into its own method so it can be tested + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + #[allow(clippy::type_complexity)] + pub fn binding_factor_preimages( + &self, + verifying_key: &VerifyingKey, + additional_prefix: &[u8], + ) -> Result, Vec)>, Error> { + let mut binding_factor_input_prefix = Vec::new(); + + // The length of a serialized verifying key of the same cipersuite does + // not change between runs of the protocol, so we don't need to hash to + // get a fixed length. + binding_factor_input_prefix.extend_from_slice(verifying_key.serialize()?.as_ref()); + + // The message is hashed with H4 to force the variable-length message + // into a fixed-length byte string, same for hashing the variable-sized + // (between runs of the protocol) set of group commitments, but with H5. + binding_factor_input_prefix.extend_from_slice(C::H4(self.message.as_slice()).as_ref()); + binding_factor_input_prefix.extend_from_slice( + C::H5(&round1::encode_group_commitments(self.signing_commitments())?[..]).as_ref(), + ); + binding_factor_input_prefix.extend_from_slice(additional_prefix); + + Ok(self + .signing_commitments() + .keys() + .map(|identifier| { + let mut binding_factor_input = Vec::new(); + + binding_factor_input.extend_from_slice(&binding_factor_input_prefix); + binding_factor_input.extend_from_slice(identifier.serialize().as_ref()); + (*identifier, binding_factor_input) + }) + .collect()) + } +} + +#[cfg(feature = "serialization")] +impl SigningPackage +where + C: Ciphersuite, +{ + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + serialization::Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + serialization::Deserialize::deserialize(bytes) + } +} + +/// The product of all signers' individual commitments, published as part of the +/// final signature. +#[derive(Clone, PartialEq, Eq)] +pub struct GroupCommitment(pub(crate) Element); + +impl GroupCommitment +where + C: Ciphersuite, +{ + /// Return the underlying element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_element(self) -> ::Element { + self.0 + } + + /// Return the underlying element. + #[cfg(feature = "internals")] + pub fn from_element(element: Element) -> Self { + Self(element) + } +} + +/// Generates the group commitment which is published as part of the joint +/// Schnorr signature. +/// +/// Implements [`compute_group_commitment`] from the spec. +/// +/// [`compute_group_commitment`]: https://datatracker.ietf.org/doc/html/rfc9591#name-group-commitment-computatio +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn compute_group_commitment( + signing_package: &SigningPackage, + binding_factor_list: &BindingFactorList, +) -> Result, Error> +where + C: Ciphersuite, +{ + let identity = ::identity(); + + let mut group_commitment = ::identity(); + + // Number of signing participants we are iterating over. + let n = signing_package.signing_commitments().len(); + + let mut binding_scalars = Vec::with_capacity(n); + + let mut binding_elements = Vec::with_capacity(n); + + for (commitment_identifier, commitment) in signing_package.signing_commitments() { + // The following check prevents a party from accidentally revealing their share. + // Note that the '&&' operator would be sufficient. + if identity == commitment.binding.value() || identity == commitment.hiding.value() { + return Err(Error::IdentityCommitment); + } + + let binding_factor = + binding_factor_list.get(commitment_identifier).ok_or(Error::UnknownIdentifier)?; + + // Collect the binding commitments and their binding factors for one big + // multiscalar multiplication at the end. + binding_elements.push(commitment.binding.value()); + binding_scalars.push(binding_factor.0); + + group_commitment = group_commitment + commitment.hiding.value(); + } + + let accumulated_binding_commitment: Element = + VartimeMultiscalarMul::::vartime_multiscalar_mul(binding_scalars, binding_elements); + + group_commitment = group_commitment + accumulated_binding_commitment; + + Ok(GroupCommitment(group_commitment)) +} + +//////////////////////////////////////////////////////////////////////////////// +// Aggregation +//////////////////////////////////////////////////////////////////////////////// + +/// Aggregates the signature shares to produce a final signature that +/// can be verified with the group public key. +/// +/// `signature_shares` maps the identifier of each participant to the +/// [`round2::SignatureShare`] they sent. These identifiers must come from whatever mapping +/// the coordinator has between communication channels and participants, i.e. +/// they must have assurance that the [`round2::SignatureShare`] came from +/// the participant with that identifier. +/// +/// This operation is performed by a coordinator that can communicate with all +/// the signing participants before publishing the final signature. The +/// coordinator can be one of the participants or a semi-trusted third party +/// (who is trusted to not perform denial of service attacks, but does not learn +/// any secret information). Note that because the coordinator is trusted to +/// report misbehaving parties in order to avoid publishing an invalid +/// signature, if the coordinator themselves is a signer and misbehaves, they +/// can avoid that step. However, at worst, this results in a denial of +/// service attack due to publishing an invalid signature. +pub fn aggregate( + signing_package: &SigningPackage, + signature_shares: &BTreeMap, round2::SignatureShare>, + pubkeys: &keys::PublicKeyPackage, +) -> Result, Error> +where + C: Ciphersuite, +{ + // Check if signing_package.signing_commitments and signature_shares have + // the same set of identifiers, and if they are all in pubkeys.verifying_shares. + if signing_package.signing_commitments().len() != signature_shares.len() { + return Err(Error::UnknownIdentifier); + } + + if !signing_package.signing_commitments().keys().all(|id| { + #[cfg(feature = "cheater-detection")] + return signature_shares.contains_key(id) && pubkeys.verifying_shares().contains_key(id); + #[cfg(not(feature = "cheater-detection"))] + return signature_shares.contains_key(id); + }) { + return Err(Error::UnknownIdentifier); + } + + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, signature_shares, pubkeys)?; + + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the + // binding factor. + let binding_factor_list: BindingFactorList = + compute_binding_factor_list(&signing_package, &pubkeys.verifying_key, &[])?; + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; + + // The aggregation of the signature shares by summing them up, resulting in + // a plain Schnorr signature. + // + // Implements [`aggregate`] from the spec. + // + // [`aggregate`]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-share-aggregation + let mut z = <::Field>::zero(); + + for signature_share in signature_shares.values() { + z = z + signature_share.to_scalar(); + } + + let signature = Signature { R: group_commitment.0, z }; + + // Verify the aggregate signature + let verification_result = pubkeys.verifying_key.verify(signing_package.message(), &signature); + + // Only if the verification of the aggregate signature failed; verify each share to find the cheater. + // This approach is more efficient since we don't need to verify all shares + // if the aggregate signature is valid (which should be the common case). + #[cfg(feature = "cheater-detection")] + if verification_result.is_err() { + detect_cheater( + &group_commitment, + &pubkeys, + &signing_package, + &signature_shares, + &binding_factor_list, + )?; + } + + #[cfg(not(feature = "cheater-detection"))] + verification_result?; + + Ok(signature) +} + +/// Optional cheater detection feature +/// Each share is verified to find the cheater +#[cfg(feature = "cheater-detection")] +fn detect_cheater( + group_commitment: &GroupCommitment, + pubkeys: &keys::PublicKeyPackage, + signing_package: &SigningPackage, + signature_shares: &BTreeMap, round2::SignatureShare>, + binding_factor_list: &BindingFactorList, +) -> Result<(), Error> { + // Compute the per-message challenge. + let challenge = + ::challenge(&group_commitment.0, &pubkeys.verifying_key, signing_package.message())?; + + // Verify the signature shares. + for (identifier, signature_share) in signature_shares { + // Look up the public key for this signer, where `signer_pubkey` = _G.ScalarBaseMult(s[i])_, + // and where s[i] is a secret share of the constant term of _f_, the secret polynomial. + let verifying_share = + pubkeys.verifying_shares.get(identifier).ok_or(Error::UnknownIdentifier)?; + + verify_signature_share_precomputed( + *identifier, + signing_package, + binding_factor_list, + group_commitment, + signature_share, + verifying_share, + challenge, + )?; + } + + // We should never reach here; but we return an error to be safe. + Err(Error::InvalidSignature) +} + +/// Verify a signature share for the given participant `identifier`, +/// `verifying_share` and `signature_share`; with the `signing_package` +/// for which the signature share was produced and with the group's +/// `verifying_key`. +/// +/// This is not required for regular FROST usage but might useful in certain +/// situations where it is desired to verify each individual signature share +/// before aggregating the signature. +pub fn verify_signature_share( + identifier: Identifier, + verifying_share: &keys::VerifyingShare, + signature_share: &round2::SignatureShare, + signing_package: &SigningPackage, + verifying_key: &VerifyingKey, +) -> Result<(), Error> { + // In order to reuse `pre_aggregate()`, we need to create some "dummy" containers + let signature_shares = BTreeMap::from([(identifier, *signature_share)]); + let verifying_shares = BTreeMap::from([(identifier, *verifying_share)]); + let public_key_package = PublicKeyPackage::new(verifying_shares, *verifying_key); + + let (signing_package, signature_shares, pubkeys) = + ::pre_aggregate(signing_package, &signature_shares, &public_key_package)?; + + // Extract the processed values back from the "dummy" containers + let verifying_share = pubkeys + .verifying_shares() + .get(&identifier) + .expect("pre_aggregate() must keep the identifiers"); + let verifying_key = pubkeys.verifying_key(); + let signature_share = signature_shares + .get(&identifier) + .expect("pre_aggregate() must keep the identifiers"); + + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the + // binding factor. + let binding_factor_list: BindingFactorList = + compute_binding_factor_list(&signing_package, verifying_key, &[])?; + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; + + // Compute the per-message challenge. + let challenge = ::challenge( + &group_commitment.clone().to_element(), + verifying_key, + signing_package.message().as_slice(), + )?; + + verify_signature_share_precomputed( + identifier, + &signing_package, + &binding_factor_list, + &group_commitment, + signature_share, + verifying_share, + challenge, + ) +} + +/// Similar to [`verify_signature_share()`] but using a precomputed +/// `binding_factor_list` and `challenge`. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +fn verify_signature_share_precomputed( + signature_share_identifier: Identifier, + signing_package: &SigningPackage, + binding_factor_list: &BindingFactorList, + group_commitment: &GroupCommitment, + signature_share: &round2::SignatureShare, + verifying_share: &keys::VerifyingShare, + challenge: Challenge, +) -> Result<(), Error> { + let lambda_i = derive_interpolating_value(&signature_share_identifier, signing_package)?; + + let binding_factor = binding_factor_list + .get(&signature_share_identifier) + .ok_or(Error::UnknownIdentifier)?; + + let R_share = signing_package + .signing_commitment(&signature_share_identifier) + .ok_or(Error::UnknownIdentifier)? + .to_group_commitment_share(binding_factor); + + // Compute relation values to verify this signature share. + ::verify_share( + group_commitment, + signature_share, + signature_share_identifier, + &R_share, + verifying_share, + lambda_i, + &challenge, + )?; + + Ok(()) +} diff --git a/frost/src/round1.rs b/frost/src/round1.rs new file mode 100644 index 00000000..1dc7ba22 --- /dev/null +++ b/frost/src/round1.rs @@ -0,0 +1,441 @@ +//! FROST Round 1 functionality and types + +use alloc::{ + collections::BTreeMap, + fmt::{self, Debug}, + string::ToString, + vec::Vec, +}; + +use derive_getters::Getters; +#[cfg(any(test, feature = "test-impl"))] +use hex::FromHex; + +use rand_core::{CryptoRng, RngCore}; +use zeroize::Zeroize; + +use crate::{ + serialization::{SerializableElement, SerializableScalar}, + Ciphersuite, Element, Error, Field, Group, Header, +}; + +#[cfg(feature = "serialization")] +use crate::serialization::{Deserialize, Serialize}; + +use super::{keys::SigningShare, Identifier}; + +/// A scalar that is a signing nonce. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct Nonce(pub(super) SerializableScalar); + +impl Nonce +where + C: Ciphersuite, +{ + /// Generates a new uniformly random signing nonce by sourcing fresh randomness and combining + /// with the secret signing share, to hedge against a bad RNG. + /// + /// Each participant generates signing nonces before performing a signing + /// operation. + /// + /// An implementation of `nonce_generate(secret)` from the [spec]. + /// + /// [spec]: https://datatracker.ietf.org/doc/html/rfc9591#name-nonce-generation + pub fn new(secret: &SigningShare, rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + let mut random_bytes = [0; 32]; + rng.fill_bytes(&mut random_bytes[..]); + + Self::nonce_generate_from_random_bytes(secret, random_bytes) + } + + /// Create a nonce from a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + fn from_scalar(scalar: <<::Group as Group>::Field as Field>::Scalar) -> Self { + Self(SerializableScalar(scalar)) + } + + /// Convert a nonce into a scalar. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_scalar( + self, + ) -> <<::Group as Group>::Field as Field>::Scalar { + self.0 .0 + } + + /// Generates a nonce from the given random bytes. + /// This function allows testing and MUST NOT be made public. + pub(crate) fn nonce_generate_from_random_bytes( + secret: &SigningShare, + random_bytes: [u8; 32], + ) -> Self { + let secret_enc = secret.0.serialize(); + + let input: Vec = random_bytes.iter().chain(secret_enc.iter()).cloned().collect(); + + Self::from_scalar(C::H3(input.as_slice())) + } + + /// Deserialize [`Nonce`] from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableScalar::deserialize(bytes)?)) + } + + /// Serialize [`Nonce`] to bytes + pub fn serialize(&self) -> Vec { + self.0.serialize() + } +} + +impl Zeroize for Nonce +where + C: Ciphersuite, +{ + fn zeroize(&mut self) { + *self = Nonce::from_scalar(<::Field>::zero()); + } +} + +#[cfg(any(test, feature = "test-impl"))] +impl FromHex for Nonce +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; + Self::deserialize(&v).map_err(|_| "malformed nonce encoding") + } +} + +/// A group element that is a commitment to a signing nonce share. +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +pub struct NonceCommitment(pub(super) SerializableElement); + +impl NonceCommitment +where + C: Ciphersuite, +{ + /// Create a new [`NonceCommitment`] from an [`Element`] + pub(crate) fn new(value: Element) -> Self { + Self(SerializableElement(value)) + } + + pub(crate) fn value(&self) -> Element { + self.0 .0 + } + + /// Deserialize [`NonceCommitment`] from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self(SerializableElement::deserialize(bytes)?)) + } + + /// Serialize [`NonceCommitment`] to bytes + pub fn serialize(&self) -> Result, Error> { + self.0.serialize() + } +} + +impl Debug for NonceCommitment +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("NonceCommitment") + .field(&self.serialize().map(hex::encode).unwrap_or("".to_string())) + .finish() + } +} + +impl From> for NonceCommitment +where + C: Ciphersuite, +{ + fn from(nonce: Nonce) -> Self { + From::from(&nonce) + } +} + +impl From<&Nonce> for NonceCommitment +where + C: Ciphersuite, +{ + fn from(nonce: &Nonce) -> Self { + Self::new(::generator() * nonce.to_scalar()) + } +} + +#[cfg(any(test, feature = "test-impl"))] +impl FromHex for NonceCommitment +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; + Self::deserialize(&v).map_err(|_| "malformed nonce commitment encoding") + } +} + +/// Comprised of hiding and binding nonces. +/// +/// Note that [`SigningNonces`] must be used *only once* for a signing +/// operation; re-using nonces will result in leakage of a signer's long-lived +/// signing key. +#[derive(Clone, Zeroize, PartialEq, Eq, Getters)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SigningNonces { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// The hiding [`Nonce`]. + pub(crate) hiding: Nonce, + /// The binding [`Nonce`]. + pub(crate) binding: Nonce, + /// The commitments to the nonces. This is precomputed to improve + /// sign() performance, since it needs to check if the commitments + /// to the participant's nonces are included in the commitments sent + /// by the Coordinator, and this prevents having to recompute them. + #[zeroize(skip)] + pub(crate) commitments: SigningCommitments, +} + +impl SigningNonces +where + C: Ciphersuite, +{ + /// Generates a new signing nonce. + /// + /// Each participant generates signing nonces before performing a signing + /// operation. + pub fn new(secret: &SigningShare, rng: &mut R) -> Self + where + R: CryptoRng + RngCore, + { + let hiding = Nonce::::new(secret, rng); + let binding = Nonce::::new(secret, rng); + + Self::from_nonces(hiding, binding) + } + + /// Generates a new [`SigningNonces`] from a pair of [`Nonce`]. + /// + /// # Security + /// + /// SigningNonces MUST NOT be repeated in different FROST signings. + /// Thus, if you're using this method (because e.g. you're writing it + /// to disk between rounds), be careful so that does not happen. + pub fn from_nonces(hiding: Nonce, binding: Nonce) -> Self { + let hiding_commitment = (&hiding).into(); + let binding_commitment = (&binding).into(); + let commitments = SigningCommitments::new(hiding_commitment, binding_commitment); + + Self { header: Header::default(), hiding, binding, commitments } + } +} + +impl Debug for SigningNonces +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SigningNonces") + .field("hiding", &"") + .field("binding", &"") + .finish() + } +} + +#[cfg(feature = "serialization")] +impl SigningNonces +where + C: Ciphersuite, +{ + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } +} + +/// Published by each participant in the first round of the signing protocol. +/// +/// This step can be batched if desired by the implementation. Each +/// SigningCommitment can be used for exactly *one* signature. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Getters)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SigningCommitments { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// Commitment to the hiding [`Nonce`]. + pub(crate) hiding: NonceCommitment, + /// Commitment to the binding [`Nonce`]. + pub(crate) binding: NonceCommitment, +} + +impl SigningCommitments +where + C: Ciphersuite, +{ + /// Create new SigningCommitments + pub fn new(hiding: NonceCommitment, binding: NonceCommitment) -> Self { + Self { header: Header::default(), hiding, binding } + } + + /// Computes the [commitment share] from these round one signing commitments. + /// + /// [commitment share]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-share-aggregation + #[cfg(any(feature = "internals", feature = "cheater-detection"))] + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(super) fn to_group_commitment_share( + self, + binding_factor: &crate::BindingFactor, + ) -> GroupCommitmentShare { + GroupCommitmentShare::(self.hiding.value() + (self.binding.value() * binding_factor.0)) + } +} + +#[cfg(feature = "serialization")] +impl SigningCommitments +where + C: Ciphersuite, +{ + /// Serialize the struct into a Vec. + pub fn serialize(&self) -> Result, Error> { + Serialize::serialize(&self) + } + + /// Deserialize the struct from a slice of bytes. + pub fn deserialize(bytes: &[u8]) -> Result> { + Deserialize::deserialize(bytes) + } +} + +impl From<&SigningNonces> for SigningCommitments +where + C: Ciphersuite, +{ + fn from(nonces: &SigningNonces) -> Self { + nonces.commitments + } +} + +/// One signer's share of the group commitment, derived from their individual signing commitments +/// and the binding factor _rho_. +#[derive(Clone, Copy, PartialEq)] +pub struct GroupCommitmentShare(pub(super) Element); + +impl GroupCommitmentShare { + /// Create from an element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn from_element(element: Element) -> Self { + Self(element) + } + + /// Return the underlying element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn to_element(self) -> Element { + self.0 + } +} + +/// Encode the list of group signing commitments. +/// +/// Implements [`encode_group_commitment_list()`] from the spec. +/// +/// `signing_commitments` must contain the sorted map of participants +/// identifiers to the signing commitments they issued. +/// +/// Returns a byte string containing the serialized representation of the +/// commitment list. +/// +/// [`encode_group_commitment_list()`]: https://datatracker.ietf.org/doc/html/rfc9591#name-list-operations +pub(super) fn encode_group_commitments( + signing_commitments: &BTreeMap, SigningCommitments>, +) -> Result, Error> { + let mut bytes = vec![]; + + for (item_identifier, item) in signing_commitments { + bytes.extend_from_slice(item_identifier.serialize().as_ref()); + bytes.extend_from_slice(::serialize(&item.hiding.value())?.as_ref()); + bytes.extend_from_slice(::serialize(&item.binding.value())?.as_ref()); + } + + Ok(bytes) +} + +/// Done once by each participant, to generate _their_ nonces and commitments +/// that are then used during signing. +/// +/// This is only needed if pre-processing is needed (for 1-round FROST). For +/// regular 2-round FROST, use [`commit`]. +/// +/// When performing signing using two rounds, num_nonces would equal 1, to +/// perform the first round. Batching entails generating more than one +/// nonce/commitment pair at a time. Nonces should be stored in secret storage +/// for later use, whereas the commitments are published. +pub fn preprocess( + num_nonces: u8, + secret: &SigningShare, + rng: &mut R, +) -> (Vec>, Vec>) +where + C: Ciphersuite, + R: CryptoRng + RngCore, +{ + let mut signing_nonces: Vec> = Vec::with_capacity(num_nonces as usize); + let mut signing_commitments: Vec> = + Vec::with_capacity(num_nonces as usize); + + for _ in 0..num_nonces { + let nonces = SigningNonces::new(secret, rng); + signing_commitments.push(SigningCommitments::from(&nonces)); + signing_nonces.push(nonces); + } + + (signing_nonces, signing_commitments) +} + +/// Performed once by each participant selected for the signing operation. +/// +/// Implements [`commit`] from the spec. +/// +/// Generates the signing nonces and commitments to be used in the signing +/// operation. +/// +/// [`commit`]: https://datatracker.ietf.org/doc/html/rfc9591#name-round-one-commitment +pub fn commit( + secret: &SigningShare, + rng: &mut R, +) -> (SigningNonces, SigningCommitments) +where + C: Ciphersuite, + R: CryptoRng + RngCore, +{ + let (mut vec_signing_nonces, mut vec_signing_commitments) = preprocess(1, secret, rng); + ( + vec_signing_nonces.pop().expect("must have 1 element"), + vec_signing_commitments.pop().expect("must have 1 element"), + ) +} diff --git a/frost/src/round2.rs b/frost/src/round2.rs new file mode 100644 index 00000000..ea600baa --- /dev/null +++ b/frost/src/round2.rs @@ -0,0 +1,171 @@ +//! FROST Round 2 functionality and types, for signature share generation + +use core::fmt::{self, Debug}; + +use crate as frost; +use crate::{ + Challenge, Ciphersuite, Error, Field, Group, {round1, *}, +}; + +/// A participant's signature share, which the coordinator will aggregate with all other signer's +/// shares into the joint signature. +#[derive(Clone, Copy, Eq, PartialEq, Getters)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(deny_unknown_fields))] +pub struct SignatureShare { + /// Serialization header + #[getter(skip)] + pub(crate) header: Header, + /// This participant's signature over the message. + pub(crate) share: SerializableScalar, +} + +impl SignatureShare +where + C: Ciphersuite, +{ + pub(crate) fn new( + scalar: <<::Group as Group>::Field as Field>::Scalar, + ) -> Self { + Self { header: Header::default(), share: SerializableScalar(scalar) } + } + + pub(crate) fn to_scalar( + self, + ) -> <<::Group as Group>::Field as Field>::Scalar { + self.share.0 + } + + /// Deserialize [`SignatureShare`] from bytes + pub fn deserialize(bytes: &[u8]) -> Result> { + Ok(Self { header: Header::default(), share: SerializableScalar::deserialize(bytes)? }) + } + + /// Serialize [`SignatureShare`] to bytes + pub fn serialize(&self) -> Vec { + self.share.serialize() + } + + /// Tests if a signature share issued by a participant is valid before + /// aggregating it into a final joint signature to publish. + /// + /// This is the final step of [`verify_signature_share`] from the spec. + /// + /// [`verify_signature_share`]: https://datatracker.ietf.org/doc/html/rfc9591#name-signature-share-aggregation + #[cfg(any(feature = "cheater-detection", feature = "internals"))] + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn verify( + &self, + identifier: Identifier, + group_commitment_share: &round1::GroupCommitmentShare, + verifying_share: &frost::keys::VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + if (::generator() * self.to_scalar()) + != (group_commitment_share.to_element() + + (verifying_share.to_element() * challenge.0 * lambda_i)) + { + return Err(Error::InvalidSignatureShare { culprit: identifier }); + } + + Ok(()) + } +} + +impl Debug for SignatureShare +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("SignatureShare") + .field("share", &hex::encode(self.serialize())) + .finish() + } +} + +/// Compute the signature share for a signing operation. +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +pub(super) fn compute_signature_share( + signer_nonces: &round1::SigningNonces, + binding_factor: BindingFactor, + lambda_i: <<::Group as Group>::Field as Field>::Scalar, + key_package: &keys::KeyPackage, + challenge: Challenge, +) -> SignatureShare { + let z_share: <::Field as Field>::Scalar = signer_nonces.hiding.to_scalar() + + (signer_nonces.binding.to_scalar() * binding_factor.0) + + (lambda_i * key_package.signing_share.to_scalar() * challenge.to_scalar()); + + SignatureShare::::new(z_share) +} + +/// Performed once by each participant selected for the signing operation. +/// +/// Implements [`sign`] from the spec. +/// +/// Receives the message to be signed and a set of signing commitments and a set +/// of randomizing commitments to be used in that signing operation, including +/// that for this participant. +/// +/// Assumes the participant has already determined which nonce corresponds with +/// the commitment that was assigned by the coordinator in the SigningPackage. +/// +/// [`sign`]: https://datatracker.ietf.org/doc/html/rfc9591#name-round-two-signature-share-g +pub fn sign( + signing_package: &SigningPackage, + signer_nonces: &round1::SigningNonces, + key_package: &frost::keys::KeyPackage, +) -> Result, Error> { + if signing_package.signing_commitments().len() < key_package.min_signers as usize { + return Err(Error::IncorrectNumberOfCommitments); + } + + // Validate the signer's commitment is present in the signing package + let commitment = signing_package + .signing_commitments + .get(&key_package.identifier) + .ok_or(Error::MissingCommitment)?; + + // Validate if the signer's commitment exists + if &signer_nonces.commitments != commitment { + return Err(Error::IncorrectCommitment); + } + + let (signing_package, signer_nonces, key_package) = + ::pre_sign(signing_package, signer_nonces, key_package)?; + + // Encodes the signing commitment list produced in round one as part of generating [`BindingFactor`], the + // binding factor. + let binding_factor_list: BindingFactorList = + compute_binding_factor_list(&signing_package, &key_package.verifying_key, &[])?; + let binding_factor: frost::BindingFactor = binding_factor_list + .get(&key_package.identifier) + .ok_or(Error::UnknownIdentifier)? + .clone(); + + // Compute the group commitment from signing commitments produced in round one. + let group_commitment = compute_group_commitment(&signing_package, &binding_factor_list)?; + + // Compute Lagrange coefficient. + let lambda_i = frost::derive_interpolating_value(key_package.identifier(), &signing_package)?; + + // Compute the per-message challenge. + let challenge = + ::challenge(&group_commitment.0, &key_package.verifying_key, signing_package.message())?; + + // Compute the Schnorr signature share. + let signature_share = ::compute_signature_share( + &group_commitment, + &signer_nonces, + binding_factor, + lambda_i, + &key_package, + challenge, + ); + + Ok(signature_share) +} diff --git a/frost/src/scalar_mul.rs b/frost/src/scalar_mul.rs new file mode 100644 index 00000000..45230f0f --- /dev/null +++ b/frost/src/scalar_mul.rs @@ -0,0 +1,267 @@ +//! Non-adjacent form (NAF) implementations for fast batch scalar multiplcation + +// We expect slicings in this module to never panic due to algorithmic +// constraints. +#![allow(clippy::indexing_slicing)] + +use core::{ + borrow::Borrow, + fmt::{Debug, Result}, + marker::PhantomData, +}; + +use alloc::vec::Vec; + +use crate::{Ciphersuite, Element, Field, Group, Scalar}; + +/// Calculates the quotient of `self` and `rhs`, rounding the result towards positive infinity. +/// +/// # Panics +/// +/// This function will panic if `rhs` is 0 or the division results in overflow. +/// +/// This function is similar to `div_ceil` that is [available on +/// Nightly](https://github.com/rust-lang/rust/issues/88581). +/// +// TODO: remove this function and use `div_ceil()` instead when `int_roundings` +// is stabilized. +const fn div_ceil(lhs: usize, rhs: usize) -> usize { + let d = lhs / rhs; + let r = lhs % rhs; + if r > 0 && rhs > 0 { + d + 1 + } else { + d + } +} + +/// A trait for transforming a scalar generic over a ciphersuite to a non-adjacent form (NAF). +pub trait NonAdjacentForm { + fn non_adjacent_form(&self, w: usize) -> Vec; +} + +impl NonAdjacentForm for Scalar +where + C: Ciphersuite, +{ + /// Computes a width-(w) "Non-Adjacent Form" of this scalar. + /// + /// Thanks to curve25519-dalek for the original implementation that informed this one. + /// + /// # Safety + /// + /// The full scalar field MUST fit in 256 bits in this implementation. + fn non_adjacent_form(&self, w: usize) -> Vec { + // required by the NAF definition + debug_assert!(w >= 2); + // required so that the NAF digits fit in i8 + debug_assert!(w <= 8); + + use byteorder::{ByteOrder, LittleEndian}; + + let serialized_scalar = <::Field>::little_endian_serialize(self); + // The canonical serialization length of this `Scalar` in bytes. + let serialization_len = serialized_scalar.as_ref().len(); + + // Compute the size of the non-adjacent form from the number of bytes needed to serialize + // `Scalar`s, plus 1 bit. + // + // The length of the NAF is at most one more than the bit length. + let naf_length: usize = serialization_len * u8::BITS as usize + 1; + + // Safety: + // + // The max value of `naf_length` (the number of bits to represent the + // scalar plus 1) _should_ have plenty of room in systems where usize is + // greater than 8 bits (aka, not a u8). If you are able to compile this + // code on a system with 8-bit pointers, well done, but this code will + // probably not compute the right thing for you, use a 16-bit or above + // system. Since the rest of this code uses u64's for limbs, we + // recommend a 64-bit system. + let mut naf = vec![0; naf_length]; + + // Get the number of 64-bit limbs we need. + let num_limbs: usize = div_ceil(naf_length, u64::BITS as usize); + + let mut x_u64 = vec![0u64; num_limbs]; + + // This length needs to be 8*destination.len(), so pad out to length num_limbs * 8. + let mut padded_le_serialized = vec![0u8; num_limbs * 8]; + + padded_le_serialized[..serialization_len].copy_from_slice(serialized_scalar.as_ref()); + + LittleEndian::read_u64_into(padded_le_serialized.as_ref(), &mut x_u64[0..num_limbs]); + + let width = 1 << w; + let window_mask = width - 1; + + let mut pos = 0; + let mut carry = 0; + while pos < naf_length { + // Construct a buffer of bits of the scalar, starting at bit `pos` + let u64_idx = pos / 64; + let bit_idx = pos % 64; + + let bit_buf: u64 = if bit_idx < 64 - w { + // This window's bits are contained in a single u64 + x_u64[u64_idx] >> bit_idx + } else { + // Combine the current u64's bits with the bits from the next u64 + (x_u64[u64_idx] >> bit_idx) | (x_u64[1 + u64_idx] << (64 - bit_idx)) + }; + + // Add the carry into the current window + let window = carry + (bit_buf & window_mask); + + if window & 1 == 0 { + // If the window value is even, preserve the carry and continue. + // Why is the carry preserved? + // If carry == 0 and window & 1 == 0, then the next carry should be 0 + // If carry == 1 and window & 1 == 0, then bit_buf & 1 == 1 so the next carry should be 1 + pos += 1; + continue; + } + + if window < width / 2 { + carry = 0; + naf[pos] = window as i8; + } else { + carry = 1; + naf[pos] = (window as i8).wrapping_sub(width as i8); + } + + pos += w; + } + + naf + } +} + +/// A trait for variable-time multiscalar multiplication without precomputation. +/// +/// Implement for a group element. +pub trait VartimeMultiscalarMul: Clone { + /// Given an iterator of public scalars and an iterator of + /// `Option`s of group elements, compute either `Some(Q)`, where + /// $$ + /// Q = c\_1 E\_1 + \cdots + c\_n E\_n, + /// $$ + /// if all points were `Some(E_i)`, or else return `None`. + fn optional_multiscalar_mul(scalars: I, elements: J) -> Option + where + I: IntoIterator, + I::Item: Borrow>, + J: IntoIterator>; + + /// Given an iterator of public scalars and an iterator of + /// public group elements, compute + /// $$ + /// Q = c\_1 E\_1 + \cdots + c\_n E\_n, + /// $$ + /// using variable-time operations. + /// + /// It is an error to call this function with two iterators of different lengths. + fn vartime_multiscalar_mul(scalars: I, elements: J) -> Self + where + I: IntoIterator, + I::Item: Borrow>, + J: IntoIterator, + J::Item: Borrow, + { + Self::optional_multiscalar_mul( + scalars, + elements.into_iter().map(|e| Some(e.borrow().clone())), + ) + .expect("all elements should be Some") + } +} + +impl VartimeMultiscalarMul for Element +where + C: Ciphersuite, +{ + #[allow(clippy::comparison_chain)] + fn optional_multiscalar_mul(scalars: I, elements: J) -> Option> + where + I: IntoIterator, + I::Item: Borrow>, + J: IntoIterator>>, + { + let nafs: Vec<_> = scalars + .into_iter() + .map(|c| NonAdjacentForm::::non_adjacent_form(c.borrow(), 5)) + .collect(); + + let lookup_tables = elements + .into_iter() + .map(|P_opt| P_opt.map(|P| LookupTable5::>::from(&P))) + .collect::>>()?; + + if nafs.len() != lookup_tables.len() { + return None; + } + + let mut r = ::identity(); + + // All NAFs will have the same size, so get it from the first + if nafs.is_empty() { + return Some(r); + } + let naf_length = nafs[0].len(); + + for i in (0..naf_length).rev() { + let mut t = r + r; + + for (naf, lookup_table) in nafs.iter().zip(lookup_tables.iter()) { + if naf[i] > 0 { + t = t + lookup_table.select(naf[i] as usize); + } else if naf[i] < 0 { + t = t - lookup_table.select(-naf[i] as usize); + } + } + + r = t; + } + + Some(r) + } +} + +/// Holds odd multiples 1A, 3A, ..., 15A of a point A. +#[derive(Copy, Clone)] +pub(crate) struct LookupTable5 { + pub(crate) bytes: [T; 8], + pub(crate) _marker: PhantomData, +} + +impl LookupTable5 { + /// Given public, odd \\( x \\) with \\( 0 < x < 2^4 \\), return \\(xA\\). + pub fn select(&self, x: usize) -> T { + debug_assert_eq!(x & 1, 1); + debug_assert!(x < 16); + + self.bytes[x / 2] + } +} + +impl Debug for LookupTable5 { + fn fmt(&self, f: &mut core::fmt::Formatter) -> Result { + write!(f, "LookupTable5({:?})", self.bytes) + } +} + +impl<'a, C> From<&'a Element> for LookupTable5> +where + C: Ciphersuite, +{ + fn from(A: &'a Element) -> Self { + let mut Ai = [*A; 8]; + let A2 = *A + *A; + for i in 0..7 { + Ai[i + 1] = A2 + Ai[i]; + } + + // Now Ai = [A, 3A, 5A, 7A, 9A, 11A, 13A, 15A] + LookupTable5 { bytes: Ai, _marker: PhantomData } + } +} diff --git a/frost/src/serialization.rs b/frost/src/serialization.rs new file mode 100644 index 00000000..750e5c87 --- /dev/null +++ b/frost/src/serialization.rs @@ -0,0 +1,234 @@ +//! Serialization support. + +use alloc::vec::Vec; + +use crate::{Ciphersuite, FieldError}; + +use crate::{Element, Error, Field, Group}; + +#[derive(Clone, Copy, PartialEq, Eq)] +#[cfg_attr(feature = "internals", visibility::make(pub))] +#[cfg_attr(docsrs, doc(cfg(feature = "internals")))] +/// Helper struct to serialize a Scalar. +pub(crate) struct SerializableScalar( + pub <<::Group as Group>::Field as Field>::Scalar, +); + +impl SerializableScalar +where + C: Ciphersuite, +{ + /// Serialize a Scalar. + pub fn serialize(&self) -> Vec { + <::Field>::serialize(&self.0).as_ref().to_vec() + } + + /// Deserialize a Scalar from a serialized buffer. + pub fn deserialize(bytes: &[u8]) -> Result> { + let serialized: <::Field as Field>::Serialization = + bytes.to_vec().try_into().map_err(|_| FieldError::MalformedScalar)?; + let scalar = <::Field>::deserialize(&serialized)?; + Ok(Self(scalar)) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SerializableScalar +where + C: Ciphersuite, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let serialized = <::Group as Group>::Field::serialize(&self.0); + serdect::array::serialize_hex_lower_or_bin(&serialized.as_ref(), serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, C> serde::Deserialize<'de> for SerializableScalar +where + C: Ciphersuite, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Get size from the size of the zero scalar + let zero = <::Field as Field>::zero(); + let len = <::Field as Field>::serialize(&zero).as_ref().len(); + + let mut bytes = vec![0u8; len]; + serdect::array::deserialize_hex_or_bin(&mut bytes[..], deserializer)?; + let array = + bytes.try_into().map_err(|_| serde::de::Error::custom("invalid byte length"))?; + <::Group as Group>::Field::deserialize(&array) + .map(|scalar| Self(scalar)) + .map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) struct SerializableElement(pub(crate) Element); + +impl SerializableElement +where + C: Ciphersuite, +{ + /// Serialize an Element. Returns an error if it's the identity. + pub fn serialize(&self) -> Result, Error> { + Ok(::serialize(&self.0)?.as_ref().to_vec()) + } + + /// Deserialize an Element. Returns an error if it's malformed or is the + /// identity. + pub fn deserialize(bytes: &[u8]) -> Result> { + let serialized: ::Serialization = + bytes.to_vec().try_into().map_err(|_| FieldError::MalformedScalar)?; + let scalar = ::deserialize(&serialized)?; + Ok(Self(scalar)) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for SerializableElement +where + C: Ciphersuite, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let serialized = + ::serialize(&self.0).map_err(serde::ser::Error::custom)?; + serdect::array::serialize_hex_lower_or_bin(&serialized.as_ref(), serializer) + } +} + +#[cfg(feature = "serde")] +impl<'de, C> serde::Deserialize<'de> for SerializableElement +where + C: Ciphersuite, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + // Get size from the size of the generator + let generator = ::generator(); + let len = ::serialize(&generator) + .expect("serializing the generator always works") + .as_ref() + .len(); + + let mut bytes = vec![0u8; len]; + serdect::array::deserialize_hex_or_bin(&mut bytes[..], deserializer)?; + let array = + bytes.try_into().map_err(|_| serde::de::Error::custom("invalid byte length"))?; + ::deserialize(&array) + .map(|element| Self(element)) + .map_err(serde::de::Error::custom) + } +} + +// The short 4-byte ID. Derived as the CRC-32 of the UTF-8 +// encoded ID in big endian format. +#[cfg(feature = "serde")] +const fn short_id() -> [u8; 4] +where + C: Ciphersuite, +{ + const_crc32::crc32(C::ID.as_bytes()).to_be_bytes() +} + +/// Serialize a placeholder ciphersuite field with the ciphersuite ID string. +#[cfg(feature = "serde")] +pub(crate) fn ciphersuite_serialize(_: &(), s: S) -> Result +where + S: serde::Serializer, + C: Ciphersuite, +{ + use serde::Serialize; + + if s.is_human_readable() { + C::ID.serialize(s) + } else { + serde::Serialize::serialize(&short_id::(), s) + } +} + +/// Deserialize a placeholder ciphersuite field, checking if it's the ciphersuite ID string. +#[cfg(feature = "serde")] +pub(crate) fn ciphersuite_deserialize<'de, D, C>(deserializer: D) -> Result<(), D::Error> +where + D: serde::Deserializer<'de>, + C: Ciphersuite, +{ + if deserializer.is_human_readable() { + let s: alloc::string::String = serde::de::Deserialize::deserialize(deserializer)?; + if s != C::ID { + Err(serde::de::Error::custom("wrong ciphersuite")) + } else { + Ok(()) + } + } else { + let buffer: [u8; 4] = serde::de::Deserialize::deserialize(deserializer)?; + if buffer != short_id::() { + Err(serde::de::Error::custom("wrong ciphersuite")) + } else { + Ok(()) + } + } +} + +/// Deserialize a version. For now, since there is a single version 0, +/// simply validate if it's 0. +#[cfg(feature = "serde")] +pub(crate) fn version_deserialize<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let version: u8 = serde::de::Deserialize::deserialize(deserializer)?; + if version != 0 { + Err(serde::de::Error::custom("wrong format version, only 0 supported")) + } else { + Ok(version) + } +} + +// Default byte-oriented serialization for structs that need to be communicated. +// +// Note that we still manually implement these methods in each applicable type, +// instead of making these traits `pub` and asking users to import the traits. +// The reason is that ciphersuite traits would need to re-export these traits, +// parametrized with the ciphersuite, but trait aliases are not currently +// supported: + +#[cfg(feature = "serialization")] +pub(crate) trait Serialize { + /// Serialize the struct into a Vec. + fn serialize(&self) -> Result, Error>; +} + +#[cfg(feature = "serialization")] +pub(crate) trait Deserialize { + /// Deserialize the struct from a slice of bytes. + fn deserialize(bytes: &[u8]) -> Result> + where + Self: core::marker::Sized; +} + +#[cfg(feature = "serialization")] +impl Serialize for T { + fn serialize(&self) -> Result, Error> { + postcard::to_allocvec(self).map_err(|_| Error::SerializationError) + } +} + +#[cfg(feature = "serialization")] +impl serde::Deserialize<'de>, C: Ciphersuite> Deserialize for T { + fn deserialize(bytes: &[u8]) -> Result> { + postcard::from_bytes(bytes).map_err(|_| Error::DeserializationError) + } +} diff --git a/frost/src/signature.rs b/frost/src/signature.rs new file mode 100644 index 00000000..f2fca739 --- /dev/null +++ b/frost/src/signature.rs @@ -0,0 +1,142 @@ +//! Schnorr signatures over prime order groups (or subgroups) + +use alloc::{string::ToString, vec::Vec}; +use derive_getters::Getters; + +use crate::{Ciphersuite, Element, Error, Field, Group, Scalar}; + +/// A Schnorr signature over some prime order group (or subgroup). +#[derive(Copy, Clone, Eq, PartialEq, Getters)] +pub struct Signature { + /// The commitment `R` to the signature nonce. + pub(crate) R: Element, + /// The response `z` to the challenge computed from the commitment `R`, the verifying key, and + /// the message. + pub(crate) z: Scalar, +} + +impl Signature +where + C: Ciphersuite, + C::Group: Group, + ::Field: Field, +{ + /// Create a new Signature. + #[cfg(feature = "internals")] + pub fn new( + R: ::Element, + z: <::Field as Field>::Scalar, + ) -> Self { + Self { R, z } + } + + /// Converts default-encoded bytes as + /// [`Ciphersuite::SignatureSerialization`] into a `Signature`. + #[cfg(feature = "internals")] + pub fn default_deserialize(bytes: &[u8]) -> Result> { + // To compute the expected length of the encoded point, encode the generator + // and get its length. Note that we can't use the identity because it can be encoded + // shorter in some cases (e.g. P-256, which uses SEC1 encoding). + let generator = ::generator(); + let mut R_bytes = Vec::from(::serialize(&generator)?.as_ref()); + let R_bytes_len = R_bytes.len(); + + let one = <::Field as Field>::zero(); + let mut z_bytes = + Vec::from(<::Field as Field>::serialize(&one).as_ref()); + let z_bytes_len = z_bytes.len(); + + if bytes.len() != R_bytes_len + z_bytes_len { + return Err(Error::MalformedSignature); + } + + R_bytes[..].copy_from_slice(bytes.get(0..R_bytes_len).ok_or(Error::MalformedSignature)?); + + let R_serialization = &R_bytes.try_into().map_err(|_| Error::MalformedSignature)?; + + // We extract the exact length of bytes we expect, not just the remaining bytes with `bytes[R_bytes_len..]` + z_bytes[..].copy_from_slice( + bytes + .get(R_bytes_len..R_bytes_len + z_bytes_len) + .ok_or(Error::MalformedSignature)?, + ); + + let z_serialization = &z_bytes.try_into().map_err(|_| Error::MalformedSignature)?; + + Ok(Self { + R: ::deserialize(R_serialization)?, + z: <::Field>::deserialize(z_serialization)?, + }) + } + + /// Converts bytes as [`Ciphersuite::SignatureSerialization`] into a `Signature`. + pub fn deserialize(bytes: &[u8]) -> Result> { + C::deserialize_signature(bytes) + } + + /// Converts this signature to its default byte serialization. + #[cfg(feature = "internals")] + pub fn default_serialize(&self) -> Result, Error> { + let mut bytes = Vec::::new(); + + bytes.extend(::serialize(&self.R)?.as_ref()); + bytes.extend(<::Field>::serialize(&self.z).as_ref()); + + Ok(bytes) + } + + /// Converts this signature to its byte serialization. + pub fn serialize(&self) -> Result, Error> { + ::serialize_signature(self) + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for Signature +where + C: Ciphersuite, + C::Group: Group, + ::Field: Field, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serdect::slice::serialize_hex_lower_or_bin( + &self.serialize().map_err(serde::ser::Error::custom)?, + serializer, + ) + } +} + +#[cfg(feature = "serde")] +impl<'de, C> serde::Deserialize<'de> for Signature +where + C: Ciphersuite, + C::Group: Group, + ::Field: Field, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let bytes = serdect::slice::deserialize_hex_or_bin_vec(deserializer)?; + let signature = Signature::deserialize(&bytes) + .map_err(|err| serde::de::Error::custom(format!("{err}")))?; + Ok(signature) + } +} + +impl core::fmt::Debug for Signature { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.debug_struct("Signature") + .field( + "R", + &::serialize(&self.R) + .map(|s| hex::encode(s.as_ref())) + .unwrap_or("".to_string()), + ) + .field("z", &hex::encode(<::Field>::serialize(&self.z).as_ref())) + .finish() + } +} diff --git a/frost/src/signing_key.rs b/frost/src/signing_key.rs new file mode 100644 index 00000000..71b31b95 --- /dev/null +++ b/frost/src/signing_key.rs @@ -0,0 +1,104 @@ +//! Schnorr signature signing keys + +use alloc::vec::Vec; + +use rand_core::{CryptoRng, RngCore}; + +use crate::{ + random_nonzero, serialization::SerializableScalar, Challenge, Ciphersuite, Error, Field, Group, + Scalar, Signature, VerifyingKey, +}; + +/// A signing key for a Schnorr signature on a FROST [`Ciphersuite::Group`]. +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct SigningKey +where + C: Ciphersuite, +{ + pub(crate) scalar: Scalar, +} + +impl SigningKey +where + C: Ciphersuite, +{ + /// Generate a new signing key. + pub fn new(rng: &mut R) -> SigningKey { + let scalar = random_nonzero::(rng); + + SigningKey { scalar } + } + + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result, Error> { + Self::from_scalar(SerializableScalar::deserialize(bytes)?.0) + } + + /// Serialize `SigningKey` to bytes + pub fn serialize(&self) -> Vec { + SerializableScalar::(self.scalar).serialize() + } + + /// Create a signature `msg` using this `SigningKey`. + pub fn sign(&self, rng: R, message: &[u8]) -> Signature { + ::single_sign(self, rng, message) + } + + /// Create a signature `msg` using this `SigningKey` using the default + /// signing. + #[cfg(feature = "internals")] + pub fn default_sign(&self, mut rng: R, message: &[u8]) -> Signature { + let public = VerifyingKey::::from(*self); + + let (k, R) = ::generate_nonce(&mut rng); + + // Generate Schnorr challenge + let c: Challenge = ::challenge(&R, &public, message).expect("should not return error since that happens only if one of the inputs is the identity. R is not since k is nonzero. The verifying_key is not because signing keys are not allowed to be zero."); + + let z = k + (c.0 * self.scalar); + + Signature { R, z } + } + + /// Creates a SigningKey from a scalar. Returns an error if the scalar is zero. + pub fn from_scalar( + scalar: <<::Group as Group>::Field as Field>::Scalar, + ) -> Result> { + if scalar == <::Field as Field>::zero() { + return Err(Error::MalformedSigningKey); + } + Ok(Self { scalar }) + } + + /// Return the underlying scalar. + pub fn to_scalar(self) -> <<::Group as Group>::Field as Field>::Scalar { + self.scalar + } +} + +impl core::fmt::Debug for SigningKey +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("SigningKey").field(&"").finish() + } +} + +impl From<&SigningKey> for VerifyingKey +where + C: Ciphersuite, +{ + fn from(signing_key: &SigningKey) -> Self { + VerifyingKey::new(C::Group::generator() * signing_key.scalar) + } +} + +impl From> for VerifyingKey +where + C: Ciphersuite, +{ + fn from(signing_key: SigningKey) -> Self { + VerifyingKey::::from(&signing_key) + } +} diff --git a/frost/src/traits.rs b/frost/src/traits.rs new file mode 100644 index 00000000..ae9091fa --- /dev/null +++ b/frost/src/traits.rs @@ -0,0 +1,412 @@ +//! Traits used to abstract Ciphersuites. + +use core::{ + fmt::Debug, + ops::{Add, Mul, Sub}, +}; + +use alloc::{borrow::Cow, collections::BTreeMap, vec::Vec}; +use rand_core::{CryptoRng, RngCore}; + +use crate::{ + challenge, + keys::{KeyPackage, PublicKeyPackage, VerifyingShare}, + random_nonzero, + round1::{self}, + round2::{self, SignatureShare}, + BindingFactor, Challenge, Error, FieldError, GroupCommitment, GroupError, Identifier, + Signature, SigningKey, SigningPackage, VerifyingKey, +}; + +/// A prime order finite field GF(q) over which all scalar values for our prime order group can be +/// multiplied are defined. +/// +/// This trait does not have to be implemented for a finite field scalar itself, it can be a +/// pass-through, implemented for a type just for the ciphersuite, and calls through to another +/// implementation underneath, so that this trait does not have to be implemented for types you +/// don't own. +pub trait Field: Copy + Clone { + /// An element of the scalar field GF(p). + /// The Eq/PartialEq implementation MUST be constant-time. + type Scalar: Add + + Copy + + Clone + + Eq + + Mul + + PartialEq + + Sub; + + /// A unique byte array buf of fixed length N. + type Serialization: AsRef<[u8]> + Debug + TryFrom>; + + /// Returns the zero element of the field, the additive identity. + fn zero() -> Self::Scalar; + + /// Returns the one element of the field, the multiplicative identity. + fn one() -> Self::Scalar; + + /// Computes the multiplicative inverse of an element of the scalar field, failing if the + /// element is zero. + fn invert(scalar: &Self::Scalar) -> Result; + + /// Generate a random scalar from the entire space [0, l-1] + /// + /// + fn random(rng: &mut R) -> Self::Scalar; + + /// A member function of a [`Field`] that maps a [`Scalar`] to a unique byte array buf of + /// fixed length Ne. + /// + /// + fn serialize(scalar: &Self::Scalar) -> Self::Serialization; + + /// A member function of a [`Field`] that maps a [`Scalar`] to a unique byte array buf of + /// fixed length Ne, in little-endian order. + /// + /// This is used internally. + fn little_endian_serialize(scalar: &Self::Scalar) -> Self::Serialization; + + /// A member function of a [`Field`] that attempts to map a byte array `buf` to a [`Scalar`]. + /// + /// Fails if the input is not a valid byte representation of an [`Scalar`] of the + /// [`Field`]. This function can raise an [`Error`] if deserialization fails. + /// + /// + fn deserialize(buf: &Self::Serialization) -> Result; +} + +/// An element of the [`Ciphersuite`] `C`'s [`Group`]'s scalar [`Field`]. +pub type Scalar = <<::Group as Group>::Field as Field>::Scalar; + +/// A prime-order group (or subgroup) that provides everything we need to create and verify Schnorr +/// signatures. +/// +/// This trait does not have to be implemented for the curve/element/point itself, it can be a +/// pass-through, implemented for a type just for the ciphersuite, and calls through to another +/// implementation underneath, so that this trait does not have to be implemented for types you +/// don't own. +pub trait Group: Copy + Clone + PartialEq { + /// A prime order finite field GF(q) over which all scalar values for our prime order group can + /// be multiplied are defined. + type Field: Field; + + /// An element of our group that we will be computing over. + type Element: Add + + Copy + + Clone + + Eq + + Mul<::Scalar, Output = Self::Element> + + PartialEq + + Sub; + + /// A unique byte array buf of fixed length N. + /// + /// Little-endian! + type Serialization: AsRef<[u8]> + Debug + TryFrom>; + + /// The order of the the quotient group when the prime order subgroup divides the order of the + /// full curve group. + /// + /// If using a prime order elliptic curve, the cofactor should be 1 in the scalar field. + fn cofactor() -> ::Scalar; + + /// Additive [identity] of the prime order group. + /// + /// [identity]: https://datatracker.ietf.org/doc/html/rfc9591#section-3.1-4.4 + fn identity() -> Self::Element; + + /// The fixed generator element of the prime order group. + /// + /// The 'base' of ['ScalarBaseMult()'] from the spec. + /// + /// [`ScalarBaseMult()`]: https://datatracker.ietf.org/doc/html/rfc9591#section-3.1-4.10 + fn generator() -> Self::Element; + + /// A member function of a group _G_ that maps an [`Element`] to a unique + /// byte array buf of fixed length Ne. This function raises an error if the + /// element is the identity element of the group. + /// + /// + fn serialize(element: &Self::Element) -> Result; + + /// A member function of a [`Group`] that attempts to map a byte array `buf` to an [`Element`]. + /// + /// Fails if the input is not a valid byte representation of an [`Element`] of the + /// [`Group`]. This function can raise an [`Error`] if deserialization fails or if the + /// resulting [`Element`] is the identity element of the group + /// + /// + fn deserialize(buf: &Self::Serialization) -> Result; +} + +/// An element of the [`Ciphersuite`] `C`'s [`Group`]. +pub type Element = <::Group as Group>::Element; + +/// A [FROST ciphersuite] specifies the underlying prime-order group details and cryptographic hash +/// function. +/// +/// [FROST ciphersuite]: https://datatracker.ietf.org/doc/html/rfc9591#name-ciphersuites +// See https://github.com/ZcashFoundation/frost/issues/693 for reasoning about the 'static bound. +pub trait Ciphersuite: Copy + Clone + PartialEq + Debug + 'static { + /// The ciphersuite ID string. It should be equal to the contextString in + /// the spec. For new ciphersuites, this should be a string that identifies + /// the ciphersuite; it's recommended to use a similar format to the + /// ciphersuites in the FROST spec, e.g. "FROST-RISTRETTO255-SHA512-v1". + const ID: &'static str; + + /// The prime order group (or subgroup) that this ciphersuite operates over. + type Group: Group; + + /// A unique byte array of fixed length. + type HashOutput: AsRef<[u8]>; + + /// A unique byte array of fixed length that is the `Group::ElementSerialization` + + /// `Group::ScalarSerialization` + type SignatureSerialization: AsRef<[u8]> + TryFrom>; + + /// [H1] for a FROST ciphersuite. + /// + /// Maps arbitrary inputs to `Self::Scalar` elements of the prime-order group scalar field. + /// + /// [H1]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function + fn H1(m: &[u8]) -> <::Field as Field>::Scalar; + + /// [H2] for a FROST ciphersuite. + /// + /// Maps arbitrary inputs to `Self::Scalar` elements of the prime-order group scalar field. + /// + /// [H2]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function + fn H2(m: &[u8]) -> <::Field as Field>::Scalar; + + /// [H3] for a FROST ciphersuite. + /// + /// Maps arbitrary inputs to `Self::Scalar` elements of the prime-order group scalar field. + /// + /// [H3]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function + fn H3(m: &[u8]) -> <::Field as Field>::Scalar; + + /// [H4] for a FROST ciphersuite. + /// + /// Usually an an alias for the ciphersuite hash function _H_ with domain separation applied. + /// + /// [H4]: https://datatracker.ietf.org/doc/html/rfc9591#name-cryptographic-hash-function + fn H4(m: &[u8]) -> Self::HashOutput; + + /// [H5] for a FROST ciphersuite. + /// + /// Usually an an alias for the ciphersuite hash function _H_ with domain separation applied. + /// + /// [H5]: https://github.com/cfrg/draft-irtf-cfrg-frost/blob/master/draft-irtf-cfrg-frost.md#cryptographic-hash + fn H5(m: &[u8]) -> Self::HashOutput; + + /// Hash function for a FROST ciphersuite, used for the DKG. + /// + /// The DKG it not part of the specification, thus this is optional. + /// It can return None if DKG is not supported by the Ciphersuite. This is + /// the default implementation. + /// + /// Maps arbitrary inputs to non-zero `Self::Scalar` elements of the prime-order group scalar field. + fn HDKG(_m: &[u8]) -> Option<<::Field as Field>::Scalar> { + None + } + + /// Hash function for a FROST ciphersuite, used for deriving identifiers from strings. + /// + /// This feature is not part of the specification and is just a convenient + /// way of creating identifiers. Therefore it can return None if this is not supported by the + /// Ciphersuite. This is the default implementation. + /// + /// Maps arbitrary inputs to non-zero `Self::Scalar` elements of the prime-order group scalar field. + fn HID(_m: &[u8]) -> Option<<::Field as Field>::Scalar> { + None + } + + // The following are optional methods that allow customizing steps of the + // protocol if required. + + /// Optional. Do regular (non-FROST) signing with a [`SigningKey`]. Called + /// by [`SigningKey::sign()`]. This is not used by FROST. Can be overriden + /// if required which is useful if FROST signing has been changed by the + /// other Ciphersuite trait methods and regular signing should be changed + /// accordingly to match. + fn single_sign( + signing_key: &SigningKey, + rng: R, + message: &[u8], + ) -> Signature { + signing_key.sign(rng, message) + } + + /// Optional. Verify a signature for this ciphersuite. Called by + /// [`VerifyingKey::verify()`]. The default implementation uses the + /// "cofactored" equation (it multiplies by the cofactor returned by + /// [`Group::cofactor()`]). + /// + /// # Cryptographic Safety + /// + /// You may override this to provide a tailored implementation, but if the + /// ciphersuite defines it, it must also multiply by the cofactor to comply + /// with the RFC. Note that batch verification (see + /// [`crate::batch::Verifier`]) also uses the default implementation + /// regardless whether a tailored implementation was provided. + fn verify_signature( + message: &[u8], + signature: &Signature, + public_key: &VerifyingKey, + ) -> Result<(), Error> { + let (message, signature, public_key) = ::pre_verify(message, signature, public_key)?; + + let c = ::challenge(&signature.R, &public_key, &message)?; + + public_key.verify_prehashed(c, &signature) + } + + /// Optional. Pre-process [`round2::sign()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_sign<'a>( + signing_package: &'a SigningPackage, + signer_nonces: &'a round1::SigningNonces, + key_package: &'a KeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, round1::SigningNonces>, + Cow<'a, KeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signer_nonces), + Cow::Borrowed(key_package), + )) + } + + /// Optional. Pre-process [`crate::aggregate()`] and + /// [`crate::verify_signature_share()`] inputs. In the latter case, "dummy" + /// container BTreeMap and PublicKeyPackage are passed with the relevant + /// values. The default implementation returns them as-is. [`Cow`] is used + /// so implementations can choose to return the same passed reference or a + /// modified clone. + #[allow(clippy::type_complexity)] + fn pre_aggregate<'a>( + signing_package: &'a SigningPackage, + signature_shares: &'a BTreeMap, round2::SignatureShare>, + public_key_package: &'a PublicKeyPackage, + ) -> Result< + ( + Cow<'a, SigningPackage>, + Cow<'a, BTreeMap, round2::SignatureShare>>, + Cow<'a, PublicKeyPackage>, + ), + Error, + > { + Ok(( + Cow::Borrowed(signing_package), + Cow::Borrowed(signature_shares), + Cow::Borrowed(public_key_package), + )) + } + + /// Optional. Pre-process [`VerifyingKey::verify()`] inputs. The default + /// implementation returns them as-is. [`Cow`] is used so implementations + /// can choose to return the same passed reference or a modified clone. + #[allow(clippy::type_complexity)] + fn pre_verify<'a>( + msg: &'a [u8], + signature: &'a Signature, + public_key: &'a VerifyingKey, + ) -> Result<(Cow<'a, [u8]>, Cow<'a, Signature>, Cow<'a, VerifyingKey>), Error> + { + Ok((Cow::Borrowed(msg), Cow::Borrowed(signature), Cow::Borrowed(public_key))) + } + + /// Optional. Generate a nonce and a commitment to it. Used by + /// [`SigningKey`] for regular (non-FROST) signing and internally by the DKG + /// to generate proof-of-knowledge signatures. + fn generate_nonce( + rng: &mut R, + ) -> (<::Field as Field>::Scalar, ::Element) { + let k = random_nonzero::(rng); + let R = ::generator() * k; + (k, R) + } + + /// Optional. Generates the challenge as is required for Schnorr signatures. + /// Called by [`round2::sign()`] and [`crate::aggregate()`]. + fn challenge( + R: &Element, + verifying_key: &VerifyingKey, + message: &[u8], + ) -> Result, Error> { + challenge(R, verifying_key, message) + } + + /// Optional. Compute the signature share for a particular signer on a given + /// challenge. Called by [`round2::sign()`]. + fn compute_signature_share( + _group_commitment: &GroupCommitment, + signer_nonces: &round1::SigningNonces, + binding_factor: BindingFactor, + lambda_i: <::Field as Field>::Scalar, + key_package: &KeyPackage, + challenge: Challenge, + ) -> round2::SignatureShare { + round2::compute_signature_share( + signer_nonces, + binding_factor, + lambda_i, + key_package, + challenge, + ) + } + + /// Optional. Verify a signing share. Called by [`crate::aggregate()`] if + /// cheater detection is enabled. + fn verify_share( + _group_commitment: &GroupCommitment, + signature_share: &SignatureShare, + identifier: Identifier, + group_commitment_share: &round1::GroupCommitmentShare, + verifying_share: &VerifyingShare, + lambda_i: Scalar, + challenge: &Challenge, + ) -> Result<(), Error> { + signature_share.verify( + identifier, + group_commitment_share, + verifying_share, + lambda_i, + challenge, + ) + } + + /// Optional. Converts a signature to its + /// [`Ciphersuite::SignatureSerialization`] in bytes. + /// + /// The default implementation serializes a signature by serializing its `R` + /// point and `z` component independently, and then concatenating them. + fn serialize_signature(signature: &Signature) -> Result, Error> { + signature.serialize() + } + + /// Optional. Converts bytes as [`Ciphersuite::SignatureSerialization`] into + /// a `Signature`. + /// + /// The default implementation assumes the serialization is a serialized `R` + /// point followed by a serialized `z` component with no padding or extra + /// fields. + fn deserialize_signature(bytes: &[u8]) -> Result, Error> { + Signature::::deserialize(bytes) + } + + /// Post-process the output of the DKG for a given participant. + fn post_dkg( + key_package: KeyPackage, + public_key_package: PublicKeyPackage, + ) -> Result<(KeyPackage, PublicKeyPackage), Error> { + Ok((key_package, public_key_package)) + } +} diff --git a/frost/src/verifying_key.rs b/frost/src/verifying_key.rs new file mode 100644 index 00000000..02301a2a --- /dev/null +++ b/frost/src/verifying_key.rs @@ -0,0 +1,112 @@ +use core::fmt::{self, Debug}; + +use alloc::{string::ToString, vec::Vec}; + +#[cfg(any(test, feature = "test-impl"))] +use hex::FromHex; + +use crate::{serialization::SerializableElement, Challenge, Ciphersuite, Error, Group, Signature}; + +/// A valid verifying key for Schnorr signatures over a FROST [`Ciphersuite::Group`]. +#[derive(Copy, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound = "C: Ciphersuite"))] +#[cfg_attr(feature = "serde", serde(transparent))] +pub struct VerifyingKey +where + C: Ciphersuite, +{ + pub(crate) element: SerializableElement, +} + +impl VerifyingKey +where + C: Ciphersuite, +{ + /// Create a new VerifyingKey from the given element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn new(element: ::Element) -> Self { + Self { element: SerializableElement(element) } + } + + /// Return the underlying element. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn to_element(self) -> ::Element { + self.element.0 + } + + /// Deserialize from bytes + pub fn deserialize(bytes: &[u8]) -> Result, Error> { + Ok(Self::new(SerializableElement::deserialize(bytes)?.0)) + } + + /// Serialize `VerifyingKey` to bytes + pub fn serialize(&self) -> Result, Error> { + self.element.serialize() + } + + /// Verify a purported `signature` with a pre-hashed [`Challenge`] made by this verification + /// key. + #[cfg_attr(feature = "internals", visibility::make(pub))] + #[cfg_attr(docsrs, doc(cfg(feature = "internals")))] + pub(crate) fn verify_prehashed( + &self, + challenge: Challenge, + signature: &Signature, + ) -> Result<(), Error> { + // Verify check is h * ( - z * B + R + c * A) == 0 + // h * ( z * B - c * A - R) == 0 + // + // where h is the cofactor + let zB = C::Group::generator() * signature.z; + let cA = self.element.0 * challenge.0; + let check = (zB - cA - signature.R) * C::Group::cofactor(); + + if check == C::Group::identity() { + Ok(()) + } else { + Err(Error::InvalidSignature) + } + } + + /// Verify a purported `signature` over `msg` made by this verification key. + pub fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { + C::verify_signature(msg, signature, self) + } + + /// Computes the group public key given the group commitment. + #[cfg_attr(feature = "internals", visibility::make(pub))] + pub(crate) fn from_commitment( + commitment: &crate::keys::VerifiableSecretSharingCommitment, + ) -> Result, Error> { + Ok(VerifyingKey::new( + commitment.coefficients().first().ok_or(Error::IncorrectCommitment)?.value(), + )) + } +} + +impl Debug for VerifyingKey +where + C: Ciphersuite, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_tuple("VerifyingKey") + .field(&self.serialize().map(hex::encode).unwrap_or("".to_string())) + .finish() + } +} + +#[cfg(any(test, feature = "test-impl"))] +impl FromHex for VerifyingKey +where + C: Ciphersuite, +{ + type Error = &'static str; + + fn from_hex>(hex: T) -> Result { + let v: Vec = FromHex::from_hex(hex).map_err(|_| "invalid hex")?; + Self::deserialize(&v).map_err(|_| "malformed verifying key encoding") + } +} diff --git a/precompiles/README.md b/precompiles/README.md index 952402ba..923b7d66 100644 --- a/precompiles/README.md +++ b/precompiles/README.md @@ -2,61 +2,53 @@ Following are the precompiles supported by Tangle network - ## Precompiled Contract Addresses -- 0-1023 - Ethereum MainNet precompiles -- 1024-2047 - precompiles that are not in Ethereum and not Tangle specific -- 2048-4095 - Tangle specific precompiles -### Ethereum MainNet Precompiles +- 0-1023 - Ethereum MainNet precompiles +- 1024-2047 - precompiles that are not in Ethereum and not Tangle specific +- 2048-4095 - Tangle specific precompiles -| Contract | Address | -|----------------------------|----------------------------------------------| -| ECRECOVER | 0x0000000000000000000000000000000000000001 | -| SHA256 | 0x0000000000000000000000000000000000000002 | -| RIPEMD160 | 0x0000000000000000000000000000000000000003 | -| Identity | 0x0000000000000000000000000000000000000004 | -| Modular Exponentiation | 0x0000000000000000000000000000000000000005 | -| BN128Add | 0x0000000000000000000000000000000000000006 | -| BN128Mul | 0x0000000000000000000000000000000000000007 | -| BN128Pairing | 0x0000000000000000000000000000000000000008 | -| Blake2 | 0x0000000000000000000000000000000000000009 | +### Ethereum MainNet Precompiles +| Contract | Address | +| ---------------------- | ------------------------------------------ | +| ECRECOVER | 0x0000000000000000000000000000000000000001 | +| SHA256 | 0x0000000000000000000000000000000000000002 | +| RIPEMD160 | 0x0000000000000000000000000000000000000003 | +| Identity | 0x0000000000000000000000000000000000000004 | +| Modular Exponentiation | 0x0000000000000000000000000000000000000005 | +| BN128Add | 0x0000000000000000000000000000000000000006 | +| BN128Mul | 0x0000000000000000000000000000000000000007 | +| BN128Pairing | 0x0000000000000000000000000000000000000008 | +| Blake2 | 0x0000000000000000000000000000000000000009 | ### Non-Tangle Specific nor Ethereum Precompiles -| Contract | Address | -|----------------------|----------------------------------------------| -| SHA3FIPS256 | 0x0000000000000000000000000000000000000400 | -| ECRecoverPublicKey | 0x0000000000000000000000000000000000000402 | - +| Contract | Address | +| ------------------ | ------------------------------------------ | +| SHA3FIPS256 | 0x0000000000000000000000000000000000000400 | +| ECRecoverPublicKey | 0x0000000000000000000000000000000000000402 | ### Tangle Specific Precompiles -| Contract | Address | -|----------------------|----------------------------------------------| -| Staking | 0x0000000000000000000000000000000000000800 | -| Vesting | 0x0000000000000000000000000000000000000801 | -| Erc-20 Balances | 0x0000000000000000000000000000000000000802 | -| Democracy | 0x0000000000000000000000000000000000000803 | -| Batch | 0x0000000000000000000000000000000000000804 | -| Call Permit | 0x0000000000000000000000000000000000000805 | -| Preimage | 0x0000000000000000000000000000000000000806 | -| Registry | 0x0000000000000000000000000000000000000807 | -| Ecdsa-Secp256k1 | 0x0000000000000000000000000000000000000816 | -| Ecdsa-Secp256r1 | 0x0000000000000000000000000000000000000817 | -| Ecdsa-Stark | 0x0000000000000000000000000000000000000818 | -| Schnorr-Sr25519 | 0x0000000000000000000000000000000000000819 | -| Schnorr-Secp256k | 0x000000000000000000000000000000000000081a | -| Schnorr-Ed25519 | 0x000000000000000000000000000000000000081b | -| Schnorr-Ed448 | 0x000000000000000000000000000000000000081c | -| Schnorr-P256 | 0x000000000000000000000000000000000000081d | -| Schnorr-P384 | 0x000000000000000000000000000000000000081e | -| Schnorr-Ristretto255 | 0x000000000000000000000000000000000000081f | -| Schnorr-Taproot | 0x0000000000000000000000000000000000000820 | -| Bls12-381 | 0x0000000000000000000000000000000000000821 | - - - - - +| Contract | Address | +| -------------------- | ------------------------------------------ | +| Staking | 0x0000000000000000000000000000000000000800 | +| Vesting | 0x0000000000000000000000000000000000000801 | +| Erc-20 Balances | 0x0000000000000000000000000000000000000802 | +| Democracy | 0x0000000000000000000000000000000000000803 | +| Batch | 0x0000000000000000000000000000000000000804 | +| Call Permit | 0x0000000000000000000000000000000000000805 | +| Preimage | 0x0000000000000000000000000000000000000806 | +| Registry | 0x0000000000000000000000000000000000000807 | +| Ecdsa-Secp256k1 | 0x0000000000000000000000000000000000000816 | +| Ecdsa-Secp256r1 | 0x0000000000000000000000000000000000000817 | +| Ecdsa-Stark | 0x0000000000000000000000000000000000000818 | +| Schnorr-Sr25519 | 0x0000000000000000000000000000000000000819 | +| Schnorr-Secp256k | 0x000000000000000000000000000000000000081a | +| Schnorr-Ed25519 | 0x000000000000000000000000000000000000081b | +| Schnorr-P256 | 0x000000000000000000000000000000000000081d | +| Schnorr-P384 | 0x000000000000000000000000000000000000081e | +| Schnorr-Ristretto255 | 0x000000000000000000000000000000000000081f | +| Schnorr-Taproot | 0x0000000000000000000000000000000000000820 | +| Bls12-381 | 0x0000000000000000000000000000000000000821 | diff --git a/precompiles/verify-schnorr-signatures/Cargo.toml b/precompiles/verify-schnorr-signatures/Cargo.toml index bc704d04..d3a7eb57 100644 --- a/precompiles/verify-schnorr-signatures/Cargo.toml +++ b/precompiles/verify-schnorr-signatures/Cargo.toml @@ -8,13 +8,19 @@ description = "A Precompile to verify schnorr signatures" [dependencies] precompile-utils = { workspace = true } +# Using remote crates frost-core = { workspace = true, default-features = false } frost-ed25519 = { workspace = true, default-features = false } -frost-ed448 = { workspace = true, default-features = false } frost-ristretto255 = { workspace = true, default-features = false } frost-secp256k1 = { workspace = true, default-features = false } frost-p256 = { workspace = true, default-features = false } +# Using local crates +tg-frost-core = { workspace = true, default-features = false } +frost-p384 = { workspace = true, default-features = false } +frost-secp256k1-tr = { workspace = true, default-features = false } +frost-ed448 = { workspace = true, default-features = false } + # Substrate sp-core = { workspace = true } sp-std = { workspace = true } @@ -54,8 +60,11 @@ std = [ "sp-io/std", "frost-core/std", "frost-ed25519/std", - "frost-ed448/std", "frost-ristretto255/std", "frost-secp256k1/std", "frost-p256/std", + "tg-frost-core/std", + "frost-p384/std", + "frost-secp256k1-tr/std", + "frost-ed448/std", ] diff --git a/precompiles/verify-schnorr-signatures/src/lib.rs b/precompiles/verify-schnorr-signatures/src/lib.rs index d604aa76..0b403c6d 100644 --- a/precompiles/verify-schnorr-signatures/src/lib.rs +++ b/precompiles/verify-schnorr-signatures/src/lib.rs @@ -24,11 +24,16 @@ use sp_std::{marker::PhantomData, prelude::*}; use frost_core::{Signature, VerifyingKey}; use frost_ed25519::Ed25519Sha512; -use frost_ed448::Ed448Shake256; use frost_p256::P256Sha256; + use frost_ristretto255::Ristretto255Sha512; use frost_secp256k1::Secp256K1Sha256; +use frost_ed448::Ed448Shake256; +use frost_p384::P384Sha384; +use frost_secp256k1_tr::Secp256K1Sha256TR; +use tg_frost_core; + #[cfg(test)] mod mock; #[cfg(test)] @@ -47,6 +52,18 @@ macro_rules! verify_signature { }}; } +macro_rules! verify_tg_frost_signature { + ($impl_type:ty, $key:expr, $signature:expr, $msg:expr, $key_default:expr, $sig_default:expr) => {{ + let verifying_key: tg_frost_core::VerifyingKey<$impl_type> = + tg_frost_core::VerifyingKey::deserialize($key.try_into().unwrap_or($key_default)) + .map_err(|_| revert("InvalidVerifyingKeyDeserialization"))?; + let sig: tg_frost_core::Signature<$impl_type> = + tg_frost_core::Signature::deserialize($signature.try_into().unwrap_or($sig_default)) + .map_err(|_| revert("InvalidSignatureDeserialization"))?; + verifying_key.verify($msg, &sig).map_err(|_| revert("InvalidSignature"))? + }}; +} + /// Utility function to create slice of fixed size pub fn to_slice_32(val: &[u8]) -> Option<[u8; 32]> { if val.len() == 32 { @@ -156,6 +173,70 @@ impl SchnorrEd25519Precompile { } } +/// A precompile to verify SchnorrP256 signature +pub struct SchnorrP256Precompile(PhantomData); + +#[precompile_utils::precompile] +impl SchnorrP256Precompile { + #[precompile::public("verify(bytes,bytes,bytes)")] + #[precompile::view] + fn verify( + _handle: &mut impl PrecompileHandle, + public_bytes: BoundedBytes>, + signature_bytes: BoundedBytes>, + message: UnboundedBytes, + ) -> EvmResult { + // Parse arguments + let public_bytes: Vec = public_bytes.into(); + let signature_bytes: Vec = signature_bytes.into(); + let message: Vec = message.into(); + + verify_signature!( + P256Sha256, + public_bytes.as_slice(), + signature_bytes.as_slice(), + &message, + &[0u8; 33], + &[0u8; 65] + ); + + Ok(false) + } +} + +/// A precompile to verify SchnorrRistretto255 signature +pub struct SchnorrRistretto255Precompile(PhantomData); + +#[precompile_utils::precompile] +impl SchnorrRistretto255Precompile { + #[precompile::public("verify(bytes,bytes,bytes)")] + #[precompile::view] + fn verify( + _handle: &mut impl PrecompileHandle, + public_bytes: BoundedBytes>, + signature_bytes: BoundedBytes>, + message: UnboundedBytes, + ) -> EvmResult { + // Parse arguments + let public_bytes: Vec = public_bytes.into(); + let signature_bytes: Vec = signature_bytes.into(); + let message: Vec = message.into(); + + verify_signature!( + Ristretto255Sha512, + public_bytes.as_slice(), + signature_bytes.as_slice(), + &message, + &[0u8; 32], + &[0u8; 64] + ); + + Ok(false) + } +} + +/* THESE LIBS USING LOCAL CUSTOM TG FROST CORE DUE TO NO_STD AND PUBLIC ISSUES */ + /// A precompile to verify SchnorrEd448 signature pub struct SchnorrEd448Precompile(PhantomData); @@ -174,7 +255,7 @@ impl SchnorrEd448Precompile { let signature_bytes: Vec = signature_bytes.into(); let message: Vec = message.into(); - verify_signature!( + verify_tg_frost_signature!( Ed448Shake256, public_bytes.as_slice(), signature_bytes.as_slice(), @@ -187,11 +268,11 @@ impl SchnorrEd448Precompile { } } -/// A precompile to verify SchnorrP256 signature -pub struct SchnorrP256Precompile(PhantomData); +/// A precompile to verify SchnorrTaproot signature +pub struct SchnorrTaprootPrecompile(PhantomData); #[precompile_utils::precompile] -impl SchnorrP256Precompile { +impl SchnorrTaprootPrecompile { #[precompile::public("verify(bytes,bytes,bytes)")] #[precompile::view] fn verify( @@ -205,8 +286,8 @@ impl SchnorrP256Precompile { let signature_bytes: Vec = signature_bytes.into(); let message: Vec = message.into(); - verify_signature!( - P256Sha256, + verify_tg_frost_signature!( + Secp256K1Sha256TR, public_bytes.as_slice(), signature_bytes.as_slice(), &message, @@ -218,17 +299,17 @@ impl SchnorrP256Precompile { } } -/// A precompile to verify SchnorrRistretto255 signature -pub struct SchnorrRistretto255Precompile(PhantomData); +/// A precompile to verify SchnorrP384 signature +pub struct SchnorrP384Precompile(PhantomData); #[precompile_utils::precompile] -impl SchnorrRistretto255Precompile { +impl SchnorrP384Precompile { #[precompile::public("verify(bytes,bytes,bytes)")] #[precompile::view] fn verify( _handle: &mut impl PrecompileHandle, - public_bytes: BoundedBytes>, - signature_bytes: BoundedBytes>, + public_bytes: BoundedBytes>, + signature_bytes: BoundedBytes>, message: UnboundedBytes, ) -> EvmResult { // Parse arguments @@ -236,13 +317,13 @@ impl SchnorrRistretto255Precompile { let signature_bytes: Vec = signature_bytes.into(); let message: Vec = message.into(); - verify_signature!( - Ristretto255Sha512, + verify_tg_frost_signature!( + P384Sha384, public_bytes.as_slice(), signature_bytes.as_slice(), &message, - &[0u8; 32], - &[0u8; 64] + &[0u8; 49], + &[0u8; 97] ); Ok(false) diff --git a/primitives/src/services/mod.rs b/primitives/src/services/mod.rs index 4531dd5f..3eb8669b 100644 --- a/primitives/src/services/mod.rs +++ b/primitives/src/services/mod.rs @@ -22,7 +22,7 @@ use frame_support::pallet_prelude::*; use serde::Deserializer; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; -use sp_core::{ecdsa, ByteArray, RuntimeDebug, H160, U256}; +use sp_core::{ByteArray, RuntimeDebug, H160, U256}; use sp_runtime::Percent; #[cfg(not(feature = "std"))]