diff --git a/maint/codeql/rust/lib/policy.qll b/maint/codeql/rust/lib/policy.qll index adb4bf26..fc092d7f 100644 --- a/maint/codeql/rust/lib/policy.qll +++ b/maint/codeql/rust/lib/policy.qll @@ -56,6 +56,9 @@ predicate isSecretType(TypeItem t) { // 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", + // A share *of a public key* is the point the quorum publishes for a participant, and what it + // recovers to is the master key, which is also public. + "BlsPkShare", // 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", diff --git a/pkgs/pkc/bench/bls.rs b/pkgs/pkc/bench/bls.rs index 2839ac8a..bc4295c8 100644 --- a/pkgs/pkc/bench/bls.rs +++ b/pkgs/pkc/bench/bls.rs @@ -15,7 +15,7 @@ use rand_core::UnwrapErr; /// Single signature creation. #[divan::bench(types = [BlsScChia, BlsScIetf])] fn sign(bencher: Bencher) { - let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap(); bencher .counter(ItemsCount::new(1u32)) .bench(|| sk.sign(S::msg_ref(&test_msg(42)))); @@ -24,20 +24,20 @@ fn sign(bencher: Bencher) { /// Single signature verification. #[divan::bench(types = [BlsScChia, BlsScIetf])] fn verify(bencher: Bencher) { - let sk = BlsSecretKey::::generate(&test_ikm(2)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(2)).unwrap(); let msg = test_msg(99); let sig = sk.sign(S::msg_ref(&msg)); let pk = sk.public_key(); bencher .counter(ItemsCount::new(1u32)) - .bench(|| sig.verify(S::msg_ref(&msg), &pk)); + .bench(|| pk.verify(S::msg_ref(&msg), &sig)); } /// Public key aggregation at various quorum sizes. #[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 5, 25, 50, 100])] fn aggregate_pk_n(bencher: Bencher, n: usize) { let pks: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap().public_key()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap().public_key()) .collect(); let pk_refs: Vec<_> = pks.iter().collect(); bencher @@ -49,7 +49,7 @@ fn aggregate_pk_n(bencher: Bencher, n: usize) { #[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 10, 100])] fn aggregate_sig_n(bencher: Bencher, n: usize) { let keys: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap()) .collect(); let sigs: Vec<_> = keys .iter() @@ -66,7 +66,7 @@ fn aggregate_sig_n(bencher: Bencher, n: usize) { #[divan::bench(types = [BlsScChia, BlsScIetf], args = [100, 1000])] fn verify_n_individual(bencher: Bencher, n: usize) { let keys: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap()) .collect(); let msgs: Vec<[u8; 32]> = (0..n).map(test_msg).collect(); let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); @@ -78,7 +78,7 @@ fn verify_n_individual(bencher: Bencher, n: usize) { bencher.counter(ItemsCount::new(n)).bench(|| { for i in 0..n { - let _ = sigs[i].verify(S::msg_ref(&msgs[i]), &pks[i]); + let _ = pks[i].verify(S::msg_ref(&msgs[i]), &sigs[i]); } }); } @@ -87,7 +87,7 @@ fn verify_n_individual(bencher: Bencher, n: usize) { #[divan::bench(types = [BlsScChia, BlsScIetf], args = [10, 100, 1000])] fn fast_verify_n(bencher: Bencher, n: usize) { let keys: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap()) .collect(); let msg = test_msg(42); let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); @@ -104,14 +104,14 @@ fn fast_verify_n(bencher: Bencher, n: usize) { /// Public key serialization. #[divan::bench(types = [BlsScChia, BlsScIetf])] fn ser_pk(bencher: Bencher) { - let pk = BlsSecretKey::::generate(&test_ikm(1)).unwrap().public_key(); + let pk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap().public_key(); bencher.bench(|| pk.to_bytes()); } /// Public key deserialization. #[divan::bench(types = [BlsScChia, BlsScIetf])] fn deser_pk(bencher: Bencher) { - let bytes = BlsSecretKey::::generate(&test_ikm(1)) + let bytes = BlsSecretKey::::from_ikm(&test_ikm(1)) .unwrap() .public_key() .to_bytes(); @@ -121,7 +121,7 @@ fn deser_pk(bencher: Bencher) { /// Signature serialization. #[divan::bench(types = [BlsScChia, BlsScIetf])] fn ser_sig(bencher: Bencher) { - let sig = BlsSecretKey::::generate(&test_ikm(1)) + let sig = BlsSecretKey::::from_ikm(&test_ikm(1)) .unwrap() .sign(S::msg_ref(&test_msg(0))); bencher.bench(|| sig.to_bytes()); @@ -130,7 +130,7 @@ fn ser_sig(bencher: Bencher) { /// Signature deserialization. #[divan::bench(types = [BlsScChia, BlsScIetf])] fn deser_sig(bencher: Bencher) { - let bytes = BlsSecretKey::::generate(&test_ikm(1)) + let bytes = BlsSecretKey::::from_ikm(&test_ikm(1)) .unwrap() .sign(S::msg_ref(&test_msg(0))) .to_bytes(); @@ -140,7 +140,7 @@ fn deser_sig(bencher: Bencher) { /// Threshold secret key splitting at various quorum sizes. #[divan::bench(types = [BlsScChia, BlsScIetf], args = [5, 10, 50])] fn split_threshold(bencher: Bencher, n: usize) { - let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap(); let threshold = n.div_ceil(2); let ids = sequential_ids(n); bencher @@ -151,7 +151,7 @@ fn split_threshold(bencher: Bencher, n: usize) { /// Threshold signature recovery via Lagrange interpolation. #[divan::bench(types = [BlsScChia, BlsScIetf], args = [3, 5, 10])] fn recover_threshold(bencher: Bencher, threshold: usize) { - let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap(); let ids = sequential_ids(threshold * 2); let shares = sk.split(threshold, &ids, &mut UnwrapErr(SysRng)).unwrap(); let msg = test_msg(42); @@ -159,7 +159,7 @@ fn recover_threshold(bencher: Bencher, threshold: usize) { let subset: Vec<&BlsSigShare> = sig_shares.iter().take(threshold).collect(); bencher .counter(ItemsCount::new(threshold)) - .bench(|| BlsSignature::::recover(&subset)); + .bench(|| BlsSignature::::recover_shares(&subset)); } /// Aggregate signatures over distinct messages, then verify. @@ -169,7 +169,7 @@ where S::Msg: Sync, { let keys: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap()) .collect(); let msgs: Vec<[u8; 32]> = (0..n).map(test_msg).collect(); let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); @@ -193,7 +193,7 @@ where #[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 10, 100])] fn secure_aggregate_n(bencher: Bencher, n: usize) { let keys: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap()) .collect(); let msg = test_msg(42); let pks: Vec<_> = keys.iter().map(BlsSecretKey::public_key).collect(); @@ -211,7 +211,7 @@ fn secure_aggregate_n(bencher: Bencher, n: usize) { #[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 5, 10])] fn derive_share_n(bencher: Bencher, n: usize) { let master: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap()) .collect(); let master_refs: Vec<&BlsSecretKey> = master.iter().collect(); let id = sequential_ids(1)[0]; @@ -224,7 +224,7 @@ fn derive_share_n(bencher: Bencher, n: usize) { /// Sealing one blob to one recipient, over a plaintext of `n` blocks. #[divan::bench(types = [BlsScChia, BlsScIetf], args = [1, 2, 16])] fn ies_encrypt_n(bencher: Bencher, n: usize) { - let pk = BlsSecretKey::::generate(&test_ikm(1)).unwrap().public_key(); + let pk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap().public_key(); let plaintext = vec![0x42u8; n * 16]; let mut rng = UnwrapErr(SysRng); @@ -236,7 +236,7 @@ fn ies_encrypt_n(bencher: Bencher, n: usize) { /// Opening one blob, paying for a DH exchange. #[divan::bench(types = [BlsScChia, BlsScIetf], args = [1, 2, 16])] fn ies_decrypt_n(bencher: Bencher, n: usize) { - let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap(); let plaintext = vec![0x42u8; n * 16]; let blob = sk.public_key().ies_encrypt(&plaintext, &mut UnwrapErr(SysRng)).unwrap(); @@ -247,7 +247,7 @@ fn ies_decrypt_n(bencher: Bencher, n: usize) { #[divan::bench(types = [BlsScChia, BlsScIetf], args = [2, 10, 100])] fn ies_encrypt_multi_n(bencher: Bencher, n: usize) { let pks: Vec<_> = (0..n) - .map(|i| BlsSecretKey::::generate(&test_ikm(i)).unwrap().public_key()) + .map(|i| BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap().public_key()) .collect(); let pk_refs: Vec<&BlsPublicKey> = pks.iter().collect(); let plaintexts = vec![[0x42u8; 32]; n]; @@ -266,14 +266,14 @@ mod ietf { /// Proof of possession creation. #[divan::bench] fn prove_pop(bencher: Bencher) { - let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap(); bencher.bench(|| sk.prove_possession()); } /// Proof of possession verification. #[divan::bench] fn verify_pop(bencher: Bencher) { - let sk = BlsSecretKey::::generate(&test_ikm(1)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(1)).unwrap(); let pop = sk.prove_possession(); let pk = sk.public_key(); bencher.bench(|| pk.verify_possession(&pop)); diff --git a/pkgs/pkc/corpus/bls_llmq_100.json5 b/pkgs/pkc/corpus/bls_llmq_100.json5 index d1374464..451619de 100644 --- a/pkgs/pkc/corpus/bls_llmq_100.json5 +++ b/pkgs/pkc/corpus/bls_llmq_100.json5 @@ -5,9 +5,9 @@ "t": 2, "n": 3, "member_ids": [ - "3c2e73326b4bca39433fd0059fcced12cb57fe8a30b00895f1b41b67ac94a28b", - "d876b7f4b6a1ca8ee6703ce4f30341556c0f6af047b5294a427407aa065b7564", - "e07311fbdb796b3cc482ea34969e67eb6ef88bce50441682327d26d38ef6d115" + "8ba294ac671bb4f19508b0308afe57cb12edcc9f05d03f4339ca4b6b32732e3c", + "64755b06aa0774424a29b547f06a0f6c554103f3e43c70e68ecaa1b6f4b776d8", + "15d1f68ed3267d3282164450ce8bf86eeb679e9634ea82c43c6b79dbfb1173e0" ] }, "contribute": [ @@ -15,7 +15,7 @@ "quorum_hash": "327186a034d2dac99bc5c3557fd204ffca52aa8d21cbe426fe39a09145b6dd59", "llmq_type": 100, "member_idx": 0, - "member_id": "3c2e73326b4bca39433fd0059fcced12cb57fe8a30b00895f1b41b67ac94a28b", + "member_id": "8ba294ac671bb4f19508b0308afe57cb12edcc9f05d03f4339ca4b6b32732e3c", "vvec": [ "8909e5ae37f2096a9d74f29b46153540abc1c22e0fab2727d1c13b84975acab50b9d9d2d1d9351eadd2167b9f85d83e8", "03b8fc232a21fac07928b5fb339ebc9fe908d9597a30e4b9e1b60484900975f69852617bb2e9f95613ef236d0fe75742" @@ -30,7 +30,7 @@ "quorum_hash": "327186a034d2dac99bc5c3557fd204ffca52aa8d21cbe426fe39a09145b6dd59", "llmq_type": 100, "member_idx": 1, - "member_id": "d876b7f4b6a1ca8ee6703ce4f30341556c0f6af047b5294a427407aa065b7564", + "member_id": "64755b06aa0774424a29b547f06a0f6c554103f3e43c70e68ecaa1b6f4b776d8", "vvec": [ "17f1975a973f905c6242c966dbf0d0ee7b18892850516b17ba37860d12459ef1ed3ed90ded5dfd66a21f6340b89d8bf0", "8a108caf8b51b0b79f64bdce6b4ba57b41c53caf5acb4b36d1e8c2a4d4c25442cdc43b1011fdf60ee242c53766f10e97" @@ -45,7 +45,7 @@ "quorum_hash": "327186a034d2dac99bc5c3557fd204ffca52aa8d21cbe426fe39a09145b6dd59", "llmq_type": 100, "member_idx": 2, - "member_id": "e07311fbdb796b3cc482ea34969e67eb6ef88bce50441682327d26d38ef6d115", + "member_id": "15d1f68ed3267d3282164450ce8bf86eeb679e9634ea82c43c6b79dbfb1173e0", "vvec": [ "924d93a80009fee628ef86114d48e123f88f8ce985f86b4793f3109ce27d609167a6eed4440d12241c6439673bf4d7d5", "043c0fdf9503194bbaa7baea4024a1678173c7df7cf47805a02ba8aa31ca76f972d13164c6f9036ee76009e2cc2f4214" @@ -206,9 +206,9 @@ "quorum_hash": "327186a034d2dac99bc5c3557fd204ffca52aa8d21cbe426fe39a09145b6dd59", "llmq_type": 100, "signer_ids": [ - "d876b7f4b6a1ca8ee6703ce4f30341556c0f6af047b5294a427407aa065b7564", - "e07311fbdb796b3cc482ea34969e67eb6ef88bce50441682327d26d38ef6d115", - "3c2e73326b4bca39433fd0059fcced12cb57fe8a30b00895f1b41b67ac94a28b" + "64755b06aa0774424a29b547f06a0f6c554103f3e43c70e68ecaa1b6f4b776d8", + "15d1f68ed3267d3282164450ce8bf86eeb679e9634ea82c43c6b79dbfb1173e0", + "8ba294ac671bb4f19508b0308afe57cb12edcc9f05d03f4339ca4b6b32732e3c" ], "threshold_sigs": [ "891c7dd2352567408501984716e07ea5d2180edcf5f692a0d602a7f67fd1f8e42939106e0b1fa74441c1cd742b914071125931c2cfad37986c9c2f52a0deea3a66573bcf4f683f1185f3d49b233d0899452fabd976c068d96fa2e985af997fcb", @@ -235,9 +235,9 @@ "t": 2, "n": 3, "member_ids": [ - "e07311fbdb796b3cc482ea34969e67eb6ef88bce50441682327d26d38ef6d115", - "d876b7f4b6a1ca8ee6703ce4f30341556c0f6af047b5294a427407aa065b7564", - "3c2e73326b4bca39433fd0059fcced12cb57fe8a30b00895f1b41b67ac94a28b" + "15d1f68ed3267d3282164450ce8bf86eeb679e9634ea82c43c6b79dbfb1173e0", + "64755b06aa0774424a29b547f06a0f6c554103f3e43c70e68ecaa1b6f4b776d8", + "8ba294ac671bb4f19508b0308afe57cb12edcc9f05d03f4339ca4b6b32732e3c" ] }, "contribute": [ @@ -245,7 +245,7 @@ "quorum_hash": "255258ad1a339cd05cbecb5517eac0e2511f12ead05b24390d4bc4db74895b8e", "llmq_type": 100, "member_idx": 0, - "member_id": "e07311fbdb796b3cc482ea34969e67eb6ef88bce50441682327d26d38ef6d115", + "member_id": "15d1f68ed3267d3282164450ce8bf86eeb679e9634ea82c43c6b79dbfb1173e0", "vvec": [ "b87aa119acd3fab719fb63a55e820373a66dd09b9947cd6c267a1c291b3cd2bbc2c96f55e01172fe7bdad0e90ff1d052", "a0402a8d9446920e75db62509eb22ec5292d38118a734e50a52c311eb6cd5ff44ca40b8dfd703ef4f762176b2010eb72" @@ -260,7 +260,7 @@ "quorum_hash": "255258ad1a339cd05cbecb5517eac0e2511f12ead05b24390d4bc4db74895b8e", "llmq_type": 100, "member_idx": 1, - "member_id": "d876b7f4b6a1ca8ee6703ce4f30341556c0f6af047b5294a427407aa065b7564", + "member_id": "64755b06aa0774424a29b547f06a0f6c554103f3e43c70e68ecaa1b6f4b776d8", "vvec": [ "88f334f0b6a8041dddec22e2394e1285dbf97f35a51db822b767317b40276a3a754ad07ddd2e42e9f91bf1d86e9b4981", "878cb8a5571a7e3b3005e5f08dbdcc21c1cff31f6c5b2404514e94e3b618cda90cff03cfd58e1827e2a67c6632c62784" @@ -275,7 +275,7 @@ "quorum_hash": "255258ad1a339cd05cbecb5517eac0e2511f12ead05b24390d4bc4db74895b8e", "llmq_type": 100, "member_idx": 2, - "member_id": "3c2e73326b4bca39433fd0059fcced12cb57fe8a30b00895f1b41b67ac94a28b", + "member_id": "8ba294ac671bb4f19508b0308afe57cb12edcc9f05d03f4339ca4b6b32732e3c", "vvec": [ "9151e824541fe71097c5264314d23e94246a17cc00c8aae6a82f8497da35a86a30b43adb7a926d301972dd95f70835f9", "828caa3890dcce53d7e5b6cb81de2d113c7e6a39d20a7a9860aaac43234fd90347cbd3b649f4d2e6d9d7a35feed0af0b" @@ -436,9 +436,9 @@ "quorum_hash": "255258ad1a339cd05cbecb5517eac0e2511f12ead05b24390d4bc4db74895b8e", "llmq_type": 100, "signer_ids": [ - "d876b7f4b6a1ca8ee6703ce4f30341556c0f6af047b5294a427407aa065b7564", - "e07311fbdb796b3cc482ea34969e67eb6ef88bce50441682327d26d38ef6d115", - "3c2e73326b4bca39433fd0059fcced12cb57fe8a30b00895f1b41b67ac94a28b" + "64755b06aa0774424a29b547f06a0f6c554103f3e43c70e68ecaa1b6f4b776d8", + "15d1f68ed3267d3282164450ce8bf86eeb679e9634ea82c43c6b79dbfb1173e0", + "8ba294ac671bb4f19508b0308afe57cb12edcc9f05d03f4339ca4b6b32732e3c" ], "threshold_sigs": [ "9672626d610163865918c1c94f40b63f39041d2236234752043c25d99fa30a0b9ba22c5087bc5d284f8a400196825cba17889af1a1012da65a0ded0e5a180e3195f9857328fe2cf82193cc014557f011d24d02dcc77e7e2e0361ca6a39e2d8fc", diff --git a/pkgs/pkc/src/bls/blst_ffi.rs b/pkgs/pkc/src/bls/blst_ffi.rs index 0fdf831f..c05e0b67 100644 --- a/pkgs/pkc/src/bls/blst_ffi.rs +++ b/pkgs/pkc/src/bls/blst_ffi.rs @@ -416,6 +416,21 @@ impl G1Affine { } Ok(Self(aff)) } + + /// Deserialize a 96-byte uncompressed G1 point. + /// + /// # Errors + /// + /// Returns the blst error code when the bytes do not encode a point on the + /// curve. + pub(crate) fn deserialize(bytes: &[u8; 96]) -> Result { + let mut aff = blst_p1_affine::default(); + let rc = unsafe { blst_p1_deserialize(&mut aff, bytes.as_ptr()) }; + if rc != BLST_ERROR::BLST_SUCCESS { + return Err(rc); + } + Ok(Self(aff)) + } } impl G2 { @@ -527,4 +542,19 @@ impl G2Affine { } Ok(Self(aff)) } + + /// Deserialize a 192-byte uncompressed G2 point. + /// + /// # Errors + /// + /// Returns the blst error code when the bytes do not encode a point on the + /// curve. + pub(crate) fn deserialize(bytes: &[u8; 192]) -> Result { + let mut aff = blst_p2_affine::default(); + let rc = unsafe { blst_p2_deserialize(&mut aff, bytes.as_ptr()) }; + if rc != BLST_ERROR::BLST_SUCCESS { + return Err(rc); + } + Ok(Self(aff)) + } } diff --git a/pkgs/pkc/src/bls/error.rs b/pkgs/pkc/src/bls/error.rs index d6b7e561..8d50159f 100644 --- a/pkgs/pkc/src/bls/error.rs +++ b/pkgs/pkc/src/bls/error.rs @@ -23,6 +23,8 @@ pub enum BlsError { IndexOutOfRange, /// recipient index above the supported maximum IndexTooLarge, + /// fewer than two master keys to evaluate a share + InsufficientCoefficients, /// not enough shares to recover InsufficientShares, /// ciphertext is empty or not a whole number of cipher blocks @@ -37,16 +39,16 @@ pub enum BlsError { InvalidPublicKey, /// secret key bytes are not a valid scalar InvalidSecretKey, - /// share id reduces to zero in the scalar field - InvalidShareId, /// signature bytes are not a valid G2 point InvalidSignature, - /// verification vector needs at least 2 elements - InvalidVerificationVector, - /// threshold is below 2 or exceeds the number of ids - ThresholdTooLarge, + /// threshold is below 2, exceeds id count or no ids supplied + InvalidThreshold, + /// tweak is not below the group order, or the result is the identity + InvalidTweak, /// signature verification failed VerifyFailed, + /// input reduces to zero in the scalar field + ZeroScalar, } impl fmt::Display for BlsError { @@ -58,6 +60,7 @@ impl fmt::Display for BlsError { Self::EmptyAggregation => write!(f, "no items provided for aggregation"), Self::IndexOutOfRange => write!(f, "recipient index past the end of the message"), Self::IndexTooLarge => write!(f, "recipient index above the supported maximum"), + Self::InsufficientCoefficients => write!(f, "fewer than two master keys to evaluate a share"), Self::InsufficientShares => write!(f, "not enough shares to recover"), Self::InvalidCiphertextLength => write!(f, "ciphertext is empty or not a whole number of cipher blocks"), Self::InvalidIvSeed => write!(f, "initialisation vector seed is all zeroes"), @@ -65,11 +68,11 @@ impl fmt::Display for BlsError { Self::InvalidPlaintextLength => write!(f, "plaintext is empty or not a whole number of cipher blocks"), Self::InvalidPublicKey => write!(f, "invalid public key bytes"), Self::InvalidSecretKey => write!(f, "invalid secret key bytes"), - Self::InvalidShareId => write!(f, "share id reduces to zero in the scalar field"), Self::InvalidSignature => write!(f, "invalid signature bytes"), - Self::InvalidVerificationVector => write!(f, "verification vector needs at least 2 elements"), - Self::ThresholdTooLarge => write!(f, "threshold is below 2 or exceeds the number of ids"), + Self::InvalidThreshold => write!(f, "threshold is below 2, exceeds id count or no ids supplied"), + Self::InvalidTweak => write!(f, "tweak is not below the group order, or the result is the identity"), Self::VerifyFailed => write!(f, "signature verification failed"), + Self::ZeroScalar => write!(f, "input reduces to zero in the scalar field"), } } } diff --git a/pkgs/pkc/src/bls/ies_bytes.rs b/pkgs/pkc/src/bls/ies_bytes.rs index 79540a4f..320365ae 100644 --- a/pkgs/pkc/src/bls/ies_bytes.rs +++ b/pkgs/pkc/src/bls/ies_bytes.rs @@ -456,7 +456,7 @@ mod tests { fn assert_codec_vectors(scheme: &str) { let corpus = Corpus::open(env!("CARGO_MANIFEST_DIR"), "bls_ies").scope(scheme); - let eph_pk = BlsSecretKey::::generate(&RSEED[1]).unwrap().public_key().to_bytes(); + let eph_pk = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap().public_key().to_bytes(); for v in corpus.vectors::("blob") { let image = vec_from_hex(&v.image); diff --git a/pkgs/pkc/src/bls/ies_ops.rs b/pkgs/pkc/src/bls/ies_ops.rs index 5143f53a..cb5b3df1 100644 --- a/pkgs/pkc/src/bls/ies_ops.rs +++ b/pkgs/pkc/src/bls/ies_ops.rs @@ -70,7 +70,7 @@ fn iv_at_index(iv_seed: &[u8; IV_SEED_LEN], index: usize) -> Result<[u8; AES_BLO fn ephemeral(rng: &mut impl CryptoRng) -> Result<(BlsSecretKey, [u8; IV_SEED_LEN]), BlsError> { let mut ikm = Zeroizing::new([0u8; 32]); rng.fill_bytes(ikm.as_mut()); - let eph_sk = BlsSecretKey::::generate(ikm.as_ref())?; + let eph_sk = BlsSecretKey::::from_ikm(ikm.as_ref())?; let mut iv_seed = [0u8; IV_SEED_LEN]; rng.fill_bytes(&mut iv_seed); @@ -505,7 +505,7 @@ mod tests { } fn make_sk(seed: usize) -> BlsSecretKey { - BlsSecretKey::generate(&RSEED[seed]).unwrap() + BlsSecretKey::from_ikm(&RSEED[seed]).unwrap() } /// The shared secret and the IV are the two inputs the ciphertext depends diff --git a/pkgs/pkc/src/bls/mod.rs b/pkgs/pkc/src/bls/mod.rs index b2a859b9..ede00e8e 100644 --- a/pkgs/pkc/src/bls/mod.rs +++ b/pkgs/pkc/src/bls/mod.rs @@ -10,6 +10,7 @@ mod dh_bytes; mod error; mod ies_bytes; mod public_bytes; +mod public_hash; mod schemes; mod secret_bytes; mod share_id; @@ -20,6 +21,7 @@ pub use dh_bytes::{BlsDhBytes, BLS_DH_LEN}; pub use error::BlsError; pub use ies_bytes::{BlsIesBlobBytes, BlsIesMultiBytes, IV_SEED_LEN, MAX_IES_RECIPIENTS}; pub use public_bytes::{BlsPkBytes, BLS_PK_LEN}; +pub use public_hash::{BlsPkHash, BLS_PK_HASH_LEN}; pub use schemes::{BlsScChia, BlsScIetf, BlsSchemeId}; pub use secret_bytes::{BlsSkBytes, BLS_SK_LEN}; pub use share_id::{BlsShareId, BLS_ID_LEN}; @@ -57,7 +59,7 @@ cfg_if::cfg_if! { pub use scalar::Fr; pub use scheme_ops::BlsScheme; pub use secret_ops::BlsSecretKey; - pub use share_ops::{BlsSigShare, BlsSkShare}; + pub use share_ops::{BlsPkShare, BlsSigShare, BlsSkShare}; pub use sig_basic::BlsSignature; } } diff --git a/pkgs/pkc/src/bls/public_bytes.rs b/pkgs/pkc/src/bls/public_bytes.rs index 7b323e1f..26f2ad5a 100644 --- a/pkgs/pkc/src/bls/public_bytes.rs +++ b/pkgs/pkc/src/bls/public_bytes.rs @@ -6,13 +6,12 @@ //! BLS public key byte bag. +use super::public_hash::BlsPkHash; use crate::bls::BlsSchemeId; use bitcoin_hashes::sha256d::Hash as Sha256d; -use dash_num::Hash256; use dash_types::make_bytes; use dash_types::Hashable; -use dash_types::Numeric; /// Raw BLS public key length (G1 compressed). pub const BLS_PK_LEN: usize = 48; @@ -23,9 +22,9 @@ make_bytes! { } impl Hashable for BlsPkBytes { - type Hash = Hash256; + type Hash = BlsPkHash; fn hash(&self) -> Self::Hash { - Hash256::from_lendian(Sha256d::hash(self.as_bytes()).to_byte_array()) + BlsPkHash::from_bytes(Sha256d::hash(self.as_bytes()).to_byte_array()) } } diff --git a/pkgs/pkc/src/bls/public_hash.rs b/pkgs/pkc/src/bls/public_hash.rs new file mode 100644 index 00000000..edaf4785 --- /dev/null +++ b/pkgs/pkc/src/bls/public_hash.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 +// + +//! Hashed representation of a BLS public key. + +use crate::bls::BlsSchemeId; + +use dash_types::make_bytes; + +/// Length of a public key hash. +pub const BLS_PK_HASH_LEN: usize = 32; + +make_bytes! { // nosemgrep: bytes-rev-means-hash + /// Scheme-tagged 32-byte public key hash. + for[S: BlsSchemeId] BlsPkHash, BLS_PK_HASH_LEN, rev +} + +#[cfg(all(test, feature = "bls", feature = "codec"))] +#[expect(clippy::unwrap_used, reason = "test code")] +mod tests { + use crate::bls::tests::RSEED; + use crate::bls::{BlsScChia, BlsScIetf, BlsSecretKey}; + + use dash_types::Hashable; + use rstest::rstest; + + #[rstest] + fn schemes_diverge_in_pk_hash() { + let chia = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap().public_key(); + let ietf = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap().public_key(); + + assert_ne!(chia.to_bytes(), ietf.to_bytes()); + assert_ne!(Hashable::hash(&chia).as_bytes(), Hashable::hash(&ietf).as_bytes()); + } +} diff --git a/pkgs/pkc/src/bls/public_ops.rs b/pkgs/pkc/src/bls/public_ops.rs index ead44184..6b03e0a7 100644 --- a/pkgs/pkc/src/bls/public_ops.rs +++ b/pkgs/pkc/src/bls/public_ops.rs @@ -8,18 +8,18 @@ use super::error::BlsError; use super::group::G1; +#[cfg(feature = "codec")] +use super::public_hash::BlsPkHash; use super::scheme_ops::BlsScheme; +use super::sig_basic::BlsSignature; use super::BlsPkBytes; #[cfg(feature = "codec")] use super::BLS_PK_LEN; +use super::{BlsScIetf, BlsSigId}; use crate::prelude::*; #[cfg(feature = "codec")] -use dash_num::Hash256; -#[cfg(feature = "codec")] -use dash_types::dlgt_codec; -#[cfg(feature = "codec")] -use dash_types::type_id::TypeId; +use dash_types::{dlgt_codec, type_id::TypeId}; use dash_types::{qtypestr, type_cvrt}; use hex_conservative::DisplayHex; @@ -35,7 +35,7 @@ use core::hash::{Hash, Hasher}; pub struct BlsPublicKey(pub(crate) S::InnerPk); #[cfg(feature = "codec")] -dlgt_codec!(for[S: BlsScheme] BlsPublicKey => BlsPkBytes, Hash256, BlsError, BLS_PK_LEN); +dlgt_codec!(for[S: BlsScheme] BlsPublicKey => BlsPkBytes, BlsPkHash, BlsError, BLS_PK_LEN); impl BlsPublicKey { /// Deserialize from 48 bytes. @@ -64,6 +64,28 @@ impl BlsPublicKey { T::g1_to_pk(S::pk_to_g1(&self.0)?).map(BlsPublicKey::from_inner) } + /// Add `tweak * G` to the point. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when `tweak` is not below the group order or the + /// sum is the point at infinity, and `InvalidPublicKey` when this key does + /// not decode to a point. + pub fn add_tweak(&self, tweak: &[u8; 32]) -> Result { + S::add_tweak_pk(&self.0, tweak).map(Self::from_inner) + } + + /// Multiply the point by `tweak`. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when `tweak` is not below the group order or the + /// product is the point at infinity, and `InvalidPublicKey` when this key + /// does not decode to a point. + pub fn mul_tweak(&self, tweak: &[u8; 32]) -> Result { + S::mul_tweak_pk(&self.0, tweak).map(Self::from_inner) + } + /// Aggregate multiple public keys into one. /// /// # Errors @@ -91,11 +113,31 @@ impl BlsPublicKey { S::secure_aggregate_pk(&inner_refs).map(Self::from_inner) } + /// Verify `sig` over a message of the scheme's message type. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the pairing check does not hold. + pub fn verify(&self, msg: &S::Msg, sig: &BlsSignature) -> Result<(), BlsError> { + S::verify(sig.as_inner(), msg, &self.0) + } + pub(crate) fn from_inner(inner: S::InnerPk) -> Self { Self(inner) } } +impl BlsPublicKey { + /// Verify `sig` under the domain separation tag selected by `scheme`. + /// + /// # Errors + /// + /// Returns `VerifyFailed` when the pairing check does not hold. + pub fn verify_with(&self, msg: &[u8], sig: &BlsSignature, scheme: BlsSigId) -> Result<(), BlsError> { + BlsScIetf::verify_with(sig.as_inner(), msg, &self.0, scheme) + } +} + impl Clone for BlsPublicKey { fn clone(&self) -> Self { Self(self.0.clone()) @@ -195,8 +237,8 @@ mod tests { } fn assert_dh_roundtrip() { - let sk_a = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let sk_b = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk_a = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk_b = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let shared_ab = sk_a.dh_exchange(&sk_b.public_key()).unwrap(); let shared_ba = sk_b.dh_exchange(&sk_a.public_key()).unwrap(); @@ -213,7 +255,7 @@ mod tests { /// In the Chia scheme, DH weighs whatever the decoder passed, which leaks /// the scalar mod the cofactor's small factors. IETF rejects this. fn assert_off_subgroup_peer_policy(encoded: &[u8; 48], reaches_dh: bool) { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); match BlsPublicKey::::from_bytes(encoded) { Ok(peer) => { @@ -239,7 +281,7 @@ mod tests { /// Conversion re-encodes one point, so a round trip returns the original and /// the same-scheme case is a copy. fn assert_scheme_conversion_round_trips() { - let pk = BlsSecretKey::::generate(&RSEED[0]).unwrap().public_key(); + let pk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap().public_key(); let there = pk.to_scheme::().unwrap(); assert_eq!(there.to_scheme::().unwrap().to_bytes(), pk.to_bytes()); @@ -282,7 +324,7 @@ mod tests { } fn assert_pk_roundtrip() { - let pk = BlsSecretKey::::generate(&RSEED[0]).unwrap().public_key(); + let pk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap().public_key(); let bytes = pk.to_bytes(); assert_eq!(BlsPublicKey::::from_bytes(&bytes).unwrap().to_bytes(), bytes); } @@ -366,7 +408,7 @@ mod tests { /// round-trips back to its canonical form. #[rstest] fn chia_masks_stray_public_key_bits() { - let clean = BlsSecretKey::::generate(&RSEED[0]) + let clean = BlsSecretKey::::from_ikm(&RSEED[0]) .unwrap() .public_key() .to_bytes(); @@ -459,7 +501,7 @@ mod tests { 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!(agg_pk.verify(S::msg_ref(&msg), &sig).is_ok()); assert!(sig.secure_verify_aggregates(S::msg_ref(&msg), &refs).is_ok()); } } @@ -477,7 +519,7 @@ mod tests { 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()) + .map(|ikm| BlsSecretKey::::from_ikm(*ikm).unwrap().public_key()) .collect(); let straight = BlsPublicKey::::secure_aggregate(&[&pks[0], &pks[1], &pks[2]]).unwrap(); @@ -499,7 +541,7 @@ mod tests { /// 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 sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let pk = sk.public_key(); let alone = BlsPublicKey::::secure_aggregate(&[&pk]).unwrap(); @@ -507,7 +549,7 @@ mod tests { 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()); + assert!(alone.verify(S::msg_ref(&MSG_8BADFOOD), &weighted).is_ok()); let none: [&BlsPublicKey; 0] = []; assert_eq!( diff --git a/pkgs/pkc/src/bls/scalar.rs b/pkgs/pkc/src/bls/scalar.rs index 966bc6ca..d18b89fd 100644 --- a/pkgs/pkc/src/bls/scalar.rs +++ b/pkgs/pkc/src/bls/scalar.rs @@ -8,7 +8,6 @@ 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; @@ -39,29 +38,23 @@ const WIDE_LEN: usize = 64; pub struct Fr(pub(super) blst_fr); impl Fr { - /// Reduces a threshold participant id into the field (big-integer). + /// Reduces a big-endian integer into the field. /// /// [`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_lendian_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 { + /// Returns `ZeroScalar` when the input reduces to zero. + pub fn from_bendian_reduce(bytes: &[u8; 32]) -> Result { let mut scalar = super::blst_ffi::scalar_from_bendian(bytes); let reduced = Self::from(&scalar); scalar.b.zeroize(); - reduced + + if bool::from(reduced.is_zero()) { + return Err(BlsError::ZeroScalar); + } + Ok(reduced) } /// Emits the canonical big-endian encoding. @@ -353,7 +346,6 @@ mod tests { use crate::bls::{BlsScChia, BlsScIetf, BlsScheme, BlsSecretKey}; use crate::prelude::*; - use dash_types::Numeric; use getrandom::SysRng; use rand_core::UnwrapErr; use rstest::rstest; @@ -416,8 +408,8 @@ mod tests { /// 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)); + assert_eq!(Fr::from_bendian_reduce(make_id(1).as_bytes()).unwrap(), Fr::ONE); + assert_eq!(Fr::from_bendian_reduce(make_id(258).as_bytes()).unwrap(), Fr::from(258)); let mut leading = [0u8; 32]; leading[0] = 1; @@ -425,7 +417,7 @@ mod tests { for _ in 0..248 { expected = expected.double(); } - assert_eq!(Fr::from_share_id(&BlsShareId::from_bendian(leading)).unwrap(), expected); + assert_eq!(Fr::from_bendian_reduce(&leading).unwrap(), expected); } /// An id is an integer rather than an encoding of one, so a value at or @@ -435,10 +427,7 @@ mod tests { 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_bendian(order_plus_one)).unwrap(), - Fr::ONE - ); + assert_eq!(Fr::from_bendian_reduce(&order_plus_one).unwrap(), Fr::ONE); let mut order_le = GROUP_ORDER; order_le.reverse(); @@ -451,10 +440,7 @@ mod tests { #[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_bendian(bytes)), - Err(BlsError::InvalidShareId) - ); + assert_eq!(Fr::from_bendian_reduce(&bytes), Err(BlsError::ZeroScalar)); } /// Read a secret key back as the field element it is: the wire form is @@ -471,14 +457,14 @@ mod tests { fn assert_share_id_matches_the_engine() { let master: Vec> = RSEED[..3] .iter() - .map(|ikm| BlsSecretKey::::generate(ikm).unwrap()) + .map(|ikm| BlsSecretKey::::from_ikm(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 x = Fr::from_bendian_reduce(id.as_bytes()).unwrap(); let evaluated = coeffs[0] + coeffs[1] * x + coeffs[2] * x * x; let share = BlsSecretKey::::derive_share(&refs, &id).unwrap(); diff --git a/pkgs/pkc/src/bls/scheme_chia.rs b/pkgs/pkc/src/bls/scheme_chia.rs index 6c73048b..bbf99073 100644 --- a/pkgs/pkc/src/bls/scheme_chia.rs +++ b/pkgs/pkc/src/bls/scheme_chia.rs @@ -12,7 +12,7 @@ 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::scheme_ops::{sealed::Sealed, BlsScheme}; use super::schemes::BlsScChia; use crate::prelude::*; @@ -24,6 +24,8 @@ fn y_c1_is_larger(y_c1: &[u8]) -> bool { y_c1.len() >= 48 && y_c1[..48] > HALF_P[..] } +impl Sealed for BlsScChia {} + impl BlsScheme for BlsScChia { type InnerSk = blst::blst_scalar; type InnerPk = G1Affine; @@ -31,8 +33,8 @@ impl BlsScheme for BlsScChia { type Msg = [u8; 32]; /// Derive via draft-03 keygen, then range-check the scalar. - fn generate(ikm: &[u8]) -> Result { - let sk = min_pk::SecretKey::key_gen_v3(ikm, &[]).map_err(|_| BlsError::InvalidSecretKey)?; + fn sk_from_ikm(ikm: &[u8]) -> Result { + let sk = min_pk::SecretKey::key_gen_v3(ikm, &[]).map_err(|_| BlsError::InvalidKeyMaterial)?; let mut bytes = sk.to_bytes(); let res = Self::sk_from_bytes(&bytes); bytes.zeroize(); @@ -343,8 +345,8 @@ mod tests { #[test] fn signing_verifies_and_rejects_mismatches() { - let sk0 = BlsScChia::generate(&RSEED[0]).unwrap(); - let sk1 = BlsScChia::generate(&RSEED[1]).unwrap(); + let sk0 = BlsScChia::sk_from_ikm(&RSEED[0]).unwrap(); + let sk1 = BlsScChia::sk_from_ikm(&RSEED[1]).unwrap(); let pk0 = BlsScChia::derive_pk(&sk0); let pk1 = BlsScChia::derive_pk(&sk1); let sig = BlsScChia::sign(&sk0, &MSG_DEADBEEF); @@ -357,7 +359,7 @@ mod tests { #[test] fn secure_verify_rejects_infinity_input_key() { - let sk = BlsScChia::generate(&RSEED[0]).unwrap(); + let sk = BlsScChia::sk_from_ikm(&RSEED[0]).unwrap(); let real_pk = BlsScChia::derive_pk(&sk); let inf_pk = G1::identity().to_affine(); // The identity key serializes to the infinity marker (bits 6-7 set). diff --git a/pkgs/pkc/src/bls/scheme_ietf.rs b/pkgs/pkc/src/bls/scheme_ietf.rs index d60f3722..f011d4ce 100644 --- a/pkgs/pkc/src/bls/scheme_ietf.rs +++ b/pkgs/pkc/src/bls/scheme_ietf.rs @@ -8,7 +8,7 @@ use super::error::BlsError; use super::group::{G1Affine, G2Affine, G1, G2}; -use super::scheme_ops::{verify_ok, BlsScheme}; +use super::scheme_ops::{sealed::Sealed, verify_ok, BlsScheme}; use super::schemes::BlsScIetf; use super::sig_id::BlsSigId; use crate::prelude::*; @@ -22,6 +22,8 @@ const DST_POP: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; /// Domain separation tag for proofs of possession. const DST_POP_PROVE: &[u8] = b"BLS_POP_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; +impl Sealed for BlsScIetf {} + impl BlsScheme for BlsScIetf { type InnerSk = SecretKey; type InnerPk = PublicKey; @@ -29,7 +31,7 @@ impl BlsScheme for BlsScIetf { type Msg = [u8]; /// Derive via draft-03 keygen. - fn generate(ikm: &[u8]) -> Result { + fn sk_from_ikm(ikm: &[u8]) -> Result { SecretKey::key_gen_v3(ikm, &[]).map_err(|_| BlsError::InvalidKeyMaterial) } @@ -69,9 +71,9 @@ impl BlsScheme for BlsScIetf { pk.compress() } - /// Decompress the blst public key into a projective G1 point. + /// Lift the blst public key into a projective G1 point. fn pk_to_g1(pk: &Self::InnerPk) -> Result { - let aff = G1Affine::uncompress(&pk.compress()).map_err(|_| BlsError::InvalidPublicKey)?; + let aff = G1Affine::deserialize(&pk.serialize()).map_err(|_| BlsError::InvalidPublicKey)?; Ok(aff.to_projective()) } @@ -103,9 +105,9 @@ impl BlsScheme for BlsScIetf { sig.compress() } - /// Decompress the blst signature into a projective G2 point. + /// Lift the blst signature into a projective G2 point. fn sig_to_g2(sig: &Self::InnerSig) -> Result { - let aff = G2Affine::uncompress(&Self::sig_to_bytes(sig)).map_err(|_| BlsError::InvalidSignature)?; + let aff = G2Affine::deserialize(&sig.serialize()).map_err(|_| BlsError::InvalidSignature)?; Ok(aff.to_projective()) } @@ -251,6 +253,41 @@ mod tests { } } + /// The lifts skip decompression, so they have to land on the point that + /// decompressing the compressed encoding yields, inclusive of identity. + /// A cancelled aggregate serializes to the infinity marker on both paths. + #[test] + fn lifts_agree_with_decompression() { + let sk = BlsScIetf::sk_from_ikm(&RSEED[0]).unwrap(); + let pk = BlsScIetf::derive_pk(&sk); + let sig = BlsScIetf::sign(&sk, &MSG_DEADBEEF); + + let decompressed = G1Affine::uncompress(&pk.compress()).unwrap().to_projective(); + assert_eq!(BlsScIetf::pk_to_g1(&pk).unwrap(), decompressed); + let decompressed = G2Affine::uncompress(&sig.compress()).unwrap().to_projective(); + assert_eq!(BlsScIetf::sig_to_g2(&sig).unwrap(), decompressed); + + let mut negated = pk.compress(); + negated[0] ^= 0x20; + let negated = PublicKey::from_bytes(&negated).unwrap(); + let cancelled = AggregatePublicKey::aggregate(&[&pk, &negated], true) + .unwrap() + .to_public_key(); + let decompressed = G1Affine::uncompress(&cancelled.compress()).unwrap().to_projective(); + assert!(decompressed.is_inf()); + assert_eq!(BlsScIetf::pk_to_g1(&cancelled).unwrap(), decompressed); + + let mut negated = sig.compress(); + negated[0] ^= 0x20; + let negated = Signature::from_bytes(&negated).unwrap(); + let cancelled = AggregateSignature::aggregate(&[&sig, &negated], true) + .unwrap() + .to_signature(); + let decompressed = G2Affine::uncompress(&cancelled.compress()).unwrap().to_projective(); + assert!(decompressed.is_inf()); + assert_eq!(BlsScIetf::sig_to_g2(&cancelled).unwrap(), decompressed); + } + #[test] fn pyecc_signature_matches() { let sk = BlsScIetf::sk_from_bytes(&hex!( @@ -281,8 +318,8 @@ mod tests { #[test] fn signing_verifies_and_rejects_mismatches() { - let sk0 = BlsScIetf::generate(&RSEED[0]).unwrap(); - let sk1 = BlsScIetf::generate(&RSEED[1]).unwrap(); + let sk0 = BlsScIetf::sk_from_ikm(&RSEED[0]).unwrap(); + let sk1 = BlsScIetf::sk_from_ikm(&RSEED[1]).unwrap(); let pk0 = BlsScIetf::derive_pk(&sk0); let pk1 = BlsScIetf::derive_pk(&sk1); let sig = BlsScIetf::sign(&sk0, &MSG_DEADBEEF); diff --git a/pkgs/pkc/src/bls/scheme_ops.rs b/pkgs/pkc/src/bls/scheme_ops.rs index 04bc5af2..ebef4dda 100644 --- a/pkgs/pkc/src/bls/scheme_ops.rs +++ b/pkgs/pkc/src/bls/scheme_ops.rs @@ -16,7 +16,7 @@ use crate::aes_cbc::{self, AES_BLOCK_LEN, AES_KEY_LEN}; use crate::prelude::*; use blst::BLST_ERROR; -use ff::Field; +use ff::{Field, PrimeField}; use sha2::{Digest, Sha256}; use zeroize::{Zeroize, Zeroizing}; @@ -37,8 +37,15 @@ pub(crate) fn verify_ok(result: BLST_ERROR) -> Result<(), BlsError> { } } +/// Keeps [`BlsScheme`] sealed to types defined in this crate. +pub(crate) mod sealed { + /// Sealing marker for [`BlsScheme`](super::BlsScheme). + pub trait Sealed {} +} + /// BLS operations tied to a specific scheme. -pub trait BlsScheme: BlsSchemeId + Sized { +#[doc(hidden)] +pub trait BlsScheme: BlsSchemeId + sealed::Sealed + Sized { /// Inner secret key representation. type InnerSk: Clone + Send + Sync; /// Inner public key representation. @@ -54,7 +61,7 @@ pub trait BlsScheme: BlsSchemeId + Sized { /// /// Returns `InvalidKeyMaterial` when `ikm` is too short, or /// `InvalidSecretKey` when the derived scalar is invalid. - fn generate(ikm: &[u8]) -> Result; + fn sk_from_ikm(ikm: &[u8]) -> Result; /// Parse a secret key from a 32-byte big-endian scalar. /// @@ -242,6 +249,68 @@ pub trait BlsScheme: BlsSchemeId + Sized { Self::g2_to_sig(difference) } + /// Add `tweak` to a secret scalar, modulo the group order. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when `tweak` is not below the group order or the + /// sum is zero, which is the tweak that is this scalar's additive inverse. + fn add_tweak_sk(sk: &Self::InnerSk, tweak: &[u8; 32]) -> Result { + let scalar = tweak_scalar(tweak)?; + let bytes = Zeroizing::new(Self::sk_to_bytes(sk)); + let mut current = Fr::from_bendian_reduce(&bytes)?; + let mut sum = current + scalar; + current.zeroize(); + + // A zero sum hands back a key whoever chose the tweak already knows. + if bool::from(sum.is_zero()) { + return Err(BlsError::InvalidTweak); + } + + // `Fr` is `Copy`, so it cannot wipe itself on the way out of scope. + let tweaked = Self::sk_from_bytes(&sum.to_bendian()); + sum.zeroize(); + tweaked.map_err(|_| BlsError::InvalidTweak) + } + + /// Add `tweak * G` to a public key's point. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when `tweak` is not below the group order or the + /// sum is the point at infinity, and `InvalidPublicKey` when the key does + /// not decode. + fn add_tweak_pk(pk: &Self::InnerPk, tweak: &[u8; 32]) -> Result { + let scalar = tweak_scalar(tweak)?; + Self::tweaked_g1_to_pk(Self::pk_to_g1(pk)? + ::mul_by_generator(&scalar)) + } + + /// Multiply a public key's point by `tweak`. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when `tweak` is not below the group order or the + /// product is the point at infinity, and `InvalidPublicKey` when the key + /// does not decode. + fn mul_tweak_pk(pk: &Self::InnerPk, tweak: &[u8; 32]) -> Result { + let scalar = tweak_scalar(tweak)?; + let blst_scalar = blst::blst_scalar::from(&scalar); + Self::tweaked_g1_to_pk(Self::pk_to_g1(pk)?.mul_scalar(&blst_scalar.b, FR_BITS)) + } + + /// Lower a tweaked point back to a public key, refusing the identity. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when the point is at infinity, which is no key; + /// the tweak cancelled the public key it was applied to. + fn tweaked_g1_to_pk(point: G1) -> Result { + if point.is_inf() { + return Err(BlsError::InvalidTweak); + } + Self::g1_to_pk(point) + } + /// Verify an aggregate signature where every signer signed `msg`. /// /// # Errors @@ -361,9 +430,9 @@ pub trait BlsScheme: BlsSchemeId + Sized { /// /// # Errors /// - /// Returns `ThresholdTooLarge` when `threshold < 2` (a 1-of-n split hands + /// Returns `InvalidThreshold` when `threshold < 2` (a 1-of-n split hands /// the master key to every participant), `ids` is empty, or `threshold > - /// ids.len()`; `InvalidShareId`/`DuplicateShareId` on bad ids; + /// ids.len()`; `ZeroScalar`/`DuplicateShareId` on bad ids; /// `InvalidSecretKey` when share generation or parsing fails. fn split_sk( sk: &Self::InnerSk, @@ -373,7 +442,7 @@ pub trait BlsScheme: BlsSchemeId + Sized { mut into_share: impl FnMut(BlsShareId, Self::InnerSk) -> S, ) -> Result, BlsError> { if threshold < 2 || ids.is_empty() || threshold > ids.len() { - return Err(BlsError::ThresholdTooLarge); + return Err(BlsError::InvalidThreshold); } // An id congruent to zero mod r would make the share equal the master @@ -397,15 +466,18 @@ pub trait BlsScheme: BlsSchemeId + Sized { /// /// # Errors /// - /// Returns `InsufficientShares` when fewer than two shares are given or when - /// `ids` and `sigs` differ in length, `InvalidShareId`/`DuplicateShareId` on - /// bad ids, or `InvalidSignature` when a share or the recovered point fails - /// to decode. + /// Returns `InsufficientShares` when fewer than two shares are given, + /// `CountMismatch` when `ids` and `sigs` differ in length, + /// `ZeroScalar`/`DuplicateShareId` on bad ids, or `InvalidSignature` + /// when a share fails to decode or the recovered point is the identity. fn recover_sig_shares(ids: &[&BlsShareId], sigs: &[&Self::InnerSig]) -> Result { + if sigs.len() < 2 { + return Err(BlsError::InsufficientShares); + } // 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() { - return Err(BlsError::InsufficientShares); + if ids.len() != sigs.len() { + return Err(BlsError::CountMismatch); } // Reduce and validate ids in the scalar field, rejecting zero-reducing @@ -417,27 +489,64 @@ pub trait BlsScheme: BlsSchemeId + Sized { .collect::, BlsError>>()?; let recovered = interpolate_g2(&reduced, &points); + if recovered.is_inf() { + return Err(BlsError::InvalidSignature); + } Self::g2_to_sig(recovered) } + /// Recover a full public key from threshold shares by interpolation. + /// + /// The G1 counterpart of [`Self::recover_sig_shares`] the same interpolation, + /// over the group public keys live in. + /// + /// # Errors + /// + /// Returns `InsufficientShares` when fewer than two shares are given, + /// `CountMismatch` when `ids` and `pks` differ in length, + /// `ZeroScalar`/`DuplicateShareId` on bad ids, or `InvalidPublicKey` + /// when a share fails to decode or the recovered point is the identity. + fn recover_pk_shares(ids: &[&BlsShareId], pks: &[&Self::InnerPk]) -> Result { + if pks.len() < 2 { + return Err(BlsError::InsufficientShares); + } + // ids and pks are paired; a length mismatch would desync interpolation + // and could index out of bounds in interpolate_g1. + if ids.len() != pks.len() { + return Err(BlsError::CountMismatch); + } + + let reduced = reduce_share_ids(ids)?; + let points = pks + .iter() + .map(|pk| Self::pk_to_g1(pk)) + .collect::, BlsError>>()?; + + let recovered = interpolate_g1(&reduced, &points); + if recovered.is_inf() { + return Err(BlsError::InvalidPublicKey); + } + Self::g1_to_pk(recovered) + } + /// Derive a public key share from the master verification vector. /// /// # Errors /// - /// Returns `InvalidVerificationVector` when fewer than two keys are - /// given, `InvalidShareId` on a zero-reducing id, or `InvalidPublicKey` + /// Returns `InsufficientCoefficients` when fewer than two keys are + /// given, `ZeroScalar` 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: &BlsShareId) -> Result { // Evaluating the verification-vector polynomial needs >= 2 coefficients. if master_pks.len() < 2 { - return Err(BlsError::InvalidVerificationVector); + return Err(BlsError::InsufficientCoefficients); } let coeffs_g1 = master_pks .iter() .map(|pk| Self::pk_to_g1(pk)) .collect::, BlsError>>()?; - let x = Fr::from_share_id(id)?; + let x = Fr::from_bendian_reduce(id.as_bytes())?; let result = eval_poly_g1(&coeffs_g1, &x); Self::g1_to_pk(result) @@ -452,12 +561,12 @@ pub trait BlsScheme: BlsSchemeId + Sized { /// /// # Errors /// - /// Returns `InvalidVerificationVector` when fewer than two keys are given, - /// `InvalidShareId` on a zero-reducing id, or `InvalidSecretKey` when the + /// Returns `InsufficientCoefficients` when fewer than two keys are given, + /// `ZeroScalar` on a zero-reducing id, or `InvalidSecretKey` when the /// result is not a valid scalar. fn derive_sk_share(master_sks: &[&Self::InnerSk], id: &BlsShareId) -> Result { if master_sks.len() < 2 { - return Err(BlsError::InvalidVerificationVector); + return Err(BlsError::InsufficientCoefficients); } let mut coeffs = Zeroizing::new(Vec::with_capacity(master_sks.len())); @@ -468,7 +577,7 @@ pub trait BlsScheme: BlsSchemeId + Sized { scalar.b.zeroize(); } - let x = Fr::from_share_id(id)?; + let x = Fr::from_bendian_reduce(id.as_bytes())?; let mut y = poly_eval(&coeffs, &x); let mut y_scalar = blst::blst_scalar::from(&y); @@ -621,6 +730,26 @@ fn interpolate_g2(ids: &[Fr], points: &[G2]) -> G2 { result } +/// Recover a G1 point from shares via Lagrange interpolation at x=0. +/// +/// The G1 counterpart of [`interpolate_g2`], over the group public keys +/// live in. Same coefficients, different group. +fn interpolate_g1(ids: &[Fr], points: &[G1]) -> G1 { + let n = ids.len(); + + // Compute Lagrange coefficients at x=0: + // L_i = prod_{j!=i} id_j / (id_j - id_i) + let coeffs = compute_lagrange_coeffs(ids); + + let mut result = G1::identity(); + for i in 0..n { + // Convert Fr coefficient to scalar for point multiplication. + let scalar = blst::blst_scalar::from(&coeffs[i]); + result += points[i].mul_scalar(&scalar.b, FR_BITS); + } + result +} + /// Lagrange coefficients at x=0 for the given evaluation points (ids). fn compute_lagrange_coeffs(ids: &[Fr]) -> Vec { let n = ids.len(); @@ -665,6 +794,22 @@ fn eval_poly_g1(coeffs_g1: &[G1], x: &Fr) -> G1 { result } +/// Parse a tweak as a scalar below the group order. +/// +/// [`Fr::from_bendian_reduce`] would fold an out-of-range tweak into the field +/// behind the caller's back, so the canonical parse is used instead and a +/// value at or above the order is refused. +/// +/// # Errors +/// +/// Returns `InvalidTweak` when `tweak` is not below the group order. +fn tweak_scalar(tweak: &[u8; 32]) -> Result { + let mut lendian = *tweak; + lendian.reverse(); + + Fr::from_repr(lendian).into_option().ok_or(BlsError::InvalidTweak) +} + /// Reduce participant ids into the scalar field, rejecting ids that /// reduce to zero and duplicates after reduction. /// @@ -674,7 +819,7 @@ fn eval_poly_g1(coeffs_g1: &[G1], x: &Fr) -> G1 { fn reduce_share_ids(ids: &[&BlsShareId]) -> Result, BlsError> { let fr_ids = ids .iter() - .map(|id| Fr::from_share_id(id)) + .map(|id| Fr::from_bendian_reduce(id.as_bytes())) .collect::, BlsError>>()?; let mut reduced: Vec<[u8; 32]> = fr_ids.iter().map(|fr| *fr.to_lendian()).collect(); reduced.sort_unstable(); diff --git a/pkgs/pkc/src/bls/secret_ops.rs b/pkgs/pkc/src/bls/secret_ops.rs index df538054..d3f5de71 100644 --- a/pkgs/pkc/src/bls/secret_ops.rs +++ b/pkgs/pkc/src/bls/secret_ops.rs @@ -24,6 +24,7 @@ use dash_types::dlgt_scodec; #[cfg(feature = "codec")] use dash_types::type_id::TypeId; use dash_types::{qtypestr, type_cvrt}; +use rand_core::CryptoRng; use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing}; use core::fmt::{Debug, Formatter, Result as FmtResult}; @@ -40,10 +41,23 @@ impl BlsSecretKey { /// /// # Errors /// - /// Returns `InvalidKeyMaterial` or `InvalidSecretKey` when `ikm` - /// is shorter than 32 bytes. - pub fn generate(ikm: &[u8]) -> Result { - S::generate(ikm).map(Self) + /// Returns `InvalidKeyMaterial` when `ikm` is shorter than 32 bytes. + pub fn from_ikm(ikm: &[u8]) -> Result { + S::sk_from_ikm(ikm).map(Self) + } + + /// Generate a new random secret key. + /// + /// Draws the key material and hands it to [`from_ikm`](Self::from_ikm). + pub fn generate(rng: &mut impl CryptoRng) -> Self { + loop { + let mut ikm = Zeroizing::new([0u8; 32]); + rng.fill_bytes(&mut *ikm); + + if let Ok(key) = Self::from_ikm(&*ikm) { + return key; + } + } } /// Parse from a 32-byte big-endian scalar. @@ -73,11 +87,28 @@ impl BlsSecretKey { BlsSecretKey::::from_bytes(&self.to_bytes()) } + /// Add `tweak` to the secret scalar, modulo the group order. + /// + /// # Errors + /// + /// Returns `InvalidTweak` when `tweak` is not below the group order, or when + /// the sum is zero. A zero sum means the tweak is this scalar's additive + /// inverse, so whoever chose the tweak already knows the key; the sum is + /// refused rather than returned as a key. + pub fn add_tweak(&self, tweak: &[u8; 32]) -> Result { + S::add_tweak_sk(&self.0, tweak).map(Self::from_inner) + } + /// Derive the corresponding public key. pub fn public_key(&self) -> BlsPublicKey { BlsPublicKey(S::derive_pk(&self.0)) } + /// Whether `pubkey` is this key's public counterpart. + pub fn verify_pubkey(&self, pubkey: &BlsPublicKey) -> bool { + self.public_key() == *pubkey + } + /// Sign a message of the scheme's message type. pub fn sign(&self, msg: &S::Msg) -> BlsSignature { BlsSignature::from_inner(S::sign(&self.0, msg)) @@ -160,8 +191,8 @@ 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 Zeroizing, BlsError, |sk| { + Fr::from_bendian_reduce(&sk.to_bytes()).map(Zeroizing::new) }); type_cvrt!(for[S: BlsScheme] TryFrom for BlsSecretKey, BlsError, |scalar| { @@ -172,14 +203,94 @@ type_cvrt!(for[S: BlsScheme] TryFrom for BlsSecretKey, BlsError, |scalar| #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; - use crate::bls::tests::RSEED; - use crate::bls::{BlsScChia, BlsScIetf}; + use crate::bls::tests::{GROUP_ORDER, RSEED}; + use crate::bls::{BlsError, BlsScChia, BlsScIetf}; use dash_dev::{arr_from_hex, Corpus}; use hex_conservative::DisplayHex; use rstest::rstest; use serde::Deserialize; + /// `(a + t)G` has to equal `aG + tG`, or the same tweak applied to the two + /// halves of a key pair would part them. + fn assert_tweaking_agrees_on_both_sides() { + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let tweak = *BlsSecretKey::::from_ikm(&RSEED[1]).unwrap().to_bytes(); + + let tweaked_sk = sk.add_tweak(&tweak).unwrap(); + let tweaked_pk = sk.public_key().add_tweak(&tweak).unwrap(); + + assert_eq!(tweaked_sk.public_key(), tweaked_pk); + } + + #[rstest] + #[case::chia(assert_tweaking_agrees_on_both_sides::)] + #[case::ietf(assert_tweaking_agrees_on_both_sides::)] + fn tweaking_agrees_on_both_sides(#[case] assertion: fn()) { + assertion(); + } + + fn assert_tweak_at_or_above_the_order_refused() { + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + + assert_eq!(sk.add_tweak(&GROUP_ORDER), Err(BlsError::InvalidTweak)); + assert_eq!(sk.add_tweak(&[0xff; 32]), Err(BlsError::InvalidTweak)); + assert_eq!(sk.public_key().add_tweak(&GROUP_ORDER), Err(BlsError::InvalidTweak)); + assert_eq!(sk.public_key().mul_tweak(&GROUP_ORDER), Err(BlsError::InvalidTweak)); + } + + #[rstest] + #[case::chia(assert_tweak_at_or_above_the_order_refused::)] + #[case::ietf(assert_tweak_at_or_above_the_order_refused::)] + fn a_tweak_at_or_above_the_order_is_refused(#[case] assertion: fn()) { + assertion(); + } + + /// `order - a`, so `a + t == 0`. Whoever picks the tweak can compute it + /// from `aG` alone, so the sum has to be refused rather than handed back. + fn assert_tweak_summing_to_zero_refused() { + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let scalar = *sk.to_bytes(); + let mut tweak = GROUP_ORDER; + let mut borrow = 0i16; + + for i in (0..32).rev() { + let diff = i16::from(tweak[i]) - i16::from(scalar[i]) - borrow; + borrow = i16::from(diff < 0); + tweak[i] = diff.rem_euclid(256) as u8; + } + + assert_eq!(sk.add_tweak(&tweak), Err(BlsError::InvalidTweak)); + // The point at infinity is no key either. + assert_eq!(sk.public_key().add_tweak(&tweak), Err(BlsError::InvalidTweak)); + } + + #[rstest] + #[case::chia(assert_tweak_summing_to_zero_refused::)] + #[case::ietf(assert_tweak_summing_to_zero_refused::)] + fn a_tweak_summing_to_zero_is_refused(#[case] assertion: fn()) { + assertion(); + } + + /// `t(aG) == a(tG)`, so multiplying a point commutes with multiplying the + /// scalar that made it. + fn assert_point_product_matches_scalar_product() { + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let factor_sk = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); + + let product = sk.public_key().mul_tweak(&factor_sk.to_bytes()).unwrap(); + let expected = factor_sk.public_key().mul_tweak(&sk.to_bytes()).unwrap(); + + assert_eq!(product, expected); + } + + #[rstest] + #[case::chia(assert_point_product_matches_scalar_product::)] + #[case::ietf(assert_point_product_matches_scalar_product::)] + fn multiplying_a_point_matches_multiplying_the_scalar(#[case] assertion: fn()) { + assertion(); + } + #[derive(Deserialize)] struct KeygenVec { sk: String, @@ -195,7 +306,7 @@ mod tests { /// 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 sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let there = sk.to_scheme::().unwrap(); assert_eq!(*there.to_bytes(), *sk.to_bytes()); @@ -211,7 +322,7 @@ mod tests { } fn assert_roundtrip() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let bytes = sk.to_bytes(); let decoded = BlsSecretKey::::from_bytes(&bytes).unwrap(); assert_eq!(decoded.to_bytes(), bytes); @@ -243,7 +354,7 @@ mod tests { /// Key generation follows the KeyGen of draft-irtf-cfrg-bls-signature-03 /// for both schemes; another variant would change these bytes. fn assert_keygen_draft03(ikm: &[u8], expected: &str) { - let sk = BlsSecretKey::::generate(ikm).unwrap(); + let sk = BlsSecretKey::::from_ikm(ikm).unwrap(); assert_eq!(sk.to_bytes().to_lower_hex_string(), expected); } @@ -258,7 +369,10 @@ mod tests { /// The keygen variant requires at least 32 bytes of input key material. fn assert_short_ikm_rejected() { - assert!(BlsSecretKey::::generate(&[0u8; 31]).is_err()); + assert_eq!( + BlsSecretKey::::from_ikm(&[0u8; 31]).map(|_| ()), + Err(BlsError::InvalidKeyMaterial) + ); } #[rstest] @@ -272,7 +386,7 @@ mod tests { /// scheme mix-up cannot go unnoticed. #[rstest] fn public_key_formats_differ() { - let chia = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let chia = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let ietf = BlsSecretKey::::from_bytes(&chia.to_bytes()).unwrap(); assert_ne!(chia.public_key().to_bytes(), ietf.public_key().to_bytes()); } @@ -281,7 +395,7 @@ mod tests { fn assert_codec_roundtrip() { use dash_types::codec::BaseCodec; - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let mut buf = Vec::new(); sk.encode(&mut buf); assert_eq!(buf.len(), 32); diff --git a/pkgs/pkc/src/bls/share_id.rs b/pkgs/pkc/src/bls/share_id.rs index 2c36eaad..92339c87 100644 --- a/pkgs/pkc/src/bls/share_id.rs +++ b/pkgs/pkc/src/bls/share_id.rs @@ -11,6 +11,25 @@ use dash_types::Numeric; make_hash! { /// Threshold participant identifier. + /// + /// Stored in the order the curve reads and shown in the order the reference + /// implementation prints, which are opposite ends of the same 32 bytes. + /// + /// The id is a masternode's `proTxHash`, held there as a `uint256` whose + /// `GetHex` reverses, so every RPC and log quotes the reverse of the bytes + /// it stores. + /// + /// Those stored bytes are what the curve gets, unreversed. `Threshold` + /// reads them through relic's `bn_read_bin`, which is big-endian, so the + /// scalar is the stored order read as an integer. + /// + /// One id therefore has two spellings there and they are byte reverses. + /// `CBLSId` wraps the same `uint256` but inherits a plain `HexStr`, so it + /// prints the stored order; nothing a user sees does. + /// + /// So `as_bytes` holds the curve's order while `Display` gives the quoted + /// order, and the scalar must be reduced from `as_bytes()`, never from + /// `to_bendian()`, since the displayed order names a different participant. BlsShareId, 32 } diff --git a/pkgs/pkc/src/bls/share_ops.rs b/pkgs/pkc/src/bls/share_ops.rs index 2940bdb1..3f47c85d 100644 --- a/pkgs/pkc/src/bls/share_ops.rs +++ b/pkgs/pkc/src/bls/share_ops.rs @@ -127,14 +127,71 @@ impl Hash for BlsSigShare { } } +/// Public key share from a threshold participant. +#[cfg_attr(feature = "codec", derive(Unencodable))] +#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(bound(serialize = "", deserialize = "")))] +pub struct BlsPkShare { + id: BlsShareId, + pk: BlsPublicKey, +} + +impl BlsPkShare { + /// Construct a public key share from an ID and a public key. + pub fn new(id: BlsShareId, pk: BlsPublicKey) -> Self { + Self { id, pk } + } + + /// Participant identifier. + pub fn id(&self) -> &BlsShareId { + &self.id + } + + /// The underlying public key. + pub fn public_key(&self) -> &BlsPublicKey { + &self.pk + } +} + +impl Clone for BlsPkShare { + fn clone(&self) -> Self { + Self { + id: self.id, + pk: self.pk.clone(), + } + } +} + +impl Debug for BlsPkShare { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + qtypestr(f, core::any::type_name::())?; + write!(f, "(id={:?})", self.id) + } +} + +impl PartialEq for BlsPkShare { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.pk == other.pk + } +} + +impl Eq for BlsPkShare {} + +impl Hash for BlsPkShare { + fn hash(&self, state: &mut H) { + self.id.hash(state); + state.write(&self.pk.to_bytes()); + } +} + impl BlsSecretKey { /// Split this secret key into shares for the given participant IDs, requiring /// `threshold` shares to recover. /// /// # Errors /// - /// Returns `ThresholdTooLarge` if `threshold` is below 2 or exceeds the - /// number of ids, `InvalidShareId` if any id reduces to zero, + /// Returns `InvalidThreshold` if `threshold` is below 2, exceeds id count + /// or no ids are supplied, `ZeroScalar` if any id reduces to zero, /// `DuplicateShareId` if two ids collide mod the group order, or /// `InvalidSecretKey` if share generation fails. pub fn split( @@ -153,8 +210,8 @@ impl BlsSecretKey { /// /// # Errors /// - /// Returns `InvalidVerificationVector` when fewer than two master keys are - /// given, `InvalidShareId` on a zero-reducing id, or `InvalidSecretKey` + /// Returns `InsufficientCoefficients` when fewer than two master keys are + /// given, `ZeroScalar` on a zero-reducing id, or `InvalidSecretKey` /// when the result is not a valid scalar. pub fn derive_share(master_sks: &[&Self], id: &BlsShareId) -> Result { let inner_refs: Vec<&S::InnerSk> = master_sks.iter().map(|sk| &sk.0).collect(); @@ -168,19 +225,35 @@ impl BlsPublicKey { /// /// # Errors /// - /// Returns `InvalidVerificationVector` when fewer than two master keys are - /// given, `InvalidShareId` on a zero-reducing id, or `InvalidPublicKey` + /// Returns `InsufficientCoefficients` when fewer than two master keys are + /// given, `ZeroScalar` on a zero-reducing id, or `InvalidPublicKey` /// when a coefficient or the result fails to decode. 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) } + + /// Recover the master public key from threshold public key shares via + /// Lagrange interpolation in G1. + /// + /// # Errors + /// + /// Returns `InsufficientShares` if fewer than 2 shares are provided, + /// `ZeroScalar`/`DuplicateShareId` on bad ids, or `InvalidPublicKey` + /// when a share fails to decode. + pub fn recover_shares(shares: &[&BlsPkShare]) -> Result { + let ids: Vec<&BlsShareId> = shares.iter().map(|s| s.id()).collect(); + let pks: Vec<&S::InnerPk> = shares.iter().map(|s| &s.public_key().0).collect(); + + S::recover_pk_shares(&ids, &pks).map(Self::from_inner) + } } #[cfg(test)] #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; + use crate::bls::scalar::Fr; use crate::bls::tests::{make_id, sequential_ids, GROUP_ORDER, MSG_DEADBEEF, RSEED}; use crate::bls::{BlsScChia, BlsScIetf}; @@ -202,24 +275,24 @@ mod tests { break; } } - BlsShareId::from_bendian(bytes) + BlsShareId::from_lendian(bytes) } /// A 1-of-n split hands the master key to every participant, so a `threshold` /// below 2 is rejected; one above the participant count yields a quorum that /// can never sign. fn assert_invalid_thresholds_rejected() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let ids = sequential_ids(5); for threshold in [0, 1, ids.len() + 1] { assert!(matches!( sk.split(threshold, &ids, &mut UnwrapErr(SysRng)), - Err(BlsError::ThresholdTooLarge) + Err(BlsError::InvalidThreshold) )); } assert!(matches!( sk.split(2, &[], &mut UnwrapErr(SysRng)), - Err(BlsError::ThresholdTooLarge) + Err(BlsError::InvalidThreshold) )); } @@ -233,20 +306,20 @@ mod tests { /// An id congruent to zero mod `r` would make the share equal the master key, /// so both the zero hash and the group order are rejected. fn assert_zero_reducing_id_rejected() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); - let zero = BlsShareId::from_bendian([0u8; 32]); + let zero = BlsShareId::from_lendian([0u8; 32]); let ids = [make_id(1), zero]; assert!(matches!( sk.split(2, &ids, &mut UnwrapErr(SysRng)), - Err(BlsError::InvalidShareId) + Err(BlsError::ZeroScalar) )); - let order = BlsShareId::from_bendian(GROUP_ORDER); + let order = BlsShareId::from_lendian(GROUP_ORDER); let ids = [make_id(1), order]; assert!(matches!( sk.split(2, &ids, &mut UnwrapErr(SysRng)), - Err(BlsError::InvalidShareId) + Err(BlsError::ZeroScalar) )); } @@ -260,7 +333,7 @@ mod tests { /// Two ids congruent mod `r` collide during interpolation, and a raw-byte /// duplicate check would miss `1` and `r + 1`. fn assert_congruent_ids_rejected() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let ids = [make_id(1), group_order_plus_one()]; assert!(matches!( sk.split(2, &ids, &mut UnwrapErr(SysRng)), @@ -281,7 +354,7 @@ mod tests { fn assert_sk_share_matches_pk_share() { let master: Vec> = [&RSEED[0], &RSEED[1], &RSEED[2]] .iter() - .map(|ikm| BlsSecretKey::::generate(*ikm).unwrap()) + .map(|ikm| BlsSecretKey::::from_ikm(*ikm).unwrap()) .collect(); let master_refs: Vec<&BlsSecretKey> = master.iter().collect(); let vvec: Vec> = master.iter().map(BlsSecretKey::public_key).collect(); @@ -296,11 +369,11 @@ mod tests { assert!(matches!( BlsSecretKey::::derive_share(&master_refs[..1], &make_id(1)), - Err(BlsError::InvalidVerificationVector) + Err(BlsError::InsufficientCoefficients) )); assert!(matches!( - BlsSecretKey::::derive_share(&master_refs, &BlsShareId::from_bendian([0u8; 32])), - Err(BlsError::InvalidShareId) + BlsSecretKey::::derive_share(&master_refs, &BlsShareId::from_lendian([0u8; 32])), + Err(BlsError::ZeroScalar) )); } @@ -314,10 +387,10 @@ mod tests { /// Evaluating the verification-vector polynomial needs at least two /// coefficients, so a single master key is rejected. fn assert_derive_share_rejects_short_vv() { - let pk = BlsSecretKey::::generate(&RSEED[0]).unwrap().public_key(); + let pk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap().public_key(); assert!(matches!( BlsPublicKey::::derive_share(&[&pk], &make_id(1)), - Err(BlsError::InvalidVerificationVector) + Err(BlsError::InsufficientCoefficients) )); } @@ -328,6 +401,108 @@ mod tests { assertion(); } + /// Public key shares interpolate back to the key they were derived from. + /// + /// Compared against the master key itself rather than verified against the + /// shares, which is what pins interpolation to the right point in G1 rather + /// than to a self-consistent point. + fn assert_pk_shares_recover_master() { + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let ids = sequential_ids(5); + + let sk_shares = sk.split(3, &ids, &mut UnwrapErr(SysRng)).unwrap(); + let pk_shares: Vec> = sk_shares + .iter() + .map(|s| BlsPkShare::new(*s.id(), s.secret_key().public_key())) + .collect(); + + // Any threshold-sized subset recovers the master key, and a different + // subset recovers the same key. + let first: Vec<&BlsPkShare> = pk_shares[..3].iter().collect(); + let second: Vec<&BlsPkShare> = pk_shares[2..].iter().collect(); + assert_eq!(BlsPublicKey::::recover_shares(&first).unwrap(), sk.public_key()); + assert_eq!(BlsPublicKey::::recover_shares(&second).unwrap(), sk.public_key()); + + // One share is a point, not a polynomial, and two ids that collide mod + // the group order would invert a zero denominator. + assert!(matches!( + BlsPublicKey::::recover_shares(&first[..1]), + Err(BlsError::InsufficientShares) + )); + assert!(matches!( + BlsPublicKey::::recover_shares(&[first[0], first[0]]), + Err(BlsError::DuplicateShareId) + )); + } + + #[rstest] + #[case::chia(assert_pk_shares_recover_master::)] + #[case::ietf(assert_pk_shares_recover_master::)] + fn pk_shares_recover_master(#[case] assertion: fn()) { + assertion(); + } + + /// Two shares whose Lagrange-weighted sum cancels recover the identity, + /// which is no public key. + fn assert_pk_shares_reject_identity() { + let (id0, id1) = (make_id(1), make_id(2)); + let x0 = Fr::from_bendian_reduce(id0.as_bytes()).unwrap(); + let x1 = Fr::from_bendian_reduce(id1.as_bytes()).unwrap(); + + let a = Fr::from_bendian_reduce(&RSEED[1]).unwrap(); + let b = a * x1 * x0.inverse(); + + let sk0 = BlsSecretKey::::from_bytes(&a.to_bendian()).unwrap(); + let sk1 = BlsSecretKey::::from_bytes(&b.to_bendian()).unwrap(); + + let share0 = BlsPkShare::new(id0, sk0.public_key()); + let share1 = BlsPkShare::new(id1, sk1.public_key()); + + assert_ne!(share0.public_key(), share1.public_key()); + assert!(matches!( + BlsPublicKey::::recover_shares(&[&share0, &share1]), + Err(BlsError::InvalidPublicKey) + )); + } + + #[rstest] + #[case::chia(assert_pk_shares_reject_identity::)] + #[case::ietf(assert_pk_shares_reject_identity::)] + fn pk_shares_reject_identity(#[case] assertion: fn()) { + assertion(); + } + + /// Two signature shares over one message whose weighted sum cancels recover + /// the identity, which signs nothing; the G2 mirror of the key case above. + fn assert_sig_shares_reject_identity() { + let (id0, id1) = (make_id(1), make_id(2)); + let x0 = Fr::from_bendian_reduce(id0.as_bytes()).unwrap(); + let x1 = Fr::from_bendian_reduce(id1.as_bytes()).unwrap(); + + let a = Fr::from_bendian_reduce(&RSEED[1]).unwrap(); + let b = a * x1 * x0.inverse(); + + let sk0 = BlsSecretKey::::from_bytes(&a.to_bendian()).unwrap(); + let sk1 = BlsSecretKey::::from_bytes(&b.to_bendian()).unwrap(); + + let msg = S::msg_ref(&MSG_DEADBEEF); + let share0 = BlsSigShare::new(id0, sk0.sign(msg)); + let share1 = BlsSigShare::new(id1, sk1.sign(msg)); + + assert_ne!(share0.signature().to_bytes(), share1.signature().to_bytes()); + assert!(matches!( + BlsSignature::::recover_shares(&[&share0, &share1]), + Err(BlsError::InvalidSignature) + )); + } + + #[rstest] + #[case::chia(assert_sig_shares_reject_identity::)] + #[case::ietf(assert_sig_shares_reject_identity::)] + fn sig_shares_reject_identity(#[case] assertion: fn()) { + assertion(); + } + /// End-to-end quorum DKG validation against reference vectors, exercising the /// full flow: contribute -> verify -> commit -> finalize. fn assert_llmq_contribute_vvec(scheme: &str) { @@ -519,7 +694,7 @@ mod tests { let sig = sk_share.sign(S::msg_ref(&msg)); let pk = sk_share.public_key(); assert!( - sig.verify(S::msg_ref(&msg), &pk).is_ok(), + pk.verify(S::msg_ref(&msg), &sig).is_ok(), "{} failed self-verification at member {}", label, c["member_idx"], @@ -575,12 +750,12 @@ mod tests { .collect(); let share_refs: Vec<&BlsSigShare> = sig_shares.iter().collect(); - let recovered = BlsSignature::recover(&share_refs).unwrap(); + let recovered = BlsSignature::recover_shares(&share_refs).unwrap(); let quorum_pk = BlsPublicKey::::from_bytes(&arr_from_hex(commits[0]["quorum_public_key"].as_str().unwrap())).unwrap(); assert!( - recovered.verify(S::msg_ref(&quorum_hash), &quorum_pk).is_ok(), + quorum_pk.verify(S::msg_ref(&quorum_hash), &recovered).is_ok(), "recovered quorum sig failed verification" ); @@ -598,7 +773,7 @@ mod tests { }) .collect(); let all_refs: Vec<&BlsSigShare> = all_shares.iter().collect(); - let recovered_all = BlsSignature::recover(&all_refs).unwrap(); + let recovered_all = BlsSignature::recover_shares(&all_refs).unwrap(); assert_eq!( recovered.to_bytes(), recovered_all.to_bytes(), @@ -682,8 +857,8 @@ mod tests { /// quietly dropping a field. Shares agreeing on id and signature compare and /// hash alike; changing either separates them. fn assert_share_eq_and_hash() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let other_sk = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let other_sk = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let msg = S::msg_ref(&MSG_DEADBEEF); let share = BlsSkShare::new(make_id(1), sk.clone()).sign(msg); @@ -716,7 +891,7 @@ mod tests { use dash_dev::assert_json_rt; fn assert_share_serde_roundtrip() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); assert_json_rt(&BlsSkShare::new(make_id(1), sk).sign(S::msg_ref(&MSG_DEADBEEF))); } diff --git a/pkgs/pkc/src/bls/sig_aggregate.rs b/pkgs/pkc/src/bls/sig_aggregate.rs index adba4e8a..b11fd7e2 100644 --- a/pkgs/pkc/src/bls/sig_aggregate.rs +++ b/pkgs/pkc/src/bls/sig_aggregate.rs @@ -132,8 +132,8 @@ mod tests { } fn assert_aggregate_same_message() { - let sk1 = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let sk2 = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk1 = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk2 = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let sig1 = sk1.sign(S::msg_ref(&MSG_DEADBEEF)); let sig2 = sk2.sign(S::msg_ref(&MSG_DEADBEEF)); @@ -144,7 +144,7 @@ mod tests { let msg = S::msg_ref(&MSG_DEADBEEF); assert!(agg.fast_verify_aggregates(msg, &[&pk1, &pk2]).is_ok()); // A key not in the set must make verification fail. - let pk3 = BlsSecretKey::::generate(&RSEED[2]).unwrap().public_key(); + let pk3 = BlsSecretKey::::from_ikm(&RSEED[2]).unwrap().public_key(); assert!(agg.fast_verify_aggregates(msg, &[&pk1, &pk3]).is_err()); // Rogue-key resistance: a naive aggregate must not pass weighted verify. assert!(agg.secure_verify_aggregates(msg, &[&pk1, &pk2]).is_err()); @@ -160,8 +160,8 @@ mod tests { /// 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 sk1 = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk2 = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let msg = S::msg_ref(&MSG_DEADBEEF); let sig1 = sk1.sign(msg); @@ -170,7 +170,7 @@ mod tests { 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()); + assert!(sk2.public_key().verify(msg, &agg.sub_insecure(&sig1).unwrap()).is_ok()); } #[rstest] @@ -184,7 +184,7 @@ mod tests { /// 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 sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let sig = sk.sign(S::msg_ref(&MSG_DEADBEEF)); assert_eq!(sig.sub_insecure(&sig), Err(BlsError::InvalidSignature)); @@ -224,8 +224,8 @@ mod tests { /// the two fails. Both schemes agree here, along with the count and /// emptiness contracts. fn assert_distinct_messages_verify() { - let sk1 = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let sk2 = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk1 = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk2 = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let msg1 = S::msg_ref(&MSG_8BADFOOD); let msg2 = S::msg_ref(&MSG_DEADBEEF); @@ -256,8 +256,8 @@ mod tests { /// which either could have picked to cancel the other. IETF refuses it; Chia /// accepts. fn assert_duplicate_message_policy(accepted: bool) { - let sk1 = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let sk2 = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk1 = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk2 = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let msg = S::msg_ref(&MSG_DEADBEEF); let sig1 = sk1.sign(msg); @@ -321,8 +321,8 @@ mod tests { /// weights follow the sorted keys rather than the caller's order, so the /// same set aggregates alike however it is presented. fn assert_secure_aggregate_round_trips() { - let sk1 = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let sk2 = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk1 = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk2 = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let msg = S::msg_ref(&MSG_DEADBEEF); let sig1 = sk1.sign(msg); @@ -399,7 +399,7 @@ mod tests { /// [`secure_aggregate_round_trips`] holds the distinct-key case, where the /// keys give a total order and the argument order stops mattering. fn assert_duplicate_key_pairing_is_order_bound() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let pk = sk.public_key(); let sig_a = sk.sign(S::msg_ref(&MSG_8BADFOOD)); @@ -438,7 +438,7 @@ mod tests { fn assert_order_independent() { let sks: Vec> = [RSEED[0], RSEED[1], RSEED[2]] .iter() - .map(|seed| BlsSecretKey::::generate(seed).unwrap()) + .map(|seed| BlsSecretKey::::from_ikm(seed).unwrap()) .collect(); let sigs: Vec> = sks.iter().map(|sk| sk.sign(S::msg_ref(&MSG_DEADBEEF))).collect(); let pks: Vec> = sks.iter().map(BlsSecretKey::public_key).collect(); @@ -491,7 +491,7 @@ mod tests { /// and consensus depends on it continuing to; the IETF scheme rejects it. /// The sign bit sits at bit 7 for legacy and bit 5 for IETF. fn assert_identity_cancellation(sign_bit: u8, accepted: bool) { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let signed = MSG_8BADFOOD; let sig = sk.sign(S::msg_ref(&signed)); let pk = sk.public_key(); @@ -517,7 +517,7 @@ mod tests { /// by computation, not off the wire. #[rstest] fn chia_identity_encodes_canonically() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let sig = sk.sign(&MSG_8BADFOOD); let mut neg_bytes = sig.to_bytes(); diff --git a/pkgs/pkc/src/bls/sig_basic.rs b/pkgs/pkc/src/bls/sig_basic.rs index 2161254d..f8c26444 100644 --- a/pkgs/pkc/src/bls/sig_basic.rs +++ b/pkgs/pkc/src/bls/sig_basic.rs @@ -8,11 +8,10 @@ use super::error::BlsError; use super::group::G2; -use super::public_ops::BlsPublicKey; use super::scheme_ops::BlsScheme; +use super::BlsSigBytes; #[cfg(feature = "codec")] use super::BLS_SIG_LEN; -use super::{BlsScIetf, BlsSigBytes, BlsSigId}; #[cfg(feature = "codec")] use dash_num::Hash256; @@ -64,13 +63,8 @@ impl BlsSignature { T::g2_to_sig(S::sig_to_g2(&self.0)?).map(BlsSignature::from_inner) } - /// Verify over a message of the scheme's message type. - /// - /// # Errors - /// - /// Returns `VerifyFailed` when the pairing check does not hold. - pub fn verify(&self, msg: &S::Msg, pk: &BlsPublicKey) -> Result<(), BlsError> { - S::verify(&self.0, msg, &pk.0) + pub(crate) fn as_inner(&self) -> &S::InnerSig { + &self.0 } pub(crate) fn from_inner(inner: S::InnerSig) -> Self { @@ -78,17 +72,6 @@ impl BlsSignature { } } -impl BlsSignature { - /// Verify under the domain separation tag selected by `scheme`. - /// - /// # Errors - /// - /// Returns `VerifyFailed` when the pairing check does not hold. - pub fn verify_with(&self, msg: &[u8], pk: &BlsPublicKey, scheme: BlsSigId) -> Result<(), BlsError> { - BlsScIetf::verify_with(&self.0, msg, &pk.0, scheme) - } -} - impl Clone for BlsSignature { fn clone(&self) -> Self { Self(self.0.clone()) @@ -136,12 +119,13 @@ type_cvrt!(for[S: BlsScheme] TryFrom for BlsSignature, BlsError, |point| #[expect(clippy::unwrap_used, reason = "test code")] mod tests { use super::*; + use crate::bls::public_ops::BlsPublicKey; use crate::bls::secret_ops::BlsSecretKey; use crate::bls::tests::{ ser_pairs, test_ikm, test_msg, SerType, G2_OFF_SUBGROUP_CHIA, G2_OFF_SUBGROUP_IETF, MSG_8BADFOOD, MSG_DEADBEEF, RSEED, }; - use crate::bls::{BlsScChia, BlsScIetf}; + use crate::bls::{BlsScChia, BlsScIetf, BlsSigId}; use crate::prelude::*; use cfg_if::cfg_if; @@ -158,15 +142,15 @@ mod tests { } fn assert_sign_verify() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let pk = sk.public_key(); let sig = sk.sign(S::msg_ref(&MSG_DEADBEEF)); - assert!(sig.verify(S::msg_ref(&MSG_DEADBEEF), &pk).is_ok()); - assert!(sig.verify(S::msg_ref(&MSG_8BADFOOD), &pk).is_err()); + assert!(pk.verify(S::msg_ref(&MSG_DEADBEEF), &sig).is_ok()); + assert!(pk.verify(S::msg_ref(&MSG_8BADFOOD), &sig).is_err()); - let other_pk = BlsSecretKey::::generate(&RSEED[1]).unwrap().public_key(); - assert!(sig.verify(S::msg_ref(&MSG_DEADBEEF), &other_pk).is_err()); + let other_pk = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap().public_key(); + assert!(other_pk.verify(S::msg_ref(&MSG_DEADBEEF), &sig).is_err()); } #[rstest] @@ -181,9 +165,9 @@ mod tests { /// and another key all fail. #[rstest] fn ietf_signature_variant_contract() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let pk = sk.public_key(); - let other_pk = BlsSecretKey::::generate(&RSEED[1]).unwrap().public_key(); + let other_pk = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap().public_key(); let msg = b"variant-bound message"; let wrong_msg = b"another message"; @@ -192,13 +176,13 @@ mod tests { (BlsSigId::ProofOfPossession, BlsSigId::Basic), ] { let sig = sk.sign_with(msg, variant); - assert!(sig.verify_with(msg, &pk, variant).is_ok()); - assert!(sig.verify_with(msg, &pk, other).is_err()); - assert!(sig.verify_with(wrong_msg, &pk, variant).is_err()); - assert!(sig.verify_with(msg, &other_pk, variant).is_err()); + assert!(pk.verify_with(msg, &sig, variant).is_ok()); + assert!(pk.verify_with(msg, &sig, other).is_err()); + assert!(pk.verify_with(wrong_msg, &sig, variant).is_err()); + assert!(other_pk.verify_with(msg, &sig, variant).is_err()); let decoded = BlsSignature::::from_bytes(&sig.to_bytes()).unwrap(); - assert!(decoded.verify_with(msg, &pk, variant).is_ok()); + assert!(pk.verify_with(msg, &decoded, variant).is_ok()); } assert_ne!( @@ -211,7 +195,7 @@ mod tests { /// BLS signing draws no randomness, so the same key over the same message /// yields the same signature every time. fn assert_sign_is_deterministic() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let msg = S::msg_ref(&MSG_DEADBEEF); assert_eq!(sk.sign(msg), sk.sign(msg)); } @@ -224,7 +208,7 @@ mod tests { } fn assert_sig_roundtrip() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let bytes = sk.sign(S::msg_ref(&MSG_DEADBEEF)).to_bytes(); assert_eq!(BlsSignature::::from_bytes(&bytes).unwrap().to_bytes(), bytes); } @@ -244,7 +228,7 @@ mod tests { let mut sig_signs = [false; 2]; for i in 0..64 { - let sk = BlsSecretKey::::generate(&test_ikm(i)).unwrap(); + let sk = BlsSecretKey::::from_ikm(&test_ikm(i)).unwrap(); let pk = sk.public_key(); let sig = sk.sign(S::msg_ref(&test_msg(i))); @@ -296,7 +280,7 @@ mod tests { #[case::sign_byte(0, 0x20)] #[case::swizzled_byte(48, 0x40)] fn chia_rejects_stray_signature_bits(#[case] index: usize, #[case] mask: u8) { - let clean = BlsSecretKey::::generate(&RSEED[0]) + let clean = BlsSecretKey::::from_ikm(&RSEED[0]) .unwrap() .sign(&MSG_DEADBEEF) .to_bytes(); @@ -385,14 +369,14 @@ mod tests { /// scheme mix-up cannot go unnoticed. #[rstest] fn signatures_differ_across_schemes() { - let chia = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let chia = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let ietf = BlsSecretKey::::from_bytes(&chia.to_bytes()).unwrap(); assert_ne!(chia.sign(&MSG_DEADBEEF).to_bytes(), ietf.sign(&MSG_DEADBEEF).to_bytes()); } /// Conversion re-encodes one point, so a round trip returns the original. fn assert_sig_scheme_conversion_round_trips() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let sig = sk.sign(S::msg_ref(&MSG_DEADBEEF)); let there = sig.to_scheme::().unwrap(); @@ -413,12 +397,12 @@ mod tests { /// hash nor the target's key. #[rstest] fn sig_scheme_conversion_does_not_move_the_augmentation() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let sig = sk.sign(&MSG_DEADBEEF); let converted = sig.to_scheme::().unwrap(); let pk_ietf = sk.public_key().to_scheme::().unwrap(); - assert!(converted.verify(&MSG_DEADBEEF, &pk_ietf).is_err()); + assert!(pk_ietf.verify(&MSG_DEADBEEF, &converted).is_err()); } /// A signature Chia admits and IETF does not must not become an IETF one by @@ -439,18 +423,18 @@ mod tests { /// pinned per scheme. #[rstest] fn serde_roundtrip() { - let chia = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let chia = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); assert_json_rt(&chia.sign(&MSG_DEADBEEF)); - let ietf = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let ietf = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); assert_json_rt(&ietf.sign(&MSG_DEADBEEF)); } #[rstest] fn serde_emits_hex_string() { - let chia = BlsSecretKey::::generate(&RSEED[0]) + let chia = BlsSecretKey::::from_ikm(&RSEED[0]) .unwrap() .sign(&MSG_DEADBEEF); - let ietf = BlsSecretKey::::generate(&RSEED[0]) + let ietf = BlsSecretKey::::from_ikm(&RSEED[0]) .unwrap() .sign(&MSG_DEADBEEF); diff --git a/pkgs/pkc/src/bls/sig_pop.rs b/pkgs/pkc/src/bls/sig_pop.rs index f5b89e42..a9990cbe 100644 --- a/pkgs/pkc/src/bls/sig_pop.rs +++ b/pkgs/pkc/src/bls/sig_pop.rs @@ -46,15 +46,15 @@ mod tests { #[rstest] fn ietf_proof_of_possession_roundtrip() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let proof = sk.prove_possession(); assert!(sk.public_key().verify_possession(&proof).is_ok()); } #[rstest] fn ietf_proof_of_possession_rejects_wrong_key() { - let sk0 = BlsSecretKey::::generate(&RSEED[0]).unwrap(); - let sk1 = BlsSecretKey::::generate(&RSEED[1]).unwrap(); + let sk0 = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let sk1 = BlsSecretKey::::from_ikm(&RSEED[1]).unwrap(); let proof = sk0.prove_possession(); assert!(sk1.public_key().verify_possession(&proof).is_err()); } diff --git a/pkgs/pkc/src/bls/sig_threshold.rs b/pkgs/pkc/src/bls/sig_threshold.rs index 070151c6..1580d279 100644 --- a/pkgs/pkc/src/bls/sig_threshold.rs +++ b/pkgs/pkc/src/bls/sig_threshold.rs @@ -20,9 +20,9 @@ impl BlsSignature { /// # Errors /// /// Returns `InsufficientShares` if fewer than 2 shares are provided, - /// `InvalidShareId`/`DuplicateShareId` on bad ids, or `InvalidSignature` + /// `ZeroScalar`/`DuplicateShareId` on bad ids, or `InvalidSignature` /// when a share fails to decode. - pub fn recover(shares: &[&BlsSigShare]) -> Result { + pub fn recover_shares(shares: &[&BlsSigShare]) -> Result { let ids: Vec<&BlsShareId> = shares.iter().map(|s| s.id()).collect(); let sigs: Vec<&S::InnerSig> = shares.iter().map(|s| &s.signature().0).collect(); @@ -35,7 +35,7 @@ impl BlsSignature { mod tests { use crate::bls::scheme_ops::BlsScheme; use crate::bls::tests::{make_id, sequential_ids, MSG_DEADBEEF, RSEED}; - use crate::bls::{BlsError, BlsScChia, BlsScIetf, BlsSecretKey, BlsSigShare, BlsSignature, BlsSkShare}; + use crate::bls::{BlsError, BlsScChia, BlsScIetf, BlsSecretKey, BlsShareId, BlsSigShare, BlsSignature, BlsSkShare}; use crate::prelude::*; use dash_dev::{arr_from_hex, Corpus, Value}; @@ -45,7 +45,7 @@ mod tests { use rstest::rstest; fn assert_threshold_split_recover() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let pk = sk.public_key(); let ids = sequential_ids(5); @@ -58,14 +58,14 @@ mod tests { let msg = S::msg_ref(&MSG_DEADBEEF); let sig_shares: Vec> = shares[..3].iter().map(|s| s.sign(msg)).collect(); let refs: Vec<&BlsSigShare> = sig_shares.iter().collect(); - let recovered = BlsSignature::::recover(&refs).unwrap(); - assert!(recovered.verify(msg, &pk).is_ok()); + let recovered = BlsSignature::::recover_shares(&refs).unwrap(); + assert!(pk.verify(msg, &recovered).is_ok()); assert_eq!(recovered.to_bytes(), sk.sign(msg).to_bytes()); // A different subset recovers the identical signature. let sig_shares2: Vec> = shares[2..5].iter().map(|s| s.sign(msg)).collect(); let refs2: Vec<&BlsSigShare> = sig_shares2.iter().collect(); - let recovered2 = BlsSignature::::recover(&refs2).unwrap(); + let recovered2 = BlsSignature::::recover_shares(&refs2).unwrap(); assert_eq!(recovered.to_bytes(), recovered2.to_bytes()); } @@ -79,17 +79,17 @@ mod tests { /// Interpolating fewer than `threshold` shares still yields a point, so the /// guard against a short quorum is that the result fails verification. fn assert_sub_threshold_does_not_verify() { - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let pk = sk.public_key(); 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(); - let below = BlsSignature::::recover(&[&signed[0], &signed[1]]).unwrap(); - assert!(below.verify(msg, &pk).is_err()); + let below = BlsSignature::::recover_shares(&[&signed[0], &signed[1]]).unwrap(); + assert!(pk.verify(msg, &below).is_err()); - let at = BlsSignature::::recover(&[&signed[0], &signed[2], &signed[4]]).unwrap(); - assert!(at.verify(msg, &pk).is_ok()); + let at = BlsSignature::::recover_shares(&[&signed[0], &signed[2], &signed[4]]).unwrap(); + assert!(pk.verify(msg, &at).is_ok()); } #[rstest] @@ -101,16 +101,16 @@ mod tests { fn assert_insufficient_shares_rejected() { assert!(matches!( - BlsSignature::::recover(&[]), + BlsSignature::::recover_shares(&[]), Err(BlsError::InsufficientShares) )); - let sk = BlsSecretKey::::generate(&RSEED[0]).unwrap(); + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); let ids = sequential_ids(3); 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]), + BlsSignature::::recover_shares(&[&one]), Err(BlsError::InsufficientShares) )); } @@ -122,6 +122,26 @@ mod tests { assertion(); } + /// The two slices are paired, so a length mismatch is a fault of its own + /// rather than a short quorum. + fn assert_mismatched_id_count_rejected() { + let sk = BlsSecretKey::::from_ikm(&RSEED[0]).unwrap(); + let ids = sequential_ids(3); + let shares = sk.split(2, &ids, &mut UnwrapErr(SysRng)).unwrap(); + let signed: Vec> = shares.iter().map(|s| s.sign(S::msg_ref(&MSG_DEADBEEF))).collect(); + + let id_refs: Vec<&BlsShareId> = ids.iter().collect(); + let sig_refs: Vec<&S::InnerSig> = signed[..2].iter().map(|s| &s.signature().0).collect(); + assert_eq!(S::recover_sig_shares(&id_refs, &sig_refs), Err(BlsError::CountMismatch)); + } + + #[rstest] + #[case::chia(assert_mismatched_id_count_rejected::)] + #[case::ietf(assert_mismatched_id_count_rejected::)] + fn recover_rejects_mismatched_id_count(#[case] assertion: fn()) { + assertion(); + } + /// Shares come from the corpus rather than a fresh `split`, whose random /// polynomial leaves nothing to assert against but a round trip. `full_sig` /// cross-checks interpolation against the master's own signature. @@ -168,7 +188,7 @@ mod tests { }) .collect(); let refs: Vec<&BlsSigShare> = picked.iter().collect(); - let recovered = BlsSignature::::recover(&refs).unwrap(); + let recovered = BlsSignature::::recover_shares(&refs).unwrap(); let expected = out["recovered_sig"].as_str().unwrap(); assert_eq!(recovered.to_bytes().to_lower_hex_string(), expected); diff --git a/pkgs/pkc/src/bls/tests.rs b/pkgs/pkc/src/bls/tests.rs index d94fb830..7c53d073 100644 --- a/pkgs/pkc/src/bls/tests.rs +++ b/pkgs/pkc/src/bls/tests.rs @@ -73,7 +73,7 @@ pub const fn ietf_g1_encoding(mut chia: [u8; 48]) -> [u8; 48] { pub fn make_id(i: u32) -> BlsShareId { let mut bytes = [0u8; 32]; bytes[28..32].copy_from_slice(&i.to_be_bytes()); - BlsShareId::from_bendian(bytes) + BlsShareId::from_lendian(bytes) } /// Build `n` sequential participant ids `1..=n`.