diff --git a/Cargo.lock b/Cargo.lock index d8005967..6c5b0099 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,9 +40,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "base16ct" -version = "0.2.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" [[package]] name = "base58ck" @@ -229,6 +229,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + [[package]] name = "blst" version = "0.3.17" @@ -365,6 +374,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "condtype" version = "1.3.0" @@ -385,9 +400,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.6" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "core-foundation-sys" @@ -395,6 +410,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -404,6 +425,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.7" @@ -437,12 +467,15 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.5.5" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" dependencies = [ - "generic-array", - "rand_core 0.6.4", + "cpubits", + "ctutils", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", "subtle", "zeroize", ] @@ -457,6 +490,26 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + [[package]] name = "dash-dev" version = "0.0.0" @@ -547,12 +600,15 @@ dependencies = [ "dash-num", "dash-types", "divan", + "ff", + "getrandom 0.4.3", + "group", "hex-conservative 0.3.2", "k256", - "rand_core 0.6.4", + "rand_core 0.10.1", "rstest", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -633,9 +689,9 @@ dependencies = [ [[package]] name = "der" -version = "0.7.10" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" dependencies = [ "const-oid", "zeroize", @@ -647,10 +703,20 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", "const-oid", - "crypto-common", - "subtle", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -691,15 +757,16 @@ dependencies = [ [[package]] name = "ecdsa" -version = "0.16.9" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" dependencies = [ "der", - "digest", + "digest 0.11.3", "elliptic-curve", "rfc6979", "signature", + "zeroize", ] [[package]] @@ -710,17 +777,18 @@ checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "elliptic-curve" -version = "0.13.8" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" dependencies = [ "base16ct", "crypto-bigint", - "digest", + "crypto-common 0.2.2", + "digest 0.11.3", "ff", - "generic-array", "group", - "rand_core 0.6.4", + "hybrid-array", + "rand_core 0.10.1", "sec1", "subtle", "zeroize", @@ -750,11 +818,11 @@ dependencies = [ [[package]] name = "ff" -version = "0.13.1" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" dependencies = [ - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -829,18 +897,6 @@ checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" dependencies = [ "typenum", "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", ] [[package]] @@ -864,6 +920,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -887,12 +944,12 @@ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" [[package]] name = "group" -version = "0.13.0" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" dependencies = [ "ff", - "rand_core 0.6.4", + "rand_core 0.10.1", "subtle", ] @@ -951,11 +1008,22 @@ checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" [[package]] name = "hmac" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", +] + +[[package]] +name = "hybrid-array" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707114b52a152fa7bdb290cd7cd5912d9467273b6d74e21b8d81aca1f8533f6b" +dependencies = [ + "subtle", + "typenum", + "zeroize", ] [[package]] @@ -1147,14 +1215,16 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.4" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +checksum = "93f50113171a713f4a4231ef82eb26703607139b35dcb56241f0ceab2ae1f7d8" dependencies = [ - "cfg-if", + "cpubits", "ecdsa", "elliptic-curve", - "sha2", + "primeorder", + "sha2 0.11.0", + "wnaf", ] [[package]] @@ -1351,6 +1421,31 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "primefield", + "wnaf", +] + [[package]] name = "proc-macro-crate" version = "3.5.0" @@ -1412,21 +1507,18 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" dependencies = [ - "getrandom 0.2.17", + "getrandom 0.3.4", ] [[package]] name = "rand_core" -version = "0.9.5" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" [[package]] name = "rayon" @@ -1491,12 +1583,12 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rfc6979" -version = "0.4.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" dependencies = [ + "crypto-bigint", "hmac", - "subtle", ] [[package]] @@ -1559,13 +1651,14 @@ checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" [[package]] name = "sec1" -version = "0.7.3" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" dependencies = [ "base16ct", + "ctutils", "der", - "generic-array", + "hybrid-array", "subtle", "zeroize", ] @@ -1658,8 +1751,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -1670,12 +1774,12 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" [[package]] name = "signature" -version = "2.2.0" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" dependencies = [ - "digest", - "rand_core 0.6.4", + "digest 0.11.3", + "rand_core 0.10.1", ] [[package]] @@ -1896,12 +2000,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -2206,6 +2304,17 @@ version = "0.57.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/contrib/codeql/lib/policy.qll b/contrib/codeql/lib/policy.qll index 68687ae3..d2e1a260 100644 --- a/contrib/codeql/lib/policy.qll +++ b/contrib/codeql/lib/policy.qll @@ -51,12 +51,17 @@ predicate isSecretType(TypeItem t) { // Scalar field wrapper holding secret key material t.getName().getText() = "Fr" ) and - // A share *of a signature* is published, so it holds nothing to protect. Excluded by - // exact name because `BlsSkShare` and `RawShare` match the same Share substring - // and do carry secret scalars. - not t.getName().getText() = "BlsSigShare" and - // Serde artifact to deserialize a tagged enum. - not t.getName().getText() = "__Seed" + not t.getName().getText() = + [ + // A share *of a signature* is published, so it's non-secret. Excluded by exact name because + // `BlsSkShare` and `RawShare` match the same Share substring and do carry secret scalars. + "BlsSigShare", + // The identifier a share is issued against is the participant's, known to every member of the + // quorum; only the scalar the share carries is secret. + "BlsShareId", + // Serde artifact to deserialize a tagged enum. + "__Seed" + ] } /** diff --git a/pkgs/pkc/Cargo.toml b/pkgs/pkc/Cargo.toml index da942bf8..d81caacd 100644 --- a/pkgs/pkc/Cargo.toml +++ b/pkgs/pkc/Cargo.toml @@ -8,18 +8,20 @@ license = "MIT" base58ck = { workspace = true, features = ["alloc"] } bitcoin_hashes = { workspace = true, features = ["alloc"] } blst = { version = "0.3", default-features = false, optional = true } +ff = { version = "0.14", default-features = false, optional = true } +group = { version = "0.14", default-features = false, optional = true } cfg-if = "1" dash-num = { version = "0.0.0", path = "../num" } dash-types = { version = "0.0.0", path = "../types", default-features = false } hex-conservative = { version = "0.3", default-features = false, features = [ "alloc", ] } -k256 = { version = "0.13", default-features = false, features = [ +k256 = { version = "0.14", default-features = false, features = [ "arithmetic", "ecdsa", "sha256", ], optional = true } -rand_core = { version = "0.6", default-features = false, optional = true } +rand_core = { version = "0.10", default-features = false, optional = true } rstest = { version = "0.25", optional = true } serde = { version = "1", default-features = false, features = [ "alloc", @@ -36,15 +38,16 @@ zeroize = { version = "1", default-features = false, features = [ bitcoin-consensus-encoding = { workspace = true, features = ["alloc"] } dash-dev = { version = "0.0.0", path = "../dev", features = ["full"] } divan = "0.1" +getrandom = { version = "0.4", features = ["sys_rng"] } hex-conservative = "0.3" -rand_core = { version = "0.6", features = ["getrandom"] } +rand_core = { version = "0.10" } rstest = "0.25" serde = { version = "1", features = ["derive"] } [features] default = [] std = ["base58ck/std", "bitcoin_hashes/std", "dash-types/std"] -bls = ["dep:blst", "dep:rand_core", "dep:sha2"] +bls = ["dep:blst", "dep:ff", "dep:group", "dep:rand_core", "dep:sha2"] ecdsa = ["dep:k256", "dep:rand_core"] serde = ["dep:serde", "dash-num/serde", "dash-types/serde"] full = ["ecdsa", "bls", "serde", "std", "tests"] diff --git a/pkgs/pkc/bench/bls.rs b/pkgs/pkc/bench/bls.rs index 235df9b8..d5bf202d 100644 --- a/pkgs/pkc/bench/bls.rs +++ b/pkgs/pkc/bench/bls.rs @@ -9,7 +9,8 @@ use dash_pkc::bls::tests::{sequential_ids, test_ikm, test_msg}; use dash_pkc::bls::{BlsPublicKey, BlsScChia, BlsScIetf, BlsScheme, BlsSecretKey, BlsSigShare, BlsSignature}; use divan::{counter::ItemsCount, Bencher}; -use rand_core::OsRng; +use getrandom::SysRng; +use rand_core::UnwrapErr; /// Single signature creation. #[divan::bench(types = [BlsScChia, BlsScIetf])] @@ -144,7 +145,7 @@ fn split_threshold(bencher: Bencher, n: usize) { let ids = sequential_ids(n); bencher .counter(ItemsCount::new(n)) - .bench(|| sk.split(threshold, &ids, &mut OsRng)); + .bench(|| sk.split(threshold, &ids, &mut UnwrapErr(SysRng))); } /// Threshold signature recovery via Lagrange interpolation. @@ -152,7 +153,7 @@ fn split_threshold(bencher: Bencher, n: usize) { fn recover_threshold(bencher: Bencher, threshold: usize) { let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); let ids = sequential_ids(threshold * 2); - let shares = sk.split(threshold, &ids, &mut OsRng).unwrap(); + let shares = sk.split(threshold, &ids, &mut UnwrapErr(SysRng)).unwrap(); let msg = test_msg(42); let sig_shares: Vec<_> = shares.iter().map(|share| share.sign(S::msg_ref(&msg))).collect(); let subset: Vec<&BlsSigShare> = sig_shares.iter().take(threshold).collect(); diff --git a/pkgs/pkc/src/bls/blst_ffi.rs b/pkgs/pkc/src/bls/blst_ffi.rs index 70473192..9e0e6617 100644 --- a/pkgs/pkc/src/bls/blst_ffi.rs +++ b/pkgs/pkc/src/bls/blst_ffi.rs @@ -6,18 +6,16 @@ //! Bridging routines for unsafe blst FFI operations. +use super::group::{G1Affine, G2Affine, Point, G1, G2}; +use super::scalar::{Fp, Fp2, Fr}; + use blst::*; use dash_types::type_cvrt; -use dash_types::type_id::Unencodable; -use zeroize::Zeroize; +use zeroize::{Zeroize, Zeroizing}; -use core::fmt; use core::ops::{Add, Mul, Neg, Sub}; use core::ptr::null_mut; -/// Bit-length for scalars known to be reduced mod q (< 2^255). -pub(crate) const FR_BITS: usize = 255; - /// Serialize a scalar to its 32-byte big-endian encoding. pub(crate) fn bendian_from_scalar(scalar: &blst_scalar) -> [u8; 32] { let mut out = [0u8; 32]; @@ -86,12 +84,47 @@ pub(crate) fn sk_to_pk2_in_g1(sk: &blst_scalar) -> G1Affine { aff.into() } -/// A scalar of the BLS12-381 scalar field, i.e. an integer reduced modulo the -/// group order `r`. -#[derive(Clone, Copy, Default)] -pub(crate) struct Fr(blst_fr); - impl Fr { + /// Doubles the element. + pub(crate) fn double(&self) -> Self { + let mut out = blst_fr::default(); + unsafe { blst_fr_lshift(&mut out, &self.0, 1) }; + Self(out) + } + + /// Reduces a small integer into the field. + pub(crate) fn from_u64(value: u64) -> Self { + let mut out = blst_fr::default(); + let limbs = [value, 0, 0, 0]; + unsafe { blst_fr_from_uint64(&mut out, limbs.as_ptr()) }; + Self(out) + } + + /// Parses a canonical little-endian encoding, rejecting anything at or above + /// the group order. + pub(crate) fn from_lendian(bytes: &[u8; 32]) -> Option { + let mut scalar = blst_scalar::default(); + unsafe { blst_scalar_from_lendian(&mut scalar, bytes.as_ptr()) }; + if unsafe { blst_scalar_fr_check(&scalar) } { + Some(Self::from(&scalar)) + } else { + None + } + } + + /// Reduces a wide little-endian integer into the field. + /// + /// Wider input than the modulus is the point; reducing 64 bytes into a + /// 255-bit field leaves a bias below `2^-250`, where rejection sampling would + /// need a loop and a branch on secret data. + pub(crate) fn from_lendian_reduce(bytes: &[u8]) -> Self { + let mut scalar = blst_scalar::default(); + unsafe { blst_scalar_from_le_bytes(&mut scalar, bytes.as_ptr(), bytes.len()) }; + let reduced = Self::from(&scalar); + scalar.b.zeroize(); + reduced + } + /// Multiplicative inverse; the inverse of zero is left unspecified. pub(crate) fn inverse(&self) -> Self { let mut out = blst_fr::default(); @@ -99,18 +132,19 @@ impl Fr { Self(out) } - /// The multiplicative identity, one. - pub(crate) fn one() -> Self { + /// Squares the element. + pub(crate) fn square(&self) -> Self { let mut out = blst_fr::default(); - let one = [1u64, 0, 0, 0]; - unsafe { blst_fr_from_uint64(&mut out, one.as_ptr()) }; + unsafe { blst_fr_sqr(&mut out, &self.0) }; Self(out) } -} -impl fmt::Debug for Fr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Fr(..)") + /// Emits the canonical little-endian encoding. + pub(crate) fn to_lendian(self) -> Zeroizing<[u8; 32]> { + let mut scalar = blst_scalar::from(&self); + let bytes = Zeroizing::new(scalar.b); + scalar.b.zeroize(); + bytes } } @@ -154,12 +188,6 @@ impl Sub for Fr { } } -impl Zeroize for Fr { - fn zeroize(&mut self) { - self.0.l.zeroize(); - } -} - type_cvrt!(From for Fr, |s| { let mut out = blst_fr::default(); unsafe { blst_fr_from_scalar(&mut out, s) }; @@ -172,20 +200,6 @@ type_cvrt!(From for blst_scalar, |fr| { out }); -/// An element of the BLS12-381 base field, i.e. an integer reduced -/// modulo the field prime `p`. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] -pub(crate) struct Fp(blst_fp); - -impl Fp { - /// Construct from a `u64`, zero-extended into the low limbs. - pub(crate) fn from_u64(v: u64) -> Self { - let mut bytes = [0u8; 48]; - bytes[40..48].copy_from_slice(&v.to_be_bytes()); - Self::from(&bytes) - } -} - impl Add for Fp { type Output = Self; @@ -238,31 +252,7 @@ type_cvrt!(From<[u8; 48]> for Fp, |bytes| { Self(out) }); -type_cvrt!(From for blst_fp, |fp| fp.0); - -type_cvrt!(From for Fp, |raw| Self(*raw)); - -/// An element of the quadratic extension field `Fp2 = Fp[u]/(u^2 + 1)`, -/// written `c0 + c1*u`. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] -pub(crate) struct Fp2(blst_fp2); - impl Fp2 { - /// The `c0` (real) component. - pub(crate) fn c0(&self) -> Fp { - Fp::from(self.0.fp[0]) - } - - /// The `c1` (coefficient of `u`) component. - pub(crate) fn c1(&self) -> Fp { - Fp::from(self.0.fp[1]) - } - - /// Big-endian byte encoding of the `c1` component. - pub(crate) fn c1_bendian(&self) -> [u8; 48] { - <[u8; 48]>::from(self.c1()) - } - /// Multiplicative inverse; the inverse of zero is left unspecified. pub(crate) fn inverse(&self) -> Self { let mut out = blst_fp2::default(); @@ -270,18 +260,6 @@ impl Fp2 { Self(out) } - /// Whether both components are zero. - pub(crate) fn is_zero(&self) -> bool { - self.0.fp[0].l == [0u64; 6] && self.0.fp[1].l == [0u64; 6] - } - - /// Construct from the two base-field components `c0` and `c1`. - pub(crate) fn new(c0: Fp, c1: Fp) -> Self { - Self(blst_fp2 { - fp: [c0.into(), c1.into()], - }) - } - /// A square root, or `None` when the value is not a quadratic residue. pub(crate) fn sqrt(&self) -> Option { let mut out = blst_fp2::default(); @@ -294,18 +272,6 @@ impl Fp2 { unsafe { blst_fp2_sqr(&mut out, &self.0) }; Self(out) } - - /// Return a copy with the `c0` component replaced. - pub(crate) fn with_c0(mut self, c0: Fp) -> Self { - self.0.fp[0] = c0.into(); - self - } - - /// Return a copy with the `c1` component replaced. - pub(crate) fn with_c1(mut self, c1: Fp) -> Self { - self.0.fp[1] = c1.into(); - self - } } impl Add for Fp2 { @@ -348,36 +314,35 @@ impl Sub for Fp2 { } } -type_cvrt!(From for Fp2, |fp| Self::new(*fp, Fp::default())); - -type_cvrt!(From for blst_fp2, |fp2| fp2.0); - -type_cvrt!(From for Fp2, |raw| Self(*raw)); - -/// A projective group element supporting curve addition and scalar -/// multiplication. -pub(crate) trait Point: Copy + Default + Add { - /// The group identity (point at infinity). - fn identity() -> Self { - Self::default() +impl G1 { + /// Point doubling. + pub(crate) fn double(&self) -> Self { + let mut out = blst_p1::default(); + unsafe { blst_p1_double(&mut out, &self.0) }; + Self(out) } - /// Scalar multiplication by a little-endian scalar of `nbits` bits. - fn mul_scalar(&self, scalar: &[u8], nbits: usize) -> Self; -} - -/// A point of the G1 group (over `Fp`) in projective coordinates, -/// suitable for accumulation before a single conversion to affine. -#[derive(Clone, Copy, Debug, Default, Unencodable)] -pub struct G1(blst_p1); + /// The conventional G1 generator. + pub(crate) fn generator() -> Self { + Self(unsafe { *blst_p1_generator() }) + } -impl G1 { /// Whether the point lies in the prime-order subgroup. - #[cfg(test)] pub(crate) fn in_subgroup(&self) -> bool { unsafe { blst_p1_in_g1(&self.0) } } + /// Whether two points are equal as group elements, projective coordinates + /// notwithstanding. + pub(crate) fn is_equal(&self, other: &Self) -> bool { + unsafe { blst_p1_is_equal(&self.0, &other.0) } + } + + /// Whether the point is at infinity, the group identity. + pub(crate) fn is_inf(&self) -> bool { + unsafe { blst_p1_is_inf(&self.0) } + } + /// Convert to affine coordinates. pub(crate) fn to_affine(self) -> G1Affine { let mut aff = blst_p1_affine::default(); @@ -396,6 +361,16 @@ impl Add for G1 { } } +impl Neg for G1 { + type Output = Self; + + fn neg(self) -> Self::Output { + let mut out = self.0; + unsafe { blst_p1_cneg(&mut out, true) }; + Self(out) + } +} + impl Point for G1 { fn mul_scalar(&self, scalar: &[u8], nbits: usize) -> Self { // Clamp to the bits actually backed by the slice: blst reads @@ -407,15 +382,6 @@ impl Point for G1 { } } -type_cvrt!(From for blst_p1, |g| g.0); - -type_cvrt!(From for G1, |raw| Self(*raw)); - -/// A point of the G1 group in affine coordinates, the canonical form -/// used for serialization and pairing inputs. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] -pub struct G1Affine(blst_p1_affine); - impl G1Affine { /// The conventional G1 generator. pub(crate) fn generator() -> Self { @@ -452,15 +418,6 @@ impl G1Affine { } } -type_cvrt!(From for blst_p1_affine, |a| a.0); - -type_cvrt!(From for G1Affine, |raw| Self(*raw)); - -/// A point of the G2 group (over `Fp2`) in projective coordinates, -/// suitable for accumulation before a single conversion to affine. -#[derive(Clone, Copy, Debug, Default, Unencodable)] -pub struct G2(blst_p2); - impl G2 { /// The conventional G2 generator. pub(crate) fn generator() -> Self { @@ -468,11 +425,21 @@ impl G2 { } /// Whether the point lies in the prime-order subgroup. - #[cfg(test)] pub(crate) fn in_subgroup(&self) -> bool { unsafe { blst_p2_in_g2(&self.0) } } + /// Whether two points are equal as group elements, notwithstanding projective + /// coordinates. + pub(crate) fn is_equal(&self, other: &Self) -> bool { + unsafe { blst_p2_is_equal(&self.0, &other.0) } + } + + /// Whether the point is at infinity, the group identity. + pub(crate) fn is_inf(&self) -> bool { + unsafe { blst_p2_is_inf(&self.0) } + } + /// Point doubling. pub(crate) fn double(&self) -> Self { let mut out = blst_p2::default(); @@ -519,34 +486,7 @@ impl Point for G2 { } } -type_cvrt!(From for G2, |raw| Self(*raw)); - -type_cvrt!(From for blst_p2, |g| g.0); - -/// A point of the G2 group in affine coordinates, the canonical form -/// used for serialization and pairing inputs. -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] -pub struct G2Affine(blst_p2_affine); - impl G2Affine { - /// Construct from affine `x` and `y` coordinates in `Fp2`. - pub(crate) fn from_coords(x: Fp2, y: Fp2) -> Self { - Self(blst_p2_affine { - x: x.into(), - y: y.into(), - }) - } - - /// The affine `x` coordinate. - pub(crate) fn x(&self) -> Fp2 { - Fp2::from(self.0.x) - } - - /// The affine `y` coordinate. - pub(crate) fn y(&self) -> Fp2 { - Fp2::from(self.0.y) - } - /// Convert to projective coordinates. pub(crate) fn to_projective(self) -> G2 { let mut out = blst_p2::default(); @@ -588,7 +528,3 @@ impl G2Affine { Ok(Self(aff)) } } - -type_cvrt!(From for blst_p2_affine, |a| a.0); - -type_cvrt!(From for G2Affine, |raw| Self(*raw)); diff --git a/pkgs/pkc/src/bls/chia_h2c.rs b/pkgs/pkc/src/bls/chia_h2c.rs index 2056c263..1ac81809 100644 --- a/pkgs/pkc/src/bls/chia_h2c.rs +++ b/pkgs/pkc/src/bls/chia_h2c.rs @@ -6,44 +6,14 @@ //! Shallue-van de Woestijne hash-to-G2 for legacy BLS. -use super::blst_ffi::{Fp, Fp2, G2Affine, Point, G2}; +use super::curve_consts::{ + BLS_X_BITS, BLS_X_LE, MONT_R_MOD_P, PSI_COEFF_X_C1, PSI_COEFF_Y_C0, PSI_COEFF_Y_C1, S3, S32, +}; +use super::group::{G2Affine, Point, G2}; +use super::scalar::{Fp, Fp2}; -use hex_conservative::hex; use sha2::{Digest, Sha256}; -// sqrt(-3) mod p (big-endian, left-padded from 40-byte B12_P381_S3). -const S3: [u8; 48] = - hex!("0000000000000000be32ce5fbeed9ca374d38c0ed41eefd5bb675277cdf12d11bc2fb026c41400045c03fffffffdfffd"); - -// (sqrt(-3) - 1) / 2 mod p (big-endian, left-padded from 40-byte B12_P381_S32). -const S32: [u8; 48] = - hex!("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"); - -// BLS12-381 curve parameter |x| in little-endian byte order. -// x = -(2^63 + 2^62 + 2^60 + 2^57 + 2^48 + 2^16) -// |x| = 0xD201000000010000 -const BLS_X_LE: [u8; 8] = hex!("00000100000001d2"); -const BLS_X_BITS: usize = 64; - -// Frobenius endomorphism constants for the BLS12-381 M-type twist. -// psi(x,y) = (conj(x)*PSI_COEFF_X, conj(y)*PSI_COEFF_Y) - -// PSI_COEFF_X.c0 is zero, so only c1 is carried. -const PSI_COEFF_X_C1: [u8; 48] = - hex!("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"); - -// PSI_COEFF_Y.c0 -const PSI_COEFF_Y_C0: [u8; 48] = - hex!("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2"); - -// PSI_COEFF_Y.c1 -const PSI_COEFF_Y_C1: [u8; 48] = - hex!("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09"); - -// 2^384 mod p for BLS12-381 (big-endian). Used in wide reduction. -const R_MOD_P: [u8; 48] = - hex!("15f65ec3fa80e4935c071a97a256ec6d77ce5853705257455f48985753c758baebf4000bc40c0002760900000002fffd"); - // The 'b' coefficient for BLS12-381 twist curve: y^2 = x^3 + 4(1+i). fn curve_b() -> Fp2 { Fp2::new(Fp::from_u64(4), Fp::from_u64(4)) @@ -158,7 +128,7 @@ fn reduce_mod_p(wide: &[u8; 64]) -> Fp { let mut hi_bytes = [0u8; 48]; hi_bytes[32..48].copy_from_slice(&wide[..16]); let hi_fp = Fp::from(&hi_bytes); - let r_fp = Fp::from(&R_MOD_P); + let r_fp = Fp::from(&MONT_R_MOD_P); // result = hi * R + lo hi_fp * r_fp + lo_fp diff --git a/pkgs/pkc/src/bls/curve_consts.rs b/pkgs/pkc/src/bls/curve_consts.rs new file mode 100644 index 00000000..811e9983 --- /dev/null +++ b/pkgs/pkc/src/bls/curve_consts.rs @@ -0,0 +1,292 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! BLS12-381 constants. + +use super::scalar::Fr; + +use hex_conservative::hex; + +/// Two-adicity of `r - 1`, the number of times two divides it. +pub(super) const S: u32 = 32; + +/// The field order, as `ff` asks it to be spelled. +pub(super) const MODULUS: &str = "0x73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"; + +/// `(t - 1) / 2`, where `t` is the odd part of `r - 1`, little-endian. +pub(super) const T_MINUS_1_DIV_2: [u64; 4] = [ + 0x7fff_2dff_7fff_ffff, + 0x04d0_ec02_a9de_d201, + 0x94ce_bea4_199c_ec04, + 0x0000_0000_39f6_d3a9, +]; + +/// Zero. +pub(super) const ZERO: Fr = Fr::from_limbs([0, 0, 0, 0]); + +/// One. +pub(super) const ONE: Fr = Fr::from_limbs([ + 0x0000_0001_ffff_fffe, + 0x5884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff5, + 0x1824_b159_acc5_056f, +]); + +/// The inverse of two. +pub(super) const TWO_INV: Fr = Fr::from_limbs([ + 0x0000_0000_ffff_ffff, + 0xac42_5bfd_0001_a401, + 0xccc6_27f7_f65e_27fa, + 0x0c12_58ac_d662_82b7, +]); + +/// A generator of the multiplicative group, seven for this field. +pub(super) const MULTIPLICATIVE_GENERATOR: Fr = Fr::from_limbs([ + 0x0000_000e_ffff_fff1, + 0x17e3_63d3_0018_9c0f, + 0xff9c_5787_6f84_57b0, + 0x3513_3220_8fc5_a8c4, +]); + +/// A primitive root of unity of order `2^S`, the generator raised to `t`. +pub(super) const ROOT_OF_UNITY: Fr = Fr::from_limbs([ + 0xb9b5_8d8c_5f0e_466a, + 0x5b1b_4c80_1819_d7ec, + 0x0af5_3ae3_52a3_1e64, + 0x5bf3_adda_19e9_b27b, +]); + +/// The inverse of [`ROOT_OF_UNITY`]. +pub(super) const ROOT_OF_UNITY_INV: Fr = Fr::from_limbs([ + 0x4256_481a_dcf3_219a, + 0x45f3_7b7f_96b6_cad3, + 0xf9c3_f1d7_5f7a_3b27, + 0x2d2f_c049_658a_fd43, +]); + +/// The generator raised to `2^S`, a non-square by construction. +pub(super) const DELTA: Fr = Fr::from_limbs([ + 0x70e3_10d3_d146_f96a, + 0x4b64_c089_19e2_99e6, + 0x51e1_1418_6a8b_970d, + 0x6185_d066_27c0_67cb, +]); + +/// BLS12-381 curve parameter `|x|`, little-endian. +/// +/// `x = -(2^63 + 2^62 + 2^60 + 2^57 + 2^48 + 2^16)`, so callers multiply by +/// the magnitude and negate. +pub(super) const BLS_X_LE: [u8; 8] = hex!("00000100000001d2"); + +/// Bit width of [`BLS_X_LE`]. +pub(super) const BLS_X_BITS: usize = 64; + +/// `sqrt(-3) mod p`, for the Shallue-van de Woestijne map. +pub(super) const S3: [u8; 48] = + hex!("0000000000000000be32ce5fbeed9ca374d38c0ed41eefd5bb675277cdf12d11bc2fb026c41400045c03fffffffdfffd"); + +/// `(sqrt(-3) - 1) / 2 mod p`, for the same map. +pub(super) const S32: [u8; 48] = + hex!("00000000000000005f19672fdf76ce51ba69c6076a0f77eaddb3a93be6f89688de17d813620a00022e01fffffffefffe"); + +/// The Montgomery radix `2^384 mod p`, for folding a wide value into `Fp`. +/// +/// Named for the radix rather than the group order: this is `R`, not `r`. +pub(super) const MONT_R_MOD_P: [u8; 48] = + hex!("15f65ec3fa80e4935c071a97a256ec6d77ce5853705257455f48985753c758baebf4000bc40c0002760900000002fffd"); + +/// `(p - 1) / 2`, the midpoint the legacy sign convention compares against. +pub(super) const HALF_P: [u8; 48] = + hex!("0d0088f51cbff34d258dd3db21a5d66bb23ba5c279c2895fb39869507b587b120f55ffff58a9ffffdcff7fffffffd555"); + +/// `c1` of the Frobenius `psi` x-coefficient on the M-type twist; `c0` is +/// zero, so only this half is carried. +pub(super) const PSI_COEFF_X_C1: [u8; 48] = + hex!("1a0111ea397fe699ec02408663d4de85aa0d857d89759ad4897d29650fb85f9b409427eb4f49fffd8bfd00000000aaad"); + +/// `c0` of the Frobenius `psi` y-coefficient. +pub(super) const PSI_COEFF_Y_C0: [u8; 48] = + hex!("135203e60180a68ee2e9c448d77a2cd91c3dedd930b1cf60ef396489f61eb45e304466cf3e67fa0af1ee7b04121bdea2"); + +/// `c1` of the Frobenius `psi` y-coefficient. +pub(super) const PSI_COEFF_Y_C1: [u8; 48] = + hex!("06af0e0437ff400b6831e36d6bd17ffe48395dabc2d3435e77f76e17009241c5ee67992f72ec05f4c81084fbede3cc09"); + +#[cfg(test)] +mod tests { + use super::*; + use crate::bls::scalar::{Fp, Fp2}; + use crate::prelude::*; + + use ff::{Field, PrimeField}; + use hex_conservative::DisplayHex; + use rstest::rstest; + + /// The curve parameter is read little-endian, so the bytes as written are the + /// magnitude the cofactor routine multiplies by. + #[rstest] + fn bls_x_is_the_curve_parameter() { + assert_eq!(u64::from_le_bytes(BLS_X_LE), 0xd201_0000_0001_0000); + assert_eq!(BLS_X_BITS, u64::BITS as usize); + } + + /// Stated as `2 * HALF_P + 1 == p` by way of `-1`, the largest element, since + /// `p` itself is not a representable field value. + #[rstest] + fn half_p_is_the_field_midpoint() { + let half = Fp::from(&HALF_P); + assert_eq!(half + half, -Fp::from_u64(1)); + } + + #[rstest] + fn s3_squares_to_minus_three() { + let s3 = Fp::from(&S3); + assert_eq!(s3 * s3, -Fp::from_u64(3)); + } + + /// Stated as `2 * S32 + 1 == S3` so the check needs no division. + #[rstest] + fn s32_is_s3_less_one_halved() { + let s32 = Fp::from(&S32); + assert_eq!(s32 + s32 + Fp::from_u64(1), Fp::from(&S3)); + } + + #[rstest] + fn mont_r_is_two_to_the_384() { + let mut acc = Fp::from_u64(1); + for _ in 0..384 { + acc = acc + acc; + } + assert_eq!(acc, Fp::from(&MONT_R_MOD_P)); + } + + /// `psi` untwists, so its coefficients are the reciprocals + /// `1/(1+u)^((p-1)/3)` and `1/(1+u)^((p-1)/2)`. + /// + /// Cubing and squaring them clears the fractional exponent, and Frobenius + /// gives `(1+u)^p = 1-u`, so each reduces to `(1+u)/(1-u)` and the check + /// needs no 381-bit exponent of its own. + #[rstest] + fn psi_coefficients_untwist_the_frobenius() { + let one = Fp::from_u64(1); + let plus = Fp2::new(one, one); + let minus = Fp2::new(one, -one); + + let psi_x = Fp2::new(Fp::default(), Fp::from(&PSI_COEFF_X_C1)); + let psi_y = Fp2::new(Fp::from(&PSI_COEFF_Y_C0), Fp::from(&PSI_COEFF_Y_C1)); + + assert_eq!(psi_x.c0(), Fp::default(), "the x coefficient is purely imaginary"); + assert_eq!(psi_x * psi_x * psi_x * minus, plus); + assert_eq!(psi_y * psi_y * minus, plus); + } + /// `t`, the odd part of `r - 1`, little-endian. + /// + /// Doubling [`T_MINUS_1_DIV_2`] and restoring the low bit, in plain integer + /// arithmetic so the field plays no part in the exponent it is tested with. + fn odd_part() -> [u64; 4] { + let mut t = [0u64; 4]; + let mut carry = 1u64; + for (out, limb) in t.iter_mut().zip(T_MINUS_1_DIV_2) { + *out = (limb << 1) | carry; + carry = limb >> 63; + } + t + } + + /// Exponentiation by squaring over a little-endian limb exponent. + fn pow(base: Fr, exp: &[u64; 4]) -> Fr { + let mut acc = ONE; + for limb in exp.iter().rev() { + for bit in (0..64).rev() { + acc = acc.square(); + if (limb >> bit) & 1 == 1 { + acc *= base; + } + } + } + acc + } + + /// The two constants blst can hand back directly. + #[rstest] + #[case::zero(ZERO, 0)] + #[case::one(ONE, 1)] + #[case::generator(MULTIPLICATIVE_GENERATOR, 7)] + fn small_constants_match_blst(#[case] literal: Fr, #[case] value: u64) { + assert_eq!(literal, Fr::from(value)); + } + + /// The inverses come from blst's own inversion rather than from a product + /// with the value they invert, so a matched pair of wrong literals cannot + /// satisfy the check between them. + #[rstest] + #[case::two_inv(TWO_INV, Fr::from(2))] + #[case::root_of_unity_inv(ROOT_OF_UNITY_INV, ROOT_OF_UNITY)] + fn inverses_match_blst(#[case] literal: Fr, #[case] of: Fr) { + assert_eq!(literal, of.invert().unwrap()); + } + + /// `ff` specifies the root as the generator raised to the odd part of + /// `r - 1`, which is one value; having order `2^S` admits every primitive + /// root and so would not pin it. + #[rstest] + fn root_of_unity_is_the_generator_raised_to_the_odd_part() { + assert_eq!(ROOT_OF_UNITY, pow(MULTIPLICATIVE_GENERATOR, &odd_part())); + } + + /// Squaring the root `S` times reaches one and `S - 1` times does not, so + /// its order is exactly `2^S` and `S` is the two-adicity it claims. + #[rstest] + fn root_of_unity_has_the_stated_order() { + let mut acc = ROOT_OF_UNITY; + for _ in 0..S - 1 { + acc = acc.square(); + } + assert_ne!(acc, ONE); + assert_eq!(acc.square(), ONE); + } + + #[rstest] + fn delta_is_the_generator_raised_to_two_to_the_s() { + let mut delta = MULTIPLICATIVE_GENERATOR; + for _ in 0..S { + delta = delta.square(); + } + assert_eq!(DELTA, delta); + } + + /// The odd part is odd and restores `r - 1` once the power of two is put + /// back, which is the whole of what makes `S` and `t` a valid split. + #[rstest] + fn odd_part_rebuilds_the_order() { + let t = odd_part(); + assert_eq!(t[0] & 1, 1, "the odd part must be odd"); + + let mut rebuilt = pow(MULTIPLICATIVE_GENERATOR, &t); + for _ in 0..S { + rebuilt = rebuilt.square(); + } + assert_eq!(rebuilt, ONE, "g^(t * 2^S) is g^(r - 1), which is one"); + } + + /// The string is the field order, one past the largest element. + #[rstest] + fn modulus_string_matches_the_order() { + let mut order = (-ONE).to_repr(); + order.reverse(); + let last = order.len() - 1; + order[last] += 1; + + assert_eq!(format!("0x{}", order.as_hex()), MODULUS); + } + + #[rstest] + fn montgomery_form_is_what_the_literals_assume() { + let mut expected = [0u8; 32]; + expected[0] = 1; + assert_eq!(ONE.to_repr(), expected); + } +} diff --git a/pkgs/pkc/src/bls/group.rs b/pkgs/pkc/src/bls/group.rs new file mode 100644 index 00000000..069dbf32 --- /dev/null +++ b/pkgs/pkc/src/bls/group.rs @@ -0,0 +1,196 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! The BLS12-381 groups G1 and G2. + +use super::macros::impl_group; +use super::scalar::{Fp2, Fr, FR_BITS}; + +use blst::{blst_p1, blst_p1_affine, blst_p2, blst_p2_affine}; +use dash_types::type_cvrt; +use dash_types::type_id::Unencodable; +use ff::{Field, PrimeField}; +use group::{Group, GroupEncoding}; +use hex_conservative::DisplayHex; +use rand_core::TryRng; +use subtle::{Choice, CtOption}; + +use core::fmt::{self, Debug, Formatter}; +use core::iter::Sum; +use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; + +/// A projective group element supporting curve addition and scalar multiply. +pub(crate) trait Point: Copy + Default + Add { + /// The group identity (point at infinity). + fn identity() -> Self { + Self::default() + } + + /// Scalar multiplication by a little-endian scalar of `nbits` bits. + fn mul_scalar(&self, scalar: &[u8], nbits: usize) -> Self; +} + +/// The compressed encoding of a group element, `N` bytes wide. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Unencodable)] +pub struct BlsPointRepr([u8; N]); + +impl BlsPointRepr { + /// Borrows the encoding as a fixed-width array. + pub(crate) fn as_bytes(&self) -> &[u8; N] { + &self.0 + } +} + +impl AsMut<[u8]> for BlsPointRepr { + fn as_mut(&mut self) -> &mut [u8] { + &mut self.0 + } +} + +impl AsRef<[u8]> for BlsPointRepr { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Default for BlsPointRepr { + fn default() -> Self { + Self([0u8; N]) + } +} + +impl From<[u8; N]> for BlsPointRepr { + fn from(bytes: [u8; N]) -> Self { + Self(bytes) + } +} + +/// A point of the G1 group (over `Fp`) in projective coordinates. +#[derive(Clone, Copy, Default, Unencodable)] +pub struct G1(pub(super) blst_p1); + +type_cvrt!(From for blst_p1, |g| g.0); + +type_cvrt!(From for G1, |raw| Self(*raw)); + +/// A point of the G1 group in affine coordinates. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +pub struct G1Affine(pub(super) blst_p1_affine); + +impl_group!(G1, G1Affine, 48); + +type_cvrt!(From for blst_p1_affine, |a| a.0); + +type_cvrt!(From for G1Affine, |raw| Self(*raw)); + +/// A point of the G2 group (over `Fp2`) in projective coordinates. +#[derive(Clone, Copy, Default, Unencodable)] +pub struct G2(pub(super) blst_p2); + +type_cvrt!(From for G2, |raw| Self(*raw)); + +type_cvrt!(From for blst_p2, |g| g.0); + +/// A point of the G2 group in affine coordinates. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +pub struct G2Affine(pub(super) blst_p2_affine); + +impl G2Affine { + /// Construct from affine `x` and `y` coordinates in `Fp2`. + pub(crate) fn from_coords(x: Fp2, y: Fp2) -> Self { + Self(blst_p2_affine { + x: x.into(), + y: y.into(), + }) + } + + /// The affine `x` coordinate. + pub(crate) fn x(&self) -> Fp2 { + Fp2::from(self.0.x) + } + + /// The affine `y` coordinate. + pub(crate) fn y(&self) -> Fp2 { + Fp2::from(self.0.y) + } +} + +type_cvrt!(From for blst_p2_affine, |a| a.0); + +type_cvrt!(From for G2Affine, |raw| Self(*raw)); + +impl_group!(G2, G2Affine, 96); + +#[cfg(test)] +mod tests { + use super::*; + use crate::bls::tests::{G1_OFF_SUBGROUP_IETF, G2_OFF_SUBGROUP_IETF}; + + use getrandom::SysRng; + use rand_core::UnwrapErr; + use rstest::rstest; + + fn assert_group_axioms>() { + let mut rng = UnwrapErr(SysRng); + let a = G::random(&mut rng); + let b = G::random(&mut rng); + + assert_eq!(a + b, b + a); + assert_eq!(a + b - b, a); + assert_eq!(a + a, a.double()); + assert_eq!(-a + a, G::identity()); + assert!(bool::from(G::identity().is_identity())); + assert!(!bool::from(a.is_identity())); + } + + #[rstest] + #[case::g1(assert_group_axioms::)] + #[case::g2(assert_group_axioms::)] + fn group_axioms(#[case] assertion: fn()) { + assertion(); + } + + /// Scalar multiplication has to agree with repeated addition, which is the + /// one place a wrong bit width or byte order would show up. + fn assert_scalar_mul_matches_addition>() { + let g = G::generator(); + let three = g + g + g; + + assert_eq!(g * Fr::from(3), three); + assert_eq!(G::mul_by_generator(&Fr::from(3)), three); + assert_eq!(g * Fr::ZERO, G::identity()); + assert_eq!(g * -Fr::ONE, -g); + } + + #[rstest] + #[case::g1(assert_scalar_mul_matches_addition::)] + #[case::g2(assert_scalar_mul_matches_addition::)] + fn scalar_mul_matches_addition(#[case] assertion: fn()) { + assertion(); + } + + /// The unchecked decoder is the one that admits a composite-order point, + /// which is the whole of what separates the two. + #[rstest] + #[case::g1( + G1::from_bytes(&G1_OFF_SUBGROUP_IETF.into()).is_none(), + G1::from_bytes_unchecked(&G1_OFF_SUBGROUP_IETF.into()).is_some() + )] + #[case::g2( + G2::from_bytes(&G2_OFF_SUBGROUP_IETF.into()).is_none(), + G2::from_bytes_unchecked(&G2_OFF_SUBGROUP_IETF.into()).is_some() + )] + fn checked_decoding_refuses_off_subgroup_points(#[case] refused: Choice, #[case] admitted: Choice) { + assert!(bool::from(refused)); + assert!(bool::from(admitted)); + } + + #[rstest] + fn decoding_refuses_malformed_encodings() { + assert!(bool::from(G1::from_bytes(&[0xff; 48].into()).is_none())); + assert!(bool::from(G2::from_bytes(&[0xff; 96].into()).is_none())); + } +} diff --git a/pkgs/pkc/src/bls/macros.rs b/pkgs/pkc/src/bls/macros.rs new file mode 100644 index 00000000..941b1aa0 --- /dev/null +++ b/pkgs/pkc/src/bls/macros.rs @@ -0,0 +1,169 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Shared macro definitions. + +/// Implements the `group` traits over one curve group. +macro_rules! impl_group { + ($name:ident, $affine:ty, $len:expr) => { + impl<'a> Add<&'a Self> for $name { + type Output = Self; + + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } + } + + impl AddAssign for $name { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } + } + + impl<'a> AddAssign<&'a Self> for $name { + fn add_assign(&mut self, rhs: &'a Self) { + *self = *self + *rhs; + } + } + + impl Debug for $name { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, concat!(stringify!($name), "({})"), self.to_bytes().as_ref().as_hex()) + } + } + + impl Eq for $name {} + + impl Group for $name { + type Scalar = Fr; + + fn identity() -> Self { + Self::default() + } + + fn generator() -> Self { + Self::generator() + } + + fn is_identity(&self) -> Choice { + Choice::from(u8::from(self.is_inf())) + } + + fn double(&self) -> Self { + Self::double(self) + } + + fn try_random(rng: &mut R) -> Result { + Ok(::generator() * Fr::try_random(rng)?) + } + } + + impl GroupEncoding for $name { + type Repr = BlsPointRepr<$len>; + + fn from_bytes(bytes: &Self::Repr) -> CtOption { + // The subgroup check is the difference from `from_bytes_unchecked`: + // the encoding only pins a curve point, and a point off the prime-order + // subgroup breaks the arithmetic every caller assumes. + match <$affine>::uncompress(bytes.as_bytes()) { + Ok(affine) => { + let point = affine.to_projective(); + let ok = point.in_subgroup(); + CtOption::new(point, Choice::from(u8::from(ok))) + } + Err(_) => CtOption::new(::identity(), Choice::from(0)), + } + } + + fn from_bytes_unchecked(bytes: &Self::Repr) -> CtOption { + match <$affine>::uncompress(bytes.as_bytes()) { + Ok(affine) => CtOption::new(affine.to_projective(), Choice::from(1)), + Err(_) => CtOption::new(::identity(), Choice::from(0)), + } + } + + fn to_bytes(&self) -> Self::Repr { + BlsPointRepr(self.to_affine().compress()) + } + } + + impl Mul for $name { + type Output = Self; + + fn mul(self, rhs: Fr) -> Self::Output { + self.mul_scalar(&rhs.to_repr(), FR_BITS) + } + } + + impl<'a> Mul<&'a Fr> for $name { + type Output = Self; + + fn mul(self, rhs: &'a Fr) -> Self::Output { + self * *rhs + } + } + + impl MulAssign for $name { + fn mul_assign(&mut self, rhs: Fr) { + *self = *self * rhs; + } + } + + impl<'a> MulAssign<&'a Fr> for $name { + fn mul_assign(&mut self, rhs: &'a Fr) { + *self = *self * *rhs; + } + } + + impl PartialEq for $name { + fn eq(&self, other: &Self) -> bool { + self.is_equal(other) + } + } + + impl Sub for $name { + type Output = Self; + + fn sub(self, rhs: Self) -> Self::Output { + self + -rhs + } + } + + impl<'a> Sub<&'a Self> for $name { + type Output = Self; + + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } + } + + impl SubAssign for $name { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } + } + + impl<'a> SubAssign<&'a Self> for $name { + fn sub_assign(&mut self, rhs: &'a Self) { + *self = *self - *rhs; + } + } + + impl<'a> Sum<&'a Self> for $name { + fn sum>(iter: I) -> Self { + iter.fold(::identity(), |acc, x| acc + x) + } + } + + impl Sum for $name { + fn sum>(iter: I) -> Self { + iter.fold(::identity(), |acc, x| acc + x) + } + } + }; +} + +pub(super) use impl_group; diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index 3387d640..b5ec6fd1 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -11,6 +11,7 @@ mod error; mod public_bytes; mod schemes; mod secret_bytes; +mod share_id; mod sig_bytes; mod sig_id; @@ -19,11 +20,14 @@ pub use error::BlsError; pub use public_bytes::{BlsPkBytes, BLS_PK_LEN}; pub use schemes::{BlsScChia, BlsScIetf, BlsSchemeId}; pub use secret_bytes::{BlsSkBytes, BLS_SK_LEN}; +pub use share_id::{BlsShareId, BLS_ID_LEN}; pub use sig_bytes::{BlsSigBytes, BLS_SIG_LEN}; pub use sig_id::BlsSigId; cfg_if::cfg_if! { if #[cfg(feature = "bls")] { + mod curve_consts; + mod macros; mod public_ops; mod scheme_chia; mod scheme_ietf; @@ -36,6 +40,8 @@ cfg_if::cfg_if! { #[expect(unsafe_code, reason = "blst C FFI")] pub(crate) mod blst_ffi; pub(crate) mod chia_h2c; + pub(crate) mod group; + pub(crate) mod scalar; pub(crate) mod scheme_ops; #[cfg(any(test, feature = "tests"))] @@ -44,6 +50,8 @@ cfg_if::cfg_if! { pub mod tests; pub use public_ops::BlsPublicKey; + pub use group::{BlsPointRepr, G1Affine, G2Affine, G1, G2}; + pub use scalar::Fr; pub use scheme_ops::BlsScheme; pub use secret_ops::BlsSecretKey; pub use share_ops::{BlsSigShare, BlsSkShare}; diff --git a/pkgs/pkc/src/bls/public_ops.rs b/pkgs/pkc/src/bls/public_ops.rs index 81ee6158..7da12595 100644 --- a/pkgs/pkc/src/bls/public_ops.rs +++ b/pkgs/pkc/src/bls/public_ops.rs @@ -7,6 +7,7 @@ //! Scheme-generic BLS public key. use super::error::BlsError; +use super::group::G1; use super::scheme_ops::BlsScheme; use super::{BlsPkBytes, BLS_PK_LEN}; use crate::prelude::*; @@ -67,6 +68,22 @@ impl BlsPublicKey { S::aggregate_pk(&inner_refs).map(Self::from_inner) } + /// Aggregate public keys under secure-verification weighting. + /// + /// The weights are those applied by + /// [`secure_verify_aggregates`](super::BlsSignature::secure_verify_aggregates) + /// to keys sorted by encoding, so the result is order-independent and a + /// signature passing that check verifies plainly against this key. + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no keys are given, or `InvalidPublicKey` + /// when a key or the weighted sum fails to decode. + pub fn secure_aggregate(keys: &[&Self]) -> Result { + let inner_refs: Vec<&S::InnerPk> = keys.iter().map(|k| &k.0).collect(); + S::secure_aggregate_pk(&inner_refs).map(Self::from_inner) + } + pub(crate) fn from_inner(inner: S::InnerPk) -> Self { Self(inner) } @@ -107,15 +124,23 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsPublicKey, BlsErro Self::from_bytes(bytes.as_bytes()) }); +type_cvrt!(for[S: BlsScheme] TryFrom> for G1, BlsError, |pk| { + S::pk_to_g1(&pk.0) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom for BlsPublicKey, BlsError, |point| { + S::g1_to_pk(*point).map(Self::from_inner) +}); + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; use crate::bls::tests::{ ietf_g1_encoding, ser_pairs, SerType, G1_OFF_SUBGROUP_CHIA, G1_OFF_SUBGROUP_IETF, G1_X_EQ_PRIME_CHIA, - G1_X_GE_PRIME_CHIA, G1_X_MAX_CHIA, RSEED, + G1_X_GE_PRIME_CHIA, G1_X_MAX_CHIA, MSG_8BADFOOD, RSEED, }; - use crate::bls::{BlsScChia, BlsScIetf, BlsSecretKey}; + use crate::bls::{BlsScChia, BlsScIetf, BlsSecretKey, BlsSignature}; use cfg_if::cfg_if; use dash_dev::{arr_from_hex, Corpus}; @@ -136,6 +161,13 @@ mod tests { shared: String, } + #[derive(Deserialize)] + struct SecureVec { + msg: String, + pks: Vec, + agg_sig_secure: String, + } + fn assert_dh_matches_vectors(scheme: &str) { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_dh").scope(scheme); let vecs: Vec = corpus.vectors("dh"); @@ -404,6 +436,86 @@ mod tests { assertion(scheme); } + fn assert_secure_aggregate_is_what_verify_checks(scheme: &str) { + let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_secure_aggregate").scope(scheme); + let vecs: Vec = corpus.vectors("verify"); + assert!(!vecs.is_empty(), "corpus section is empty"); + + for v in &vecs { + let pks: Vec> = v + .pks + .iter() + .map(|pk| BlsPublicKey::::from_bytes(&arr_from_hex(pk)).unwrap()) + .collect(); + let refs: Vec<&BlsPublicKey> = pks.iter().collect(); + let agg_pk = BlsPublicKey::::secure_aggregate(&refs).unwrap(); + + let sig = BlsSignature::::from_bytes(&arr_from_hex(&v.agg_sig_secure)).unwrap(); + let msg: [u8; 32] = arr_from_hex(&v.msg); + assert!(sig.verify(S::msg_ref(&msg), &agg_pk).is_ok()); + assert!(sig.secure_verify_aggregates(S::msg_ref(&msg), &refs).is_ok()); + } + } + + #[rstest] + #[case::chia(assert_secure_aggregate_is_what_verify_checks::, "chia")] + #[case::ietf(assert_secure_aggregate_is_what_verify_checks::, "ietf")] + fn secure_aggregate_is_what_verify_checks(#[case] assertion: fn(&str), #[case] scheme: &str) { + assertion(scheme); + } + + /// The weights go by the sorted keys, so the set decides the aggregate and + /// the order it arrives in does not. A plain sum would ignore the weights + /// and land somewhere else entirely. + fn assert_secure_aggregate_follows_the_set() { + let pks: Vec> = [&RSEED[0], &RSEED[1], &RSEED[2]] + .iter() + .map(|ikm| BlsSecretKey::::generate(*ikm).unwrap().public_key()) + .collect(); + + let straight = BlsPublicKey::::secure_aggregate(&[&pks[0], &pks[1], &pks[2]]).unwrap(); + let rotated = BlsPublicKey::::secure_aggregate(&[&pks[2], &pks[0], &pks[1]]).unwrap(); + assert_eq!(straight, rotated); + + let plain = BlsPublicKey::::aggregate(&[&pks[0], &pks[1], &pks[2]]).unwrap(); + assert_ne!(straight, plain, "the weights left no mark on the sum"); + } + + #[rstest] + #[case::chia(assert_secure_aggregate_follows_the_set::)] + #[case::ietf(assert_secure_aggregate_follows_the_set::)] + fn secure_aggregate_follows_the_set(#[case] assertion: fn()) { + assertion(); + } + + /// A lone signer is still weighted, so the aggregate is not that signer's + /// key; nothing is left to weight for an empty set, which is refused as it + /// is elsewhere. + fn assert_secure_aggregate_edges() { + let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let pk = sk.public_key(); + + let alone = BlsPublicKey::::secure_aggregate(&[&pk]).unwrap(); + assert_ne!(alone, pk, "a single key went through unweighted"); + + let sig = sk.sign(S::msg_ref(&MSG_8BADFOOD)); + let weighted = BlsSignature::::secure_aggregate(&[&sig], &[&pk]).unwrap(); + assert!(weighted.verify(S::msg_ref(&MSG_8BADFOOD), &alone).is_ok()); + + let none: [&BlsPublicKey; 0] = []; + assert_eq!( + BlsPublicKey::::secure_aggregate(&none), + Err(BlsError::EmptyAggregation) + ); + } + + #[rstest] + #[case::chia(assert_secure_aggregate_edges::)] + #[case::ietf(assert_secure_aggregate_edges::)] + fn secure_aggregate_edges(#[case] assertion: fn()) { + assertion(); + } + cfg_if! { if #[cfg(feature = "serde")] { use dash_dev::{assert_json_rt, to_json}; diff --git a/pkgs/pkc/src/bls/scalar.rs b/pkgs/pkc/src/bls/scalar.rs new file mode 100644 index 00000000..9dc13f95 --- /dev/null +++ b/pkgs/pkc/src/bls/scalar.rs @@ -0,0 +1,491 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! The BLS12-381 scalar and base fields. + +use super::curve_consts; +use super::error::BlsError; +use super::share_id::BlsShareId; + +use blst::{blst_fp, blst_fp2, blst_fr}; +use dash_types::type_cvrt; +use dash_types::type_id::Unencodable; +use ff::helpers::{sqrt_ratio_generic, sqrt_tonelli_shanks}; +use ff::{Field, PrimeField}; +use rand_core::TryRng; +use subtle::{Choice, ConditionallySelectable, ConstantTimeEq, CtOption}; +use zeroize::{Zeroize, Zeroizing}; + +use core::fmt; +use core::iter::{Product, Sum}; +use core::ops::{Add, AddAssign, Mul, MulAssign, Sub, SubAssign}; + +/// Bit-length for scalars known to be reduced mod q (< 2^255). +pub(crate) const FR_BITS: usize = 255; + +/// Width of the canonical scalar encoding. +const REPR_LEN: usize = 32; + +/// Bytes drawn per uniform sample, wide enough that reduction is unbiased. +const WIDE_LEN: usize = 64; + +/// A scalar of the BLS12-381 scalar field, i.e. an integer reduced modulo the +/// group order `r`. +#[derive(Clone, Copy, Default)] +pub struct Fr(pub(super) blst_fr); + +impl Fr { + /// Reduces a threshold participant id into the field (big-integer). + /// + /// [`PrimeField::from_repr`] takes a canonical little-endian encoding and + /// rejects anything above `r` while this function will reduce. + /// + /// # Errors + /// + /// Returns `InvalidShareId` when the id reduces to zero; the polynomial + /// evaluated there yields its constant term, the master secret itself. + pub fn from_share_id(id: &BlsShareId) -> Result { + let reduced = Self::from_bendian_reduce(id.as_bytes()); + if bool::from(reduced.is_zero()) { + return Err(BlsError::InvalidShareId); + } + Ok(reduced) + } + + /// Reduces a big-endian integer into the field. + pub(crate) fn from_bendian_reduce(bytes: &[u8; 32]) -> Self { + let mut scalar = super::blst_ffi::scalar_from_bendian(bytes); + let reduced = Self::from(&scalar); + scalar.b.zeroize(); + reduced + } + + /// Emits the canonical big-endian encoding. + pub(crate) fn to_bendian(self) -> Zeroizing<[u8; 32]> { + let mut bytes = self.to_lendian(); + bytes.reverse(); + bytes + } + + /// Wraps Montgomery-form limbs. + pub(crate) const fn from_limbs(limbs: [u64; 4]) -> Self { + Self(blst_fr { l: limbs }) + } + + /// Borrows the Montgomery-form limbs. + pub(crate) const fn limbs(&self) -> &[u64; 4] { + &self.0.l + } +} + +impl<'a> Add<&'a Self> for Fr { + type Output = Self; + + fn add(self, rhs: &'a Self) -> Self::Output { + self + *rhs + } +} + +impl AddAssign for Fr { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl<'a> AddAssign<&'a Self> for Fr { + fn add_assign(&mut self, rhs: &'a Self) { + *self = *self + *rhs; + } +} + +impl ConditionallySelectable for Fr { + fn conditional_select(a: &Self, b: &Self, choice: Choice) -> Self { + let mut out = [0u64; 4]; + for (slot, (x, y)) in out.iter_mut().zip(a.limbs().iter().zip(b.limbs())) { + *slot = u64::conditional_select(x, y, choice); + } + Self::from_limbs(out) + } +} + +impl ConstantTimeEq for Fr { + fn ct_eq(&self, other: &Self) -> Choice { + self.limbs().ct_eq(other.limbs()) + } +} + +impl fmt::Debug for Fr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Fr(..)") + } +} + +impl Eq for Fr {} + +impl Field for Fr { + const ONE: Self = curve_consts::ONE; + const ZERO: Self = curve_consts::ZERO; + + fn double(&self) -> Self { + Fr::double(self) + } + + fn invert(&self) -> CtOption { + CtOption::new(self.inverse(), !self.is_zero()) + } + + /// Overriding this is mandatory, not stylistic: `ff` implements `sqrt` in + /// terms of `sqrt_ratio`, so leaving the default beside a `sqrt_ratio` built + /// from `sqrt_ratio_generic` puts the two in a recursive cycle. + fn sqrt(&self) -> CtOption { + sqrt_tonelli_shanks(self, curve_consts::T_MINUS_1_DIV_2) + } + + fn sqrt_ratio(num: &Self, div: &Self) -> (Choice, Self) { + sqrt_ratio_generic(num, div) + } + + fn square(&self) -> Self { + Fr::square(self) + } + + fn try_random(rng: &mut R) -> Result { + let mut wide = [0u8; WIDE_LEN]; + rng.try_fill_bytes(&mut wide)?; + let sampled = Self::from_lendian_reduce(&wide); + wide.zeroize(); + Ok(sampled) + } +} + +impl From for Fr { + fn from(value: u64) -> Self { + Self::from_u64(value) + } +} + +impl<'a> Mul<&'a Self> for Fr { + type Output = Self; + + fn mul(self, rhs: &'a Self) -> Self::Output { + self * *rhs + } +} + +impl MulAssign for Fr { + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl<'a> MulAssign<&'a Self> for Fr { + fn mul_assign(&mut self, rhs: &'a Self) { + *self = *self * *rhs; + } +} + +impl PartialEq for Fr { + fn eq(&self, other: &Self) -> bool { + self.ct_eq(other).into() + } +} + +impl PrimeField for Fr { + type Repr = [u8; REPR_LEN]; + + const CAPACITY: u32 = Self::NUM_BITS - 1; + const DELTA: Self = curve_consts::DELTA; + const MODULUS: &'static str = curve_consts::MODULUS; + const MULTIPLICATIVE_GENERATOR: Self = curve_consts::MULTIPLICATIVE_GENERATOR; + const NUM_BITS: u32 = FR_BITS as u32; + const ROOT_OF_UNITY: Self = curve_consts::ROOT_OF_UNITY; + const ROOT_OF_UNITY_INV: Self = curve_consts::ROOT_OF_UNITY_INV; + const S: u32 = curve_consts::S; + const TWO_INV: Self = curve_consts::TWO_INV; + + fn from_repr(repr: Self::Repr) -> CtOption { + match Self::from_lendian(&repr) { + Some(fr) => CtOption::new(fr, Choice::from(1)), + None => CtOption::new(Self::ZERO, Choice::from(0)), + } + } + + fn is_odd(&self) -> Choice { + Choice::from(self.to_repr()[0] & 1) + } + + fn to_repr(&self) -> Self::Repr { + *self.to_lendian() + } +} + +impl<'a> Product<&'a Self> for Fr { + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +impl Product for Fr { + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +impl<'a> Sub<&'a Self> for Fr { + type Output = Self; + + fn sub(self, rhs: &'a Self) -> Self::Output { + self - *rhs + } +} + +impl SubAssign for Fr { + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl<'a> SubAssign<&'a Self> for Fr { + fn sub_assign(&mut self, rhs: &'a Self) { + *self = *self - *rhs; + } +} + +impl<'a> Sum<&'a Self> for Fr { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl Sum for Fr { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl Zeroize for Fr { + fn zeroize(&mut self) { + self.0.l.zeroize(); + } +} + +/// An element of the BLS12-381 base field, i.e. an integer reduced modulo the +/// field prime `p`. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +pub(crate) struct Fp(pub(super) blst_fp); + +impl Fp { + /// Construct from a `u64`, zero-extended into the low limbs. + pub(crate) fn from_u64(v: u64) -> Self { + let mut bytes = [0u8; 48]; + bytes[40..48].copy_from_slice(&v.to_be_bytes()); + Self::from(&bytes) + } +} + +type_cvrt!(From for blst_fp, |fp| fp.0); + +type_cvrt!(From for Fp, |raw| Self(*raw)); + +/// An element of the quadratic extension field `Fp2 = Fp[u]/(u^2 + 1)`, written +/// `c0 + c1*u`. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Unencodable)] +pub(crate) struct Fp2(pub(super) blst_fp2); + +impl Fp2 { + /// The `c0` (real) component. + pub(crate) fn c0(&self) -> Fp { + Fp::from(self.0.fp[0]) + } + + /// The `c1` (coefficient of `u`) component. + pub(crate) fn c1(&self) -> Fp { + Fp::from(self.0.fp[1]) + } + + /// Big-endian byte encoding of the `c1` component. + pub(crate) fn c1_bendian(&self) -> [u8; 48] { + <[u8; 48]>::from(self.c1()) + } + + /// Whether both components are zero. + pub(crate) fn is_zero(&self) -> bool { + self.0.fp[0].l == [0u64; 6] && self.0.fp[1].l == [0u64; 6] + } + + /// Construct from the two base-field components `c0` and `c1`. + pub(crate) fn new(c0: Fp, c1: Fp) -> Self { + Self(blst_fp2 { + fp: [c0.into(), c1.into()], + }) + } + + /// Return a copy with the `c0` component replaced. + pub(crate) fn with_c0(mut self, c0: Fp) -> Self { + self.0.fp[0] = c0.into(); + self + } + + /// Return a copy with the `c1` component replaced. + pub(crate) fn with_c1(mut self, c1: Fp) -> Self { + self.0.fp[1] = c1.into(); + self + } +} + +type_cvrt!(From for Fp2, |fp| Self::new(*fp, Fp::default())); + +type_cvrt!(From for blst_fp2, |fp2| fp2.0); + +type_cvrt!(From for Fp2, |raw| Self(*raw)); + +#[cfg(test)] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use super::*; + use crate::bls::tests::{make_id, GROUP_ORDER, RSEED}; + use crate::bls::{BlsScChia, BlsScIetf, BlsScheme, BlsSecretKey}; + use crate::prelude::*; + + use getrandom::SysRng; + use rand_core::UnwrapErr; + use rstest::rstest; + + #[rstest] + fn repr_round_trips_and_rejects_the_order() { + let x = Fr::random(&mut UnwrapErr(SysRng)); + assert_eq!(Fr::from_repr(x.to_repr()).unwrap(), x); + + // The order itself is the least non-canonical encoding. + let mut order = (-Fr::ONE).to_repr(); + order[0] += 1; + assert!(bool::from(Fr::from_repr(order).is_none())); + } + + /// The encoding is little-endian, which the round trip alone cannot tell + /// apart from big-endian. + #[rstest] + fn repr_is_little_endian() { + assert_eq!(Fr::from(1).to_repr()[0], 1); + assert_eq!(Fr::from(1).to_repr()[REPR_LEN - 1], 0); + } + + #[rstest] + fn arithmetic_agrees_with_the_field_axioms() { + let mut rng = UnwrapErr(SysRng); + let a = Fr::random(&mut rng); + let b = Fr::random(&mut rng); + + assert_eq!(a + b, b + a); + assert_eq!(a - a, Fr::ZERO); + assert_eq!(a + a, a.double()); + assert_eq!(a * a, a.square()); + assert_eq!(a * b * b.invert().unwrap(), a); + assert_eq!(-a + a, Fr::ZERO); + } + + /// Both halves of the square-root path at once: `sqrt` drives the exponent + /// above and `sqrt_ratio` drives `ff`'s generic form off the constants. + #[rstest] + fn square_roots_recover_the_square() { + let a = Fr::random(&mut UnwrapErr(SysRng)); + let root = a.square().sqrt().unwrap(); + assert!(root == a || root == -a); + + let (is_square, root) = Fr::sqrt_ratio(&a.square(), &Fr::ONE); + assert!(bool::from(is_square)); + assert!(root == a || root == -a); + + // A non-residue: the generator is one by construction. + assert!(bool::from(Fr::MULTIPLICATIVE_GENERATOR.sqrt().is_none())); + } + + #[rstest] + fn is_odd_reads_the_low_bit() { + assert!(bool::from(Fr::from(3).is_odd())); + assert!(bool::from(Fr::from(4).is_even())); + } + /// An id is read most significant byte first, so a one in the last byte is + /// the field's one and the same byte at the front is `2^248` instead. + #[rstest] + fn share_id_reads_big_endian() { + assert_eq!(Fr::from_share_id(&make_id(1)).unwrap(), Fr::ONE); + assert_eq!(Fr::from_share_id(&make_id(258)).unwrap(), Fr::from(258)); + + let mut leading = [0u8; 32]; + leading[0] = 1; + let mut expected = Fr::ONE; + for _ in 0..248 { + expected = expected.double(); + } + assert_eq!(Fr::from_share_id(&BlsShareId::from_bytes(leading)).unwrap(), expected); + } + + /// An id is an integer rather than an encoding of one, so a value at or + /// above the order is reduced where [`PrimeField::from_repr`] would refuse + /// it outright. + #[rstest] + fn share_id_reduces_past_the_order() { + let mut order_plus_one = GROUP_ORDER; + order_plus_one[31] += 1; + assert_eq!( + Fr::from_share_id(&BlsShareId::from_bytes(order_plus_one)).unwrap(), + Fr::ONE + ); + + let mut order_le = GROUP_ORDER; + order_le.reverse(); + assert!(bool::from(Fr::from_repr(order_le).is_none())); + } + + /// The polynomial evaluated at a zero-reducing id is its constant term, so + /// the id is refused rather than handed the master secret. + #[rstest] + #[case::zero([0u8; 32])] + #[case::order(GROUP_ORDER)] + fn share_id_rejects_the_zero_residue(#[case] bytes: [u8; 32]) { + assert_eq!( + Fr::from_share_id(&BlsShareId::from_bytes(bytes)), + Err(BlsError::InvalidShareId) + ); + } + + /// Read a secret key back as the field element it is: the wire form is + /// big-endian and `from_repr` wants the canonical little-endian one. + fn scalar_of(sk: &BlsSecretKey) -> Fr { + let mut le = *sk.to_bytes(); + le.reverse(); + Fr::from_repr(le).unwrap() + } + + /// The share engine reads an id through this same reduction, so a derived + /// share is the master polynomial evaluated at the scalar it yields. A + /// reversed byte order or a refusing parse would part the two. + fn assert_share_id_matches_the_engine() { + let master: Vec> = RSEED[..3] + .iter() + .map(|ikm| BlsSecretKey::::generate(ikm).unwrap()) + .collect(); + let refs: Vec<&BlsSecretKey> = master.iter().collect(); + let coeffs: Vec = master.iter().map(scalar_of).collect(); + + for i in 1..=4u32 { + let id = make_id(i); + let x = Fr::from_share_id(&id).unwrap(); + let evaluated = coeffs[0] + coeffs[1] * x + coeffs[2] * x * x; + + let share = BlsSecretKey::::derive_share(&refs, &id).unwrap(); + assert_eq!(scalar_of(&share), evaluated); + } + } + + #[rstest] + #[case::chia(assert_share_id_matches_the_engine::)] + #[case::ietf(assert_share_id_matches_the_engine::)] + fn share_id_matches_the_engine(#[case] assertion: fn()) { + assertion(); + } +} diff --git a/pkgs/pkc/src/bls/scheme_chia.rs b/pkgs/pkc/src/bls/scheme_chia.rs index 4b677625..6c73048b 100644 --- a/pkgs/pkc/src/bls/scheme_chia.rs +++ b/pkgs/pkc/src/bls/scheme_chia.rs @@ -6,22 +6,21 @@ //! Legacy BLS scheme implementation. -use super::blst_ffi::{self, G1Affine, G2Affine, Point, G1, G2}; +use super::blst_ffi; use super::chia_h2c; +use super::curve_consts::HALF_P; use super::error::BlsError; +use super::group::{G1Affine, G2Affine, Point, G1, G2}; +use super::scalar::FR_BITS; use super::scheme_ops::BlsScheme; use super::schemes::BlsScChia; use crate::prelude::*; use blst::min_pk; -use hex_conservative::hex; use zeroize::Zeroize; /// y.c1 > (p-1)/2, matching the legacy sign convention. fn y_c1_is_larger(y_c1: &[u8]) -> bool { - const HALF_P: [u8; 48] = - hex!("0d0088f51cbff34d258dd3db21a5d66bb23ba5c279c2895fb39869507b587b120f55ffff58a9ffffdcff7fffffffd555"); - y_c1.len() >= 48 && y_c1[..48] > HALF_P[..] } @@ -213,7 +212,7 @@ impl BlsScheme for BlsScChia { fn sign(sk: &Self::InnerSk, msg: &Self::Msg) -> Self::InnerSig { let h = chia_h2c::hash_to_g2(msg); // blst_sign_pk_in_g1 applies IETF transformations, do manually instead. - h.mul_scalar(&sk.b, blst_ffi::FR_BITS).to_affine() + h.mul_scalar(&sk.b, FR_BITS).to_affine() } /// Check the pairing e(sig, G1) == e(H(msg), pk). @@ -238,7 +237,7 @@ impl BlsScheme for BlsScChia { } let mut acc = pks[0].to_projective(); for pk in &pks[1..] { - acc = acc + pk.to_projective(); + acc += pk.to_projective(); } Ok(acc.to_affine()) } @@ -250,7 +249,7 @@ impl BlsScheme for BlsScChia { } let mut acc = sigs[0].to_projective(); for sig in &sigs[1..] { - acc = acc + sig.to_projective(); + acc += sig.to_projective(); } Ok(acc.to_affine()) } diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index a6e67640..d60f3722 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -6,8 +6,8 @@ //! Basic BLS scheme implementation. -use super::blst_ffi::{G1Affine, G2Affine, G1, G2}; use super::error::BlsError; +use super::group::{G1Affine, G2Affine, G1, G2}; use super::scheme_ops::{verify_ok, BlsScheme}; use super::schemes::BlsScIetf; use super::sig_id::BlsSigId; diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index 1cc53ef5..c9b2ed99 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -6,13 +6,16 @@ //! Scalar-field arithmetic and threshold helpers. -use super::blst_ffi::{self, Fr, Point, G1, G2}; +use super::blst_ffi; use super::error::BlsError; +use super::group::{Point, G1, G2}; +use super::scalar::{Fr, FR_BITS}; use super::schemes::BlsSchemeId; +use super::BlsShareId; use crate::prelude::*; use blst::BLST_ERROR; -use dash_num::Hash256; +use ff::Field; use sha2::{Digest, Sha256}; use zeroize::{Zeroize, Zeroizing}; @@ -21,7 +24,7 @@ use core::fmt::Debug; /// Multiplier width for the secure-aggregation weights. /// /// The weight is an unreduced SHA-256 digest, so it needs the full width -/// rather than [`blst_ffi::FR_BITS`]. +/// rather than [`FR_BITS`]. const WEIGHT_BITS: usize = 256; /// Map a blst verification outcome onto a [`BlsError`]. @@ -139,7 +142,7 @@ pub trait BlsScheme: BlsSchemeId { let point = Self::pk_to_g1(peer_pk)?; let mut sk_bytes = Self::sk_to_bytes(sk); let mut sk_scalar = blst_ffi::scalar_from_bendian(&sk_bytes); - let product = point.mul_scalar(&sk_scalar.b, blst_ffi::FR_BITS); + let product = point.mul_scalar(&sk_scalar.b, FR_BITS); sk_bytes.zeroize(); sk_scalar.b.zeroize(); Self::g1_to_pk(product) @@ -161,6 +164,22 @@ pub trait BlsScheme: BlsSchemeId { /// `InvalidSignature` when a signature fails to aggregate. fn aggregate_sig(sigs: &[&Self::InnerSig]) -> Result; + /// Remove one signature from another. + /// + /// # Errors + /// + /// Returns `InvalidSignature` when either signature or the difference + /// fails to decode, or when the difference is the identity. + fn sub_sig(sig: &Self::InnerSig, other: &Self::InnerSig) -> Result { + let difference = Self::sig_to_g2(sig)? - Self::sig_to_g2(other)?; + // The identity signs nothing and verifies against everything paired with an + // identity key. + if difference.is_inf() { + return Err(BlsError::InvalidSignature); + } + Self::g2_to_sig(difference) + } + /// Verify an aggregate signature where every signer signed `msg`. /// /// # Errors @@ -186,16 +205,16 @@ pub trait BlsScheme: BlsSchemeId { /// Returns `InvalidPublicKey` when the bytes do not decode to a point. fn secure_agg_point(pk_bytes: &[u8; 48]) -> Result; - /// Verify an aggregate with public-key weighting to resist rogue keys. + /// Aggregate public keys with the weighting that resists rogue keys. /// /// Each key is weighted by `SHA256(index || SHA256(sorted pk bytes))` so a /// signer cannot cancel an honest key with a crafted rogue one. /// /// # Errors /// - /// Returns `EmptyAggregation` when no keys are given, `InvalidPublicKey` - /// when a key fails to decode, or `VerifyFailed` on mismatch. - fn secure_verify_aggregates(sig: &Self::InnerSig, msg: &Self::Msg, pks: &[&Self::InnerPk]) -> Result<(), BlsError> { + /// Returns `EmptyAggregation` when no keys are given, or `InvalidPublicKey` + /// when a key or the weighted sum fails to decode. + fn secure_aggregate_pk(pks: &[&Self::InnerPk]) -> Result { if pks.is_empty() { return Err(BlsError::EmptyAggregation); } @@ -207,11 +226,22 @@ pub trait BlsScheme: BlsSchemeId { let mut acc = G1::identity(); for (pk_bytes, weight) in sorted.iter().zip(secure_weights(&sorted)) { - acc = acc + Self::secure_agg_point(pk_bytes)?.mul_scalar(&weight.b, WEIGHT_BITS); + acc += Self::secure_agg_point(pk_bytes)?.mul_scalar(&weight.b, WEIGHT_BITS); } - let agg_pk = Self::g1_to_pk(acc)?; - Self::verify(sig, msg, &agg_pk) + Self::g1_to_pk(acc) + } + + /// Verify an aggregate against the weighted key + /// [`Self::secure_aggregate_pk`] builds, in the scheme's own single + /// pairing. + /// + /// # Errors + /// + /// Returns `EmptyAggregation` when no keys are given, `InvalidPublicKey` + /// when a key fails to decode, or `VerifyFailed` on mismatch. + fn secure_verify_aggregates(sig: &Self::InnerSig, msg: &Self::Msg, pks: &[&Self::InnerPk]) -> Result<(), BlsError> { + Self::verify(sig, msg, &Self::secure_aggregate_pk(pks)?) } /// Aggregate signatures under the same public-key weighting that @@ -243,7 +273,7 @@ pub trait BlsScheme: BlsSchemeId { let mut acc = G2::identity(); for ((_, sig), weight) in paired.iter().zip(secure_weights(&sorted)) { - acc = acc + Self::sig_to_g2(sig)?.mul_scalar(&weight.b, WEIGHT_BITS); + acc += Self::sig_to_g2(sig)?.mul_scalar(&weight.b, WEIGHT_BITS); } Self::g2_to_sig(acc) @@ -276,9 +306,9 @@ pub trait BlsScheme: BlsSchemeId { fn split_sk( sk: &Self::InnerSk, threshold: usize, - ids: &[Hash256], - rng: &mut impl rand_core::CryptoRngCore, - mut into_share: impl FnMut(Hash256, Self::InnerSk) -> S, + ids: &[BlsShareId], + rng: &mut impl rand_core::CryptoRng, + mut into_share: impl FnMut(BlsShareId, Self::InnerSk) -> S, ) -> Result, BlsError> { if threshold < 2 || ids.is_empty() || threshold > ids.len() { return Err(BlsError::ThresholdTooLarge); @@ -286,11 +316,11 @@ pub trait BlsScheme: BlsSchemeId { // An id congruent to zero mod r would make the share equal the master // key, and ids congruent mod r collide during interpolation. - let id_refs: Vec<&Hash256> = ids.iter().collect(); - reduce_share_ids(&id_refs)?; + let id_refs: Vec<&BlsShareId> = ids.iter().collect(); + let xs = reduce_share_ids(&id_refs)?; let sk_bytes = Zeroizing::new(Self::sk_to_bytes(sk)); - let raw = generate_shares(&sk_bytes, threshold, ids, rng).map_err(|()| BlsError::InvalidSecretKey)?; + let raw = generate_shares(&sk_bytes, threshold, ids, &xs, rng).map_err(|()| BlsError::InvalidSecretKey)?; raw .into_iter() @@ -309,7 +339,7 @@ pub trait BlsScheme: BlsSchemeId { /// `ids` and `sigs` differ in length, `InvalidShareId`/`DuplicateShareId` on /// bad ids, or `InvalidSignature` when a share or the recovered point fails /// to decode. - fn recover_sig_shares(ids: &[&Hash256], sigs: &[&Self::InnerSig]) -> Result { + fn recover_sig_shares(ids: &[&BlsShareId], sigs: &[&Self::InnerSig]) -> Result { // ids and sigs are paired; a length mismatch would desync interpolation // and could index out of bounds in interpolate_g2. if sigs.len() < 2 || ids.len() != sigs.len() { @@ -335,7 +365,7 @@ pub trait BlsScheme: BlsSchemeId { /// Returns `InvalidVerificationVector` when fewer than two keys are /// given, `InvalidShareId` on a zero-reducing id, or `InvalidPublicKey` /// when a coefficient or the result fails to decode. - fn derive_pk_share(master_pks: &[&Self::InnerPk], id: &Hash256) -> Result { + fn derive_pk_share(master_pks: &[&Self::InnerPk], id: &BlsShareId) -> Result { // Evaluating the verification-vector polynomial needs >= 2 coefficients. if master_pks.len() < 2 { return Err(BlsError::InvalidVerificationVector); @@ -345,7 +375,7 @@ pub trait BlsScheme: BlsSchemeId { .map(|pk| Self::pk_to_g1(pk)) .collect::, BlsError>>()?; - let x = reduce_id(id)?; + let x = Fr::from_share_id(id)?; let result = eval_poly_g1(&coeffs_g1, &x); Self::g1_to_pk(result) @@ -363,7 +393,7 @@ pub trait BlsScheme: BlsSchemeId { /// Returns `InvalidVerificationVector` when fewer than two keys are given, /// `InvalidShareId` on a zero-reducing id, or `InvalidSecretKey` when the /// result is not a valid scalar. - fn derive_sk_share(master_sks: &[&Self::InnerSk], id: &Hash256) -> Result { + fn derive_sk_share(master_sks: &[&Self::InnerSk], id: &BlsShareId) -> Result { if master_sks.len() < 2 { return Err(BlsError::InvalidVerificationVector); } @@ -376,7 +406,7 @@ pub trait BlsScheme: BlsSchemeId { scalar.b.zeroize(); } - let x = reduce_id(id)?; + let x = Fr::from_share_id(id)?; let mut y = poly_eval(&coeffs, &x); let mut y_scalar = blst::blst_scalar::from(&y); @@ -417,7 +447,7 @@ fn sum_sk_scalars(key_bytes: &[[u8; 32]]) -> Zeroizing<[u8; 32]> { for bytes in key_bytes { let mut scalar = blst_ffi::scalar_from_bendian(bytes); let mut term = Fr::from(&scalar); - acc = acc + term; + acc += term; term.zeroize(); scalar.b.zeroize(); } @@ -432,7 +462,7 @@ fn sum_sk_scalars(key_bytes: &[[u8; 32]]) -> Zeroizing<[u8; 32]> { /// zeroized on drop. A custom `Debug` redacts the secret scalar. struct RawShare { /// Participant identifier. - id: Hash256, + id: BlsShareId, /// Secret scalar bytes, zeroized on drop. secret: Zeroizing<[u8; 32]>, } @@ -452,8 +482,9 @@ impl core::fmt::Debug for RawShare { fn generate_shares( sk_bytes: &[u8; 32], threshold: usize, - ids: &[Hash256], - rng: &mut impl rand_core::CryptoRngCore, + ids: &[BlsShareId], + xs: &[Fr], + rng: &mut impl rand_core::CryptoRng, ) -> Result, ()> { let mut coeffs = Zeroizing::new(Vec::with_capacity(threshold)); @@ -474,9 +505,8 @@ fn generate_shares( } let mut shares = Vec::with_capacity(ids.len()); - for id in ids { - let x = fr_from_hash(id); - let mut y = poly_eval(&coeffs, &x); + for (id, x) in ids.iter().zip(xs) { + let mut y = poly_eval(&coeffs, x); let mut y_scalar = blst::blst_scalar::from(&y); let y_bytes = blst_ffi::bendian_from_scalar(&y_scalar); @@ -524,7 +554,7 @@ fn interpolate_g2(ids: &[Fr], points: &[G2]) -> G2 { for i in 0..n { // Convert Fr coefficient to scalar for point multiplication. let scalar = blst::blst_scalar::from(&coeffs[i]); - result = result + points[i].mul_scalar(&scalar.b, blst_ffi::FR_BITS); + result += points[i].mul_scalar(&scalar.b, FR_BITS); } result } @@ -536,19 +566,19 @@ fn compute_lagrange_coeffs(ids: &[Fr]) -> Vec { for i in 0..n { // L_i = prod_{j!=i} ids[j] / (ids[j] - ids[i]) - let mut num = Fr::one(); - let mut den = Fr::one(); + let mut num = Fr::ONE; + let mut den = Fr::ONE; for j in 0..n { if i == j { continue; } // num *= ids[j] - num = num * ids[j]; + num *= ids[j]; // den *= (ids[j] - ids[i]) let diff = ids[j] - ids[i]; - den = den * diff; + den *= diff; } coeffs.push(num * den.inverse()); @@ -568,44 +598,23 @@ fn eval_poly_g1(coeffs_g1: &[G1], x: &Fr) -> G1 { let x_scalar = blst::blst_scalar::from(x); let mut result = coeffs_g1[n - 1]; for i in (0..n - 1).rev() { - result = result.mul_scalar(&x_scalar.b, blst_ffi::FR_BITS) + coeffs_g1[i]; + result = result.mul_scalar(&x_scalar.b, FR_BITS) + coeffs_g1[i]; } result } -/// Convert a 32-byte participant ID to a scalar. -fn fr_from_hash(id: &Hash256) -> Fr { - Fr::from(&blst_ffi::scalar_from_bendian(id.as_bytes())) -} - -/// Reduce a participant id into the scalar field, rejecting zero. -/// -/// An id congruent to zero mod `r` evaluates the polynomial at its -/// constant term, which leaks the master secret in share generation. -fn reduce_id(id: &Hash256) -> Result { - let fr = fr_from_hash(id); - if blst::blst_scalar::from(&fr).b == [0u8; 32] { - return Err(BlsError::InvalidShareId); - } - Ok(fr) -} - /// Reduce participant ids into the scalar field, rejecting ids that /// reduce to zero and duplicates after reduction. /// /// Two distinct hashes congruent mod `r` share a scalar, producing a /// zero Lagrange denominator that blst inverts to zero silently; a /// raw-byte duplicate check would not catch them. -fn reduce_share_ids(ids: &[&Hash256]) -> Result, BlsError> { - let fr_ids: Vec = ids.iter().map(|id| fr_from_hash(id)).collect(); - let mut reduced: Vec<[u8; 32]> = Vec::with_capacity(fr_ids.len()); - for fr in &fr_ids { - let bytes = blst::blst_scalar::from(fr).b; - if bytes == [0u8; 32] { - return Err(BlsError::InvalidShareId); - } - reduced.push(bytes); - } +fn reduce_share_ids(ids: &[&BlsShareId]) -> Result, BlsError> { + let fr_ids = ids + .iter() + .map(|id| Fr::from_share_id(id)) + .collect::, BlsError>>()?; + let mut reduced: Vec<[u8; 32]> = fr_ids.iter().map(|fr| *fr.to_lendian()).collect(); reduced.sort_unstable(); for pair in reduced.windows(2) { if pair[0] == pair[1] { diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index 3b616998..c8850f76 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -9,6 +9,7 @@ use super::dh_bytes::BlsDhBytes; use super::error::BlsError; use super::public_ops::BlsPublicKey; +use super::scalar::Fr; use super::scheme_ops::BlsScheme; use super::sig_basic::BlsSignature; use super::{BlsScIetf, BlsSigId, BlsSkBytes, BLS_SK_LEN}; @@ -52,6 +53,19 @@ impl BlsSecretKey { Zeroizing::new(S::sk_to_bytes(&self.0)) } + /// Retag this key under another scheme. + /// + /// A secret key is a scalar, so a retag re-encodes nothing; only the public + /// key it derives changes encoding rather than value. The secret-side + /// companion to [`BlsPublicKey::to_scheme`], for a holder typed to one arm. + /// + /// # Errors + /// + /// Returns `InvalidSecretKey` when the target scheme refuses the scalar. + pub fn to_scheme(&self) -> Result, BlsError> { + BlsSecretKey::::from_bytes(&self.to_bytes()) + } + /// Derive the corresponding public key. pub fn public_key(&self) -> BlsPublicKey { BlsPublicKey(S::derive_pk(&self.0)) @@ -140,6 +154,14 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSecretKey, BlsErro Self::from_bytes(bytes.as_bytes()) }); +type_cvrt!(for[S: BlsScheme] From> for Zeroizing, |sk| { + Zeroizing::new(Fr::from_bendian_reduce(&sk.to_bytes())) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom for BlsSecretKey, BlsError, |scalar| { + Self::from_bytes(&scalar.to_bendian()) +}); + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { @@ -164,6 +186,24 @@ mod tests { agg_sk: String, } + /// A retag moves no scalar, so the bytes survive and the derived public key + /// is the converted one rather than a different key. + fn assert_scheme_retag_keeps_the_scalar() { + let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let there = sk.to_scheme::().unwrap(); + + assert_eq!(*there.to_bytes(), *sk.to_bytes()); + assert_eq!(there.public_key(), sk.public_key().to_scheme::().unwrap()); + } + + #[rstest] + #[case::chia_to_ietf(assert_scheme_retag_keeps_the_scalar::)] + #[case::ietf_to_chia(assert_scheme_retag_keeps_the_scalar::)] + #[case::ietf_to_ietf(assert_scheme_retag_keeps_the_scalar::)] + fn scheme_retag_keeps_the_scalar(#[case] assertion: fn()) { + assertion(); + } + fn assert_roundtrip() { let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); let bytes = sk.to_bytes(); diff --git a/pkgs/pkc/src/bls/share_id.rs b/pkgs/pkc/src/bls/share_id.rs new file mode 100644 index 00000000..718e968a --- /dev/null +++ b/pkgs/pkc/src/bls/share_id.rs @@ -0,0 +1,38 @@ +// +// Copyright (c) 2026-present, The Dash Core developers +// SPDX-License-Identifier: MIT +// See the accompanying file LICENSE or https://opensource.org/license/MIT +// + +//! Threshold participant identifier. + +use dash_types::derive_bytes; +use dash_types::type_id::Unencodable; + +/// Threshold participant identifier length. +pub const BLS_ID_LEN: usize = 32; + +/// Threshold participant identifier. +#[derive(Unencodable)] +pub struct BlsShareId { + inner: [u8; BLS_ID_LEN], +} + +impl BlsShareId { + /// Wraps raw bytes. + pub const fn from_bytes(bytes: [u8; BLS_ID_LEN]) -> Self { + Self { inner: bytes } + } + + /// Borrows the inner byte array. + pub const fn as_bytes(&self) -> &[u8; BLS_ID_LEN] { + &self.inner + } + + /// Returns the inner byte array. + pub const fn into_bytes(self) -> [u8; BLS_ID_LEN] { + self.inner + } +} + +derive_bytes!(BlsShareId, BLS_ID_LEN, rev); diff --git a/pkgs/pkc/src/bls/share_ops.rs b/pkgs/pkc/src/bls/share_ops.rs index 9e9a6c5b..a2b597b3 100644 --- a/pkgs/pkc/src/bls/share_ops.rs +++ b/pkgs/pkc/src/bls/share_ops.rs @@ -11,12 +11,12 @@ use super::public_ops::BlsPublicKey; use super::scheme_ops::BlsScheme; use super::secret_ops::BlsSecretKey; use super::sig_basic::BlsSignature; +use super::BlsShareId; use crate::prelude::*; -use dash_num::Hash256; use dash_types::qtypestr; use dash_types::type_id::Unencodable; -use rand_core::CryptoRngCore; +use rand_core::CryptoRng; use core::fmt::{Debug, Formatter, Result as FmtResult}; use core::hash::{Hash, Hasher}; @@ -24,18 +24,18 @@ use core::hash::{Hash, Hasher}; /// Secret key share for threshold signing. #[derive(Unencodable)] pub struct BlsSkShare { - id: Hash256, + id: BlsShareId, sk: BlsSecretKey, } impl BlsSkShare { /// Construct a secret key share from an ID and a secret key. - pub fn new(id: Hash256, sk: BlsSecretKey) -> Self { + pub fn new(id: BlsShareId, sk: BlsSecretKey) -> Self { Self { id, sk } } - /// Participant identifier (32-byte hash). - pub fn id(&self) -> &Hash256 { + /// Participant identifier. + pub fn id(&self) -> &BlsShareId { &self.id } @@ -74,18 +74,18 @@ impl Debug for BlsSkShare { #[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] #[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] pub struct BlsSigShare { - id: Hash256, + id: BlsShareId, sig: BlsSignature, } impl BlsSigShare { /// Construct a signature share from an ID and a signature. - pub fn new(id: Hash256, sig: BlsSignature) -> Self { + pub fn new(id: BlsShareId, sig: BlsSignature) -> Self { Self { id, sig } } - /// Participant identifier (32-byte hash). - pub fn id(&self) -> &Hash256 { + /// Participant identifier. + pub fn id(&self) -> &BlsShareId { &self.id } @@ -139,8 +139,8 @@ impl BlsSecretKey { pub fn split( &self, threshold: usize, - ids: &[Hash256], - rng: &mut impl CryptoRngCore, + ids: &[BlsShareId], + rng: &mut impl CryptoRng, ) -> Result>, BlsError> { S::split_sk(&self.0, threshold, ids, rng, |id, inner| { BlsSkShare::new(id, BlsSecretKey::from_inner(inner)) @@ -155,7 +155,7 @@ impl BlsSecretKey { /// Returns `InvalidVerificationVector` when fewer than two master keys are /// given, `InvalidShareId` on a zero-reducing id, or `InvalidSecretKey` /// when the result is not a valid scalar. - pub fn derive_share(master_sks: &[&Self], id: &Hash256) -> Result { + pub fn derive_share(master_sks: &[&Self], id: &BlsShareId) -> Result { let inner_refs: Vec<&S::InnerSk> = master_sks.iter().map(|sk| &sk.0).collect(); S::derive_sk_share(&inner_refs, id).map(Self::from_inner) } @@ -170,7 +170,7 @@ impl BlsPublicKey { /// Returns `InvalidVerificationVector` when fewer than two master keys are /// given, `InvalidShareId` on a zero-reducing id, or `InvalidPublicKey` /// when a coefficient or the result fails to decode. - pub fn derive_share(master_pks: &[&Self], id: &Hash256) -> Result { + pub fn derive_share(master_pks: &[&Self], id: &BlsShareId) -> Result { let inner_refs: Vec<&S::InnerPk> = master_pks.iter().map(|pk| &pk.0).collect(); S::derive_pk_share(&inner_refs, id).map(Self::from_inner) } @@ -180,17 +180,18 @@ impl BlsPublicKey { #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; - use crate::bls::tests::{hash_from_hex, make_id, sequential_ids, GROUP_ORDER, MSG_DEADBEEF, RSEED}; + use crate::bls::tests::{id_from_hex, make_id, sequential_ids, GROUP_ORDER, MSG_DEADBEEF, RSEED}; use crate::bls::{BlsScChia, BlsScIetf}; use cfg_if::cfg_if; use dash_dev::{arr_from_hex, Corpus, Value}; + use getrandom::SysRng; use hex_conservative::DisplayHex; - use rand_core::OsRng; + use rand_core::UnwrapErr; use rstest::rstest; /// The scalar-field order `r + 1`, congruent to `1` mod `r`. - fn group_order_plus_one() -> Hash256 { + fn group_order_plus_one() -> BlsShareId { let mut bytes = GROUP_ORDER; for b in bytes.iter_mut().rev() { let (v, carry) = b.overflowing_add(1); @@ -199,7 +200,7 @@ mod tests { break; } } - Hash256::from_bytes(bytes) + BlsShareId::from_bytes(bytes) } /// A 1-of-n split hands the master key to every participant, so a `threshold` @@ -210,11 +211,14 @@ mod tests { let ids = sequential_ids(5); for threshold in [0, 1, ids.len() + 1] { assert!(matches!( - sk.split(threshold, &ids, &mut OsRng), + sk.split(threshold, &ids, &mut UnwrapErr(SysRng)), Err(BlsError::ThresholdTooLarge) )); } - assert!(matches!(sk.split(2, &[], &mut OsRng), Err(BlsError::ThresholdTooLarge))); + assert!(matches!( + sk.split(2, &[], &mut UnwrapErr(SysRng)), + Err(BlsError::ThresholdTooLarge) + )); } #[rstest] @@ -229,13 +233,19 @@ mod tests { fn assert_zero_reducing_id_rejected() { let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let zero = Hash256::from_bytes([0u8; 32]); + let zero = BlsShareId::from_bytes([0u8; 32]); let ids = [make_id(1), zero]; - assert!(matches!(sk.split(2, &ids, &mut OsRng), Err(BlsError::InvalidShareId))); + assert!(matches!( + sk.split(2, &ids, &mut UnwrapErr(SysRng)), + Err(BlsError::InvalidShareId) + )); - let order = Hash256::from_bytes(GROUP_ORDER); + let order = BlsShareId::from_bytes(GROUP_ORDER); let ids = [make_id(1), order]; - assert!(matches!(sk.split(2, &ids, &mut OsRng), Err(BlsError::InvalidShareId))); + assert!(matches!( + sk.split(2, &ids, &mut UnwrapErr(SysRng)), + Err(BlsError::InvalidShareId) + )); } #[rstest] @@ -250,7 +260,10 @@ mod tests { fn assert_congruent_ids_rejected() { let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); let ids = [make_id(1), group_order_plus_one()]; - assert!(matches!(sk.split(2, &ids, &mut OsRng), Err(BlsError::DuplicateShareId))); + assert!(matches!( + sk.split(2, &ids, &mut UnwrapErr(SysRng)), + Err(BlsError::DuplicateShareId) + )); } #[rstest] @@ -284,7 +297,7 @@ mod tests { Err(BlsError::InvalidVerificationVector) )); assert!(matches!( - BlsSecretKey::::derive_share(&master_refs, &Hash256::from_bytes([0u8; 32])), + BlsSecretKey::::derive_share(&master_refs, &BlsShareId::from_bytes([0u8; 32])), Err(BlsError::InvalidShareId) )); } @@ -399,7 +412,7 @@ mod tests { let sk_share = BlsSecretKey::::from_bytes(&arr_from_hex(sk_hex.as_str().unwrap())).unwrap(); let pk_from_share = sk_share.public_key(); - let member_id = hash_from_hex(&member_ids[member_idx]); + let member_id = id_from_hex(&member_ids[member_idx]); let pk_from_vvec = BlsPublicKey::derive_share(&vvec_refs, &member_id).unwrap(); let matches = pk_from_share.to_bytes() == pk_from_vvec.to_bytes(); @@ -548,22 +561,13 @@ mod tests { .collect(); let quorum_hash: [u8; 32] = arr_from_hex(fin["quorum_hash"].as_str().unwrap()); - - // signer_ids are in internal byte order; byte-reverse to match the - // display-order member_ids. let sig_shares: Vec> = signer_ids .iter() .map(|sid| { - let sid_bytes = arr_from_hex::<32>(sid); - let sid_display = sid_bytes - .iter() - .copied() - .rev() - .collect::>() - .to_lower_hex_string(); + let member_id = BlsShareId::from_bytes(arr_from_hex::<32>(sid)); + let sid_display = member_id.to_string(); let idx = member_ids.iter().position(|m| *m == sid_display).unwrap(); let sk = BlsSecretKey::::from_bytes(&arr_from_hex(commits[idx]["sk_share"].as_str().unwrap())).unwrap(); - let member_id = hash_from_hex(&sid_display); BlsSkShare::new(member_id, sk).sign(S::msg_ref(&quorum_hash)) }) .collect(); @@ -579,7 +583,7 @@ mod tests { ); // Cross-check: recovery from all members should match the subset recovery. - let all_ids: Vec = member_ids.iter().map(|mid| hash_from_hex(mid)).collect(); + let all_ids: Vec = member_ids.iter().map(|mid| id_from_hex(mid)).collect(); let all_shares: Vec> = commits .iter() .zip(all_ids.iter()) diff --git a/pkgs/pkc/src/bls/sig_aggregate.rs b/pkgs/pkc/src/bls/sig_aggregate.rs index ab9a457e..adba4e8a 100644 --- a/pkgs/pkc/src/bls/sig_aggregate.rs +++ b/pkgs/pkc/src/bls/sig_aggregate.rs @@ -24,6 +24,17 @@ impl BlsSignature { S::aggregate_sig(&inner_refs).map(Self::from_inner) } + /// Remove `other` from this signature, leaving behind the aggregate of the + /// rest. + /// + /// # Errors + /// + /// Returns `InvalidSignature` when either signature or the difference fails + /// to decode, or when the difference is the identity. + pub fn sub_insecure(&self, other: &Self) -> Result { + S::sub_sig(&self.0, &other.0).map(Self::from_inner) + } + /// Verify an aggregated signature where every signer signed /// the same message. /// @@ -146,6 +157,46 @@ mod tests { assertion(); } + /// Subtraction is the inverse of aggregation, so taking one signature back + /// out of the pair leaves the other exactly as it was signed. + fn assert_sub_insecure_undoes_aggregation() { + let sk1 = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk2 = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let msg = S::msg_ref(&MSG_DEADBEEF); + + let sig1 = sk1.sign(msg); + let sig2 = sk2.sign(msg); + let agg = BlsSignature::::aggregate(&[&sig1, &sig2]).unwrap(); + + assert_eq!(agg.sub_insecure(&sig1).unwrap(), sig2); + assert_eq!(agg.sub_insecure(&sig2).unwrap(), sig1); + assert!(agg.sub_insecure(&sig1).unwrap().verify(msg, &sk2.public_key()).is_ok()); + } + + #[rstest] + #[case::chia(assert_sub_insecure_undoes_aggregation::)] + #[case::ietf(assert_sub_insecure_undoes_aggregation::)] + fn sub_insecure_undoes_aggregation(#[case] assertion: fn()) { + assertion(); + } + + /// A signature taken from itself leaves the identity, which signs nothing. + /// Only the IETF decoder refuses identities, so a check is added to ensure + /// rejection under both schemes. + fn assert_sub_insecure_rejects_the_identity() { + let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sig = sk.sign(S::msg_ref(&MSG_DEADBEEF)); + + assert_eq!(sig.sub_insecure(&sig), Err(BlsError::InvalidSignature)); + } + + #[rstest] + #[case::chia(assert_sub_insecure_rejects_the_identity::)] + #[case::ietf(assert_sub_insecure_rejects_the_identity::)] + fn sub_insecure_rejects_the_identity(#[case] assertion: fn()) { + assertion(); + } + fn assert_secure_verify(scheme: &str) { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_secure_aggregate").scope(scheme); let vecs: Vec = corpus.vectors("verify"); diff --git a/pkgs/pkc/src/bls/sig_basic.rs b/pkgs/pkc/src/bls/sig_basic.rs index 066634bc..16dd2fa3 100644 --- a/pkgs/pkc/src/bls/sig_basic.rs +++ b/pkgs/pkc/src/bls/sig_basic.rs @@ -7,6 +7,7 @@ //! Scheme-generic BLS signature. use super::error::BlsError; +use super::group::G2; use super::public_ops::BlsPublicKey; use super::scheme_ops::BlsScheme; use super::{BlsScIetf, BlsSigBytes, BlsSigId, BLS_SIG_LEN}; @@ -116,6 +117,14 @@ type_cvrt!(for[S: BlsScheme] TryFrom> for BlsSignature, BlsErr Self::from_bytes(bytes.as_bytes()) }); +type_cvrt!(for[S: BlsScheme] TryFrom> for G2, BlsError, |sig| { + S::sig_to_g2(&sig.0) +}); + +type_cvrt!(for[S: BlsScheme] TryFrom for BlsSignature, BlsError, |point| { + S::g2_to_sig(*point).map(Self::from_inner) +}); + #[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { diff --git a/pkgs/pkc/src/bls/sig_threshold.rs b/pkgs/pkc/src/bls/sig_threshold.rs index 654f5fd3..070151c6 100644 --- a/pkgs/pkc/src/bls/sig_threshold.rs +++ b/pkgs/pkc/src/bls/sig_threshold.rs @@ -10,10 +10,9 @@ use super::error::BlsError; use super::scheme_ops::BlsScheme; use super::share_ops::BlsSigShare; use super::sig_basic::BlsSignature; +use super::BlsShareId; use crate::prelude::*; -use dash_num::Hash256; - impl BlsSignature { /// Recover a full signature from threshold signature shares via Lagrange /// interpolation in G2. @@ -24,7 +23,7 @@ impl BlsSignature { /// `InvalidShareId`/`DuplicateShareId` on bad ids, or `InvalidSignature` /// when a share fails to decode. pub fn recover(shares: &[&BlsSigShare]) -> Result { - let ids: Vec<&Hash256> = shares.iter().map(|s| s.id()).collect(); + let ids: Vec<&BlsShareId> = shares.iter().map(|s| s.id()).collect(); let sigs: Vec<&S::InnerSig> = shares.iter().map(|s| &s.signature().0).collect(); S::recover_sig_shares(&ids, &sigs).map(BlsSignature::from_inner) @@ -40,8 +39,9 @@ mod tests { use crate::prelude::*; use dash_dev::{arr_from_hex, Corpus, Value}; + use getrandom::SysRng; use hex_conservative::DisplayHex; - use rand_core::OsRng; + use rand_core::UnwrapErr; use rstest::rstest; fn assert_threshold_split_recover() { @@ -49,7 +49,7 @@ mod tests { let pk = sk.public_key(); let ids = sequential_ids(5); - let shares = sk.split(3, &ids, &mut OsRng).unwrap(); + let shares = sk.split(3, &ids, &mut UnwrapErr(SysRng)).unwrap(); assert_eq!(shares.len(), 5); // Any threshold-sized subset recovers the master signature. Comparing @@ -81,7 +81,7 @@ mod tests { fn assert_sub_threshold_does_not_verify() { let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); let pk = sk.public_key(); - let shares = sk.split(3, &sequential_ids(5), &mut OsRng).unwrap(); + let shares = sk.split(3, &sequential_ids(5), &mut UnwrapErr(SysRng)).unwrap(); let msg = S::msg_ref(&MSG_DEADBEEF); let signed: Vec> = shares.iter().map(|s| s.sign(msg)).collect(); @@ -107,7 +107,7 @@ mod tests { let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); let ids = sequential_ids(3); - let shares = sk.split(2, &ids, &mut OsRng).unwrap(); + let shares = sk.split(2, &ids, &mut UnwrapErr(SysRng)).unwrap(); let one = shares[0].sign(S::msg_ref(&MSG_DEADBEEF)); assert!(matches!( BlsSignature::::recover(&[&one]), diff --git a/pkgs/pkc/src/bls/tests.rs b/pkgs/pkc/src/bls/tests.rs index cda1bc5b..3c8d8adc 100644 --- a/pkgs/pkc/src/bls/tests.rs +++ b/pkgs/pkc/src/bls/tests.rs @@ -6,10 +6,11 @@ //! Shared test fixtures and constants. +use crate::bls::BlsShareId; use crate::prelude::*; use cfg_if::cfg_if; -use hex_conservative::hex; +use hex_conservative::{hex, FromHex}; /// BLS12-381 scalar field order r, big-endian. pub const GROUP_ORDER: [u8; 32] = hex!("73eda753299d7d483339d80809a1d80553bda402fffe5bfeffffffff00000001"); @@ -67,20 +68,22 @@ pub const fn ietf_g1_encoding(mut chia: [u8; 48]) -> [u8; 48] { chia } -/// Parse a 32-byte hash from a hex string. -pub fn hash_from_hex(s: &str) -> dash_num::Hash256 { - dash_num::Hash256::from_hex(s).unwrap() +/// Parse a participant id from a big-endian hex string. +pub fn id_from_hex(s: &str) -> BlsShareId { + let mut bytes = <[u8; 32]>::from_hex(s).unwrap(); + bytes.reverse(); + BlsShareId::from_bytes(bytes) } /// Build a participant id whose low bytes encode `i`. -pub fn make_id(i: u32) -> dash_num::Hash256 { +pub fn make_id(i: u32) -> BlsShareId { let mut bytes = [0u8; 32]; bytes[28..32].copy_from_slice(&i.to_be_bytes()); - dash_num::Hash256::from_bytes(bytes) + BlsShareId::from_bytes(bytes) } /// Build `n` sequential participant ids `1..=n`. -pub fn sequential_ids(n: usize) -> Vec { +pub fn sequential_ids(n: usize) -> Vec { (1..=n).map(|i| make_id(i as u32)).collect() } diff --git a/pkgs/pkc/src/ecdsa/public_ops.rs b/pkgs/pkc/src/ecdsa/public_ops.rs index 17c7312a..f241dec5 100644 --- a/pkgs/pkc/src/ecdsa/public_ops.rs +++ b/pkgs/pkc/src/ecdsa/public_ops.rs @@ -128,7 +128,7 @@ impl EcdsaPublicKey { /// Serialize as 33-byte compressed SEC1. pub fn to_compressed(&self) -> [u8; 33] { - let pt = self.inner.to_encoded_point(true); + let pt = self.inner.to_sec1_point(true); let mut out = [0u8; 33]; out.copy_from_slice(pt.as_bytes()); out @@ -143,7 +143,7 @@ impl EcdsaPublicKey { /// Serialize as 65-byte uncompressed SEC1. pub fn to_uncompressed(&self) -> [u8; 65] { - let pt = self.inner.to_encoded_point(false); + let pt = self.inner.to_sec1_point(false); let mut out = [0u8; 65]; out.copy_from_slice(pt.as_bytes()); out diff --git a/pkgs/pkc/src/ecdsa/secret_ops.rs b/pkgs/pkc/src/ecdsa/secret_ops.rs index 635a797a..8593e32d 100644 --- a/pkgs/pkc/src/ecdsa/secret_ops.rs +++ b/pkgs/pkc/src/ecdsa/secret_ops.rs @@ -21,8 +21,9 @@ use dash_types::{impl_stype, type_cvrt, ArrayBuf}; use hex_conservative::hex; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; use k256::elliptic_curve::ops::Neg; -use k256::{elliptic_curve::sec1::ToEncodedPoint, AffinePoint}; -use rand_core::CryptoRngCore; +use k256::elliptic_curve::Generate; +use k256::{elliptic_curve::sec1::ToSec1Point, AffinePoint}; +use rand_core::CryptoRng; use zeroize::{Zeroize, Zeroizing}; use core::fmt; @@ -124,9 +125,9 @@ impl BaseCodec for EcdsaSecretKey { /// and zeroize or drop it themselves once done. fn encode(&self, buf: &mut impl EncodeBuf) { let scalar = self.to_bytes(); - let public = self.inner.verifying_key().to_encoded_point(self.compressed); + let public = self.inner.verifying_key().to_sec1_point(self.compressed); let public = public.as_bytes(); - let generator = AffinePoint::GENERATOR.to_encoded_point(self.compressed); + let generator = AffinePoint::GENERATOR.to_sec1_point(self.compressed); let generator = generator.as_bytes(); let point_len = public.len(); let params_len = point_len + 97; @@ -182,9 +183,9 @@ impl EcdsaSecretKey { } /// Generate a new random secret key. - pub fn generate(rng: &mut impl CryptoRngCore, compressed: Compression) -> Self { + pub fn generate(rng: &mut impl CryptoRng, compressed: Compression) -> Self { Self { - inner: SigningKey::random(rng), + inner: SigningKey::generate_from_rng(rng), compressed: compressed.is_compressed(), } } diff --git a/pkgs/pkc/src/ecdsa/sig_ops.rs b/pkgs/pkc/src/ecdsa/sig_ops.rs index 6889ef1c..f4e44560 100644 --- a/pkgs/pkc/src/ecdsa/sig_ops.rs +++ b/pkgs/pkc/src/ecdsa/sig_ops.rs @@ -14,6 +14,7 @@ use dash_num::Hash256; use dash_types::type_id::{TypeId, Unencodable}; use dash_types::{dlgt_codec, type_cvrt}; use k256::ecdsa::{DerSignature, Signature}; +use k256::elliptic_curve::scalar::IsHigh; use core::hash::{Hash, Hasher}; @@ -66,13 +67,14 @@ impl EcdsaSignature { /// Whether the S component is in the lower half of the curve order. pub fn is_low_s(&self) -> bool { - self.0.normalize_s().is_none() + !bool::from(self.0.s().is_high()) } /// Return a signature with the S value normalised to the lower half of the /// curve order. Returns `None` if already normalised. pub fn normalize_s(&self) -> Option { - self.0.normalize_s().map(Self) + let normalized = self.0.normalize_s(); + (normalized != self.0).then_some(Self(normalized)) } /// Serialize as 64-byte compact format (r || s). diff --git a/pkgs/types/src/entity.rs b/pkgs/types/src/entity.rs index b98b7952..c91741b9 100644 --- a/pkgs/types/src/entity.rs +++ b/pkgs/types/src/entity.rs @@ -219,11 +219,14 @@ macro_rules! impl_bytes { /// `Hash`, `is_null`, `AsRef<[u8]>`, `AsRef<[u8; N]>`, `From for /// [u8; N]`, a hex `Debug`/`Display`, and the hex `serde` pair. /// +/// A trailing `rev` renders the hex in reverse storage order, the default `fwd` +/// renders storage order. +/// /// For a newtype holding secrets use [`derive_sbytes!`](crate::derive_sbytes), /// which withholds everything that would read or copy out the plaintext. #[macro_export] macro_rules! derive_bytes { - (@parse [$($g:tt)*] $ty:ty, $n:expr) => { + (@parse [$($g:tt)*] $ty:ty, $n:expr, $rev:expr) => { impl<$($g)*> ::core::clone::Clone for $ty { fn clone(&self) -> Self { *self } } @@ -284,9 +287,20 @@ macro_rules! derive_bytes { } } + $crate::derive_bytes!(@hex [$($g)*] $ty, $n, $rev); + }; + (@order [$($g:tt)*] $ty:ty, $n:expr, fwd) => { + $crate::derive_bytes!(@parse [$($g)*] $ty, $n, false); + }; + (@order [$($g:tt)*] $ty:ty, $n:expr, rev) => { + $crate::derive_bytes!(@parse [$($g)*] $ty, $n, true); + }; + (@hex [$($g:tt)*] $ty:ty, $n:expr, $rev:expr) => { impl<$($g)*> ::core::fmt::Display for $ty { fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - for byte in self.as_bytes() { + let bytes = self.as_bytes(); + for i in 0..$n { + let byte = if $rev { bytes[$n - 1 - i] } else { bytes[i] }; ::core::write!(f, "{byte:02x}")?; } ::core::result::Result::Ok(()) @@ -299,8 +313,7 @@ macro_rules! derive_bytes { where Z: $crate::__private::serde::Serializer, { - use $crate::__private::hex_conservative::DisplayHex as _; - serializer.serialize_str(&self.as_bytes().to_lower_hex_string()) + serializer.serialize_str(&::alloc::format!("{self}")) } } @@ -311,18 +324,27 @@ macro_rules! derive_bytes { { use $crate::__private::serde::de::Error as _; let s = <::alloc::string::String as $crate::__private::serde::Deserialize>::deserialize(deserializer)?; - <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) - .map(Self::from_bytes) - .map_err(D::Error::custom) + let mut bytes = <[u8; $n] as $crate::__private::hex_conservative::FromHex>::from_hex(&s) + .map_err(D::Error::custom)?; + if $rev { + bytes.reverse(); + } + ::core::result::Result::Ok(Self::from_bytes(bytes)) } } } }; - (for[$($generic:tt)*] $($args:tt)*) => { - $crate::derive_bytes!(@parse [$($generic)*] $($args)*); + (for[$($generic:tt)*] $ty:ty, $n:expr, $order:tt) => { + $crate::derive_bytes!(@order [$($generic)*] $ty, $n, $order); }; - ($($args:tt)*) => { - $crate::derive_bytes!(@parse [] $($args)*); + (for[$($generic:tt)*] $ty:ty, $n:expr) => { + $crate::derive_bytes!(@order [$($generic)*] $ty, $n, fwd); + }; + ($ty:ty, $n:expr, $order:tt) => { + $crate::derive_bytes!(@order [] $ty, $n, $order); + }; + ($ty:ty, $n:expr) => { + $crate::derive_bytes!(@order [] $ty, $n, fwd); }; }