use commonware_codec::{DecodeExt, Encode as _};
use commonware_cryptography::bls12381::dkg;
use commonware_cryptography::bls12381::primitives::group::Share;
use commonware_cryptography::bls12381::primitives::ops::{self, threshold};
use commonware_cryptography::bls12381::primitives::sharing::{Mode, Sharing};
use commonware_cryptography::bls12381::primitives::variant::{MinSig, PartialSignature, Variant};
use commonware_parallel::Sequential;
use commonware_utils::{Faults, N3f1};
use crate::Error;
use crate::algorithm::Algorithm;
use crate::signer::Signer;
pub const NAMESPACE: &[u8] = b"mkit-attest/dsse/v1";
pub type V = MinSig;
const SIGNATURE_SIZE: usize = 48;
pub const PUBLIC_KEY_SIZE: usize = 96;
pub const KEYID_PREFIX: &str = "bls12381-thr:";
#[derive(Debug)]
pub struct ThresholdSigner {
share: Share,
sharing: Sharing<V>,
keyid: String,
}
impl ThresholdSigner {
#[must_use]
pub fn new(share: Share, sharing: Sharing<V>) -> Self {
let pk_hex = mkit_core::to_hex_bytes(&sharing.public().encode());
let keyid = format!("{KEYID_PREFIX}{pk_hex}");
Self {
share,
sharing,
keyid,
}
}
#[must_use]
pub fn aggregate_public_key(&self) -> Vec<u8> {
self.sharing.public().encode().to_vec()
}
}
impl Signer for ThresholdSigner {
fn algorithm(&self) -> Algorithm {
Algorithm::Bls12381Threshold
}
fn keyid(&self) -> Result<String, Error> {
Ok(self.keyid.clone())
}
fn sign(&mut self, pae: &[u8]) -> Result<Vec<u8>, Error> {
let partial = threshold::sign_message::<V>(&self.share, NAMESPACE, pae);
Ok(partial.encode().to_vec())
}
}
pub fn aggregate(sharing: &Sharing<V>, partials: &[Vec<u8>]) -> Result<Vec<u8>, Error> {
let mut decoded: Vec<PartialSignature<V>> = Vec::with_capacity(partials.len());
for p in partials {
let ps = PartialSignature::<V>::decode(p.as_slice())
.map_err(|_| Error::BlsThresholdPartialDecode)?;
decoded.push(ps);
}
let sig = threshold::recover::<V, _>(sharing, &decoded, &Sequential)
.map_err(|_| Error::BlsThresholdInsufficientPartials)?;
Ok(sig.encode().to_vec())
}
pub fn verify(aggregate_pubkey: &[u8], message: &[u8], signature: &[u8]) -> Result<(), Error> {
if aggregate_pubkey.len() != PUBLIC_KEY_SIZE {
return Err(Error::BlsThresholdPublicKeyDecode);
}
let pk = <V as Variant>::Public::decode(aggregate_pubkey)
.map_err(|_| Error::BlsThresholdPublicKeyDecode)?;
if signature.len() != SIGNATURE_SIZE {
return Err(Error::BlsThresholdSignatureDecode);
}
let sig = <V as Variant>::Signature::decode(signature)
.map_err(|_| Error::BlsThresholdSignatureDecode)?;
ops::verify_message::<V>(&pk, NAMESPACE, message, &sig)
.map_err(|_| Error::BlsThresholdVerifyFailed)
}
#[must_use]
pub fn trusted_dealer<R: rand_core::CryptoRng>(
rng: &mut R,
n: core::num::NonZeroU32,
) -> (Sharing<V>, Vec<Share>) {
dkg::feldman_desmedt::deal_anonymous::<V, N3f1>(rng, Mode::NonZeroCounter, n)
}
#[must_use]
pub fn threshold_for(n: u32) -> u32 {
N3f1::quorum(n)
}
#[cfg(test)]
mod tests {
use super::*;
use commonware_utils::{NZU32, TestRng};
fn deal_3_of_4() -> (Sharing<V>, Vec<Share>) {
let mut rng = TestRng::new(0x4242);
trusted_dealer(&mut rng, NZU32!(4))
}
#[test]
fn threshold_3_of_4_keyids_share_one_public_identity() {
let (sharing, shares) = deal_3_of_4();
assert_eq!(threshold_for(4), 3);
let expected_kid = format!(
"{KEYID_PREFIX}{}",
mkit_core::to_hex_bytes(&sharing.public().encode())
);
for s in shares.iter().take(3) {
let signer = ThresholdSigner::new(s.clone(), sharing.clone());
assert_eq!(signer.algorithm(), Algorithm::Bls12381Threshold);
let kid = signer.keyid().expect("keyid");
assert!(kid.starts_with(KEYID_PREFIX));
assert_eq!(kid.len(), KEYID_PREFIX.len() + PUBLIC_KEY_SIZE * 2);
assert_eq!(kid, expected_kid);
}
}
#[test]
fn threshold_3_of_4_aggregate_recovers_verifying_signature() {
let (sharing, shares) = deal_3_of_4();
let pae = b"DSSEv1 28 application/vnd.in-toto+json 12 release v0.2.0";
let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
for s in shares.iter().take(3) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
let bytes = signer.sign(pae).expect("sign partial");
partials_bytes.push(bytes);
}
let agg_sig = aggregate(&sharing, &partials_bytes).expect("aggregate");
assert_eq!(agg_sig.len(), SIGNATURE_SIZE);
let pk = sharing.public().encode().to_vec();
assert_eq!(pk.len(), PUBLIC_KEY_SIZE);
verify(&pk, pae, &agg_sig).expect("aggregated signature verifies");
}
#[test]
fn verify_rejects_wrong_length_signature_before_pairing() {
let (sharing, _shares) = deal_3_of_4();
let pk = sharing.public().encode().to_vec();
let pae = b"DSSEv1 4 test 2 hi";
for len in [0, 1, SIGNATURE_SIZE - 1, SIGNATURE_SIZE + 1, 128] {
let bogus_sig = vec![0u8; len];
assert!(
matches!(
verify(&pk, pae, &bogus_sig),
Err(Error::BlsThresholdSignatureDecode)
),
"signature length {len} (!= {SIGNATURE_SIZE}) must be rejected before pairing"
);
}
}
#[test]
fn verify_rejects_wrong_length_cohort_key_before_pairing() {
let (sharing, shares) = deal_3_of_4();
let pae = b"DSSEv1 4 test 2 hi";
let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
for s in shares.iter().take(3) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
partials_bytes.push(signer.sign(pae).expect("partial"));
}
let agg_sig = aggregate(&sharing, &partials_bytes).expect("aggregate");
for len in [0, 1, PUBLIC_KEY_SIZE - 1, PUBLIC_KEY_SIZE + 1, 256] {
let bogus_pk = vec![0u8; len];
assert!(
matches!(
verify(&bogus_pk, pae, &agg_sig),
Err(Error::BlsThresholdPublicKeyDecode)
),
"public key length {len} (!= {PUBLIC_KEY_SIZE}) must be rejected before pairing"
);
}
}
#[test]
fn namespace_literal_is_pinned() {
assert_eq!(NAMESPACE, b"mkit-attest/dsse/v1");
}
#[test]
fn insufficient_threshold_returns_error() {
let (sharing, shares) = deal_3_of_4();
let pae = b"DSSEv1 4 test 2 hi";
let mut signer = ThresholdSigner::new(shares[0].clone(), sharing.clone());
let only = signer.sign(pae).expect("partial");
let partials = vec![only];
match aggregate(&sharing, &partials) {
Err(Error::BlsThresholdInsufficientPartials) => {}
other => panic!("expected BlsThresholdInsufficientPartials, got {other:?}"),
}
}
#[test]
fn two_of_four_also_insufficient() {
let (sharing, shares) = deal_3_of_4();
let pae = b"DSSEv1 4 test 2 hi";
let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(2);
for s in shares.iter().take(2) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
partials_bytes.push(signer.sign(pae).expect("partial"));
}
assert!(matches!(
aggregate(&sharing, &partials_bytes),
Err(Error::BlsThresholdInsufficientPartials)
));
}
#[test]
fn tampered_partial_fails_aggregate_verify() {
let (sharing, shares) = deal_3_of_4();
let pae = b"DSSEv1 28 application/vnd.in-toto+json 2 {}";
let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
for s in shares.iter().take(3) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
partials_bytes.push(signer.sign(pae).expect("partial"));
}
let last = partials_bytes[1].len() - 1;
partials_bytes[1][last] ^= 0x01;
match aggregate(&sharing, &partials_bytes) {
Err(Error::BlsThresholdPartialDecode) => {
}
Ok(agg_sig) => {
let pk = sharing.public().encode().to_vec();
assert!(
matches!(
verify(&pk, pae, &agg_sig),
Err(Error::BlsThresholdVerifyFailed)
),
"tampered partial must not produce a verifying aggregate"
);
}
other => panic!("unexpected aggregate result on tampered partial: {other:?}"),
}
}
#[test]
fn aggregated_signature_does_not_verify_wrong_message() {
let (sharing, shares) = deal_3_of_4();
let pae_signed = b"DSSEv1 1 a 1 b";
let pae_other = b"DSSEv1 1 a 1 c";
let mut partials_bytes: Vec<Vec<u8>> = Vec::with_capacity(3);
for s in shares.iter().take(3) {
let mut signer = ThresholdSigner::new(s.clone(), sharing.clone());
partials_bytes.push(signer.sign(pae_signed).expect("partial"));
}
let agg_sig = aggregate(&sharing, &partials_bytes).expect("aggregate");
let pk = sharing.public().encode().to_vec();
verify(&pk, pae_signed, &agg_sig).expect("signed message verifies");
match verify(&pk, pae_other, &agg_sig) {
Err(Error::BlsThresholdVerifyFailed) => {}
other => panic!("expected verify failure on wrong message, got {other:?}"),
}
}
#[test]
fn algorithm_enum_round_trips_through_buffa() {
use buffa::Message;
use mkit_rpc::mkit::rpc::v1::Algorithm as RpcAlgorithm;
use mkit_rpc::mkit::rpc::v1::signer::{SignRequest, SignerFrame, signer_frame};
let frame = SignerFrame {
body: Some(signer_frame::Body::SignRequest(Box::new(
SignRequest::default()
.with_algorithm(RpcAlgorithm::Bls12381Threshold)
.with_payload(b"DSSEv1 1 a 1 b".to_vec()),
))),
..Default::default()
};
let bytes = frame.encode_to_vec();
let decoded = SignerFrame::decode(&mut &bytes[..]).expect("decode");
let Some(signer_frame::Body::SignRequest(req)) = decoded.body else {
panic!("expected SignRequest body");
};
assert_eq!(req.algorithm, Some(RpcAlgorithm::Bls12381Threshold.into()),);
assert_eq!(RpcAlgorithm::Bls12381Threshold as i32, 5);
}
#[test]
fn trusted_dealer_is_deterministic_for_a_seeded_rng() {
let mut rng = TestRng::new(0x1357_9bdf);
let (sharing, shares) = trusted_dealer(&mut rng, NZU32!(4));
assert_eq!(shares.len(), 4);
assert_eq!(
mkit_core::to_hex_bytes(&sharing.public().encode()),
"8bdb735ad697430b7ff022196750e30b53c6b6596947c59c18c6f498410cd7\
b3e93f3080b7772033b12fe2a3f3d02b4b08a6b9827d9dcb53be24ff40f5b8e48\
4612fdc8fffa694cc8d4b5234715f58c8845dce9e29b3e5cb2fe1361bca819893"
);
}
}