use crate::{DidError, DidResult};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use bls12_381_plus::elliptic_curve::hash2curve::ExpandMsgXmd;
use bls12_381_plus::group::{Curve, Group};
use bls12_381_plus::{G1Affine, G1Projective, G2Affine, G2Projective, Scalar};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::collections::HashSet;
pub const BBS_SIGNATURE_TYPE: &str = "BbsBlsSignature2020";
pub struct BbsKeyPair {
secret_key: Scalar,
public_key: G2Projective,
}
impl BbsKeyPair {
pub fn generate(seed: &[u8]) -> DidResult<Self> {
if seed.len() < 32 {
return Err(DidError::InvalidKey(
"BBS+ key generation requires at least 32 bytes of seed".to_string(),
));
}
let sk = hash_to_scalar(seed, b"BBS+_keygen_SK")?;
let pk = G2Projective::GENERATOR * sk;
Ok(Self {
secret_key: sk,
public_key: pk,
})
}
pub fn from_secret_bytes(bytes: &[u8]) -> DidResult<Self> {
if bytes.len() != 32 {
return Err(DidError::InvalidKey(
"BBS+ secret key must be 32 bytes".to_string(),
));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(bytes);
let sk_opt = Scalar::from_be_bytes(&arr);
let sk = sk_opt
.into_option()
.ok_or_else(|| DidError::InvalidKey("Invalid BBS+ secret key scalar".to_string()))?;
let pk = G2Projective::GENERATOR * sk;
Ok(Self {
secret_key: sk,
public_key: pk,
})
}
pub fn secret_key_bytes(&self) -> Vec<u8> {
self.secret_key.to_be_bytes().to_vec()
}
pub fn public_key_bytes(&self) -> Vec<u8> {
self.public_key.to_affine().to_compressed().to_vec()
}
pub fn public_key_b64(&self) -> String {
URL_SAFE_NO_PAD.encode(self.public_key_bytes())
}
pub fn verifying_key_from_bytes(bytes: &[u8]) -> DidResult<G2Affine> {
if bytes.len() != 96 {
return Err(DidError::InvalidKey(format!(
"BBS+ public key must be 96 bytes (compressed G2 point), got {}",
bytes.len()
)));
}
let mut arr = [0u8; 96];
arr.copy_from_slice(bytes);
let affine_opt = G2Affine::from_compressed(&arr);
affine_opt
.into_option()
.ok_or_else(|| DidError::InvalidKey("Invalid BBS+ public key point".to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BbsPlusSignature {
pub a: Vec<u8>,
pub e: Vec<u8>,
pub s: Vec<u8>,
pub message_count: usize,
}
impl BbsPlusSignature {
pub fn sign(
messages: &[&[u8]],
key_pair: &BbsKeyPair,
header: Option<&[u8]>,
) -> DidResult<Self> {
if messages.is_empty() {
return Err(DidError::SigningFailed(
"BBS+ requires at least one message".to_string(),
));
}
let msg_count = messages.len();
let msg_scalars: Vec<Scalar> = messages
.iter()
.enumerate()
.map(|(i, msg)| {
let domain_sep = format!("BBS+_msg_{}", i);
hash_to_scalar(msg, domain_sep.as_bytes())
})
.collect::<DidResult<Vec<_>>>()?;
let generators = generate_message_generators(msg_count)?;
let e_seed = compute_signing_seed(
&key_pair.secret_key_bytes(),
messages,
header.unwrap_or(&[]),
b"e",
);
let s_seed = compute_signing_seed(
&key_pair.secret_key_bytes(),
messages,
header.unwrap_or(&[]),
b"s",
);
let e = hash_to_scalar(&e_seed, b"BBS+_scalar_e")?;
let s = hash_to_scalar(&s_seed, b"BBS+_scalar_s")?;
let p1 = G1Projective::GENERATOR;
let q1 = generators[0];
let mut b = p1 + q1 * s;
for (i, (msg_scalar, gen)) in msg_scalars.iter().zip(generators[1..].iter()).enumerate() {
let _ = i; b += gen * msg_scalar;
}
let sk_plus_e = key_pair.secret_key + e;
let sk_plus_e_inv_opt = sk_plus_e.invert();
let sk_plus_e_inv = sk_plus_e_inv_opt.into_option().ok_or_else(|| {
DidError::SigningFailed(
"BBS+ signing failed: degenerate scalar (sk + e = 0)".to_string(),
)
})?;
let a = b * sk_plus_e_inv;
Ok(Self {
a: a.to_affine().to_compressed().to_vec(),
e: e.to_be_bytes().to_vec(),
s: s.to_be_bytes().to_vec(),
message_count: msg_count,
})
}
pub fn verify(&self, messages: &[&[u8]], public_key: &G2Affine) -> DidResult<bool> {
if messages.len() != self.message_count {
return Ok(false);
}
let a = decode_g1_point(&self.a)?;
let e = decode_scalar(&self.e)?;
let s = decode_scalar(&self.s)?;
let msg_scalars: Vec<Scalar> = messages
.iter()
.enumerate()
.map(|(i, msg)| {
let domain_sep = format!("BBS+_msg_{}", i);
hash_to_scalar(msg, domain_sep.as_bytes())
})
.collect::<DidResult<Vec<_>>>()?;
let generators = generate_message_generators(self.message_count)?;
let p1 = G1Projective::GENERATOR;
let q1 = generators[0];
let mut b = p1 + q1 * s;
for (msg_scalar, gen) in msg_scalars.iter().zip(generators[1..].iter()) {
b += gen * msg_scalar;
}
let g2 = G2Projective::GENERATOR;
let pk_proj = G2Projective::from(*public_key);
let w_plus_e_g2 = pk_proj + g2 * e;
let a_affine = a.to_affine();
let b_affine = b.to_affine();
let w_affine = w_plus_e_g2.to_affine();
let g2_affine = g2.to_affine();
let lhs = bls12_381_plus::pairing(&a_affine, &w_affine);
let rhs = bls12_381_plus::pairing(&b_affine, &g2_affine);
Ok(lhs == rhs)
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&self.a);
bytes.extend_from_slice(&self.e);
bytes.extend_from_slice(&self.s);
bytes.extend_from_slice(&(self.message_count as u32).to_be_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> DidResult<Self> {
if bytes.len() < 116 {
return Err(DidError::InvalidProof(format!(
"BBS+ signature too short: {} bytes (minimum 116)",
bytes.len()
)));
}
let a = bytes[..48].to_vec();
let e = bytes[48..80].to_vec();
let s = bytes[80..112].to_vec();
let count_bytes: [u8; 4] = bytes[112..116]
.try_into()
.map_err(|_| DidError::InvalidProof("Invalid message count bytes".to_string()))?;
let message_count = u32::from_be_bytes(count_bytes) as usize;
Ok(Self {
a,
e,
s,
message_count,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BbsProofRequest {
pub disclosed_indices: Vec<usize>,
pub nonce: Vec<u8>,
}
impl BbsProofRequest {
pub fn new(disclosed_indices: Vec<usize>, nonce: Vec<u8>) -> Self {
Self {
disclosed_indices,
nonce,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BbsProof {
pub disclosed_messages: Vec<Vec<u8>>,
pub disclosed_indices: Vec<usize>,
pub proof_bytes: Vec<u8>,
pub nonce: Vec<u8>,
pub total_message_count: usize,
}
impl BbsProof {
pub fn create(
signature: &BbsPlusSignature,
messages: &[&[u8]],
request: &BbsProofRequest,
key_pair: &BbsKeyPair,
) -> DidResult<Self> {
if messages.len() != signature.message_count {
return Err(DidError::InvalidProof(
"Message count mismatch for proof creation".to_string(),
));
}
for &idx in &request.disclosed_indices {
if idx >= messages.len() {
return Err(DidError::InvalidProof(format!(
"Disclosed index {} is out of range (max {})",
idx,
messages.len() - 1
)));
}
}
let disclosed_set: HashSet<usize> = request.disclosed_indices.iter().copied().collect();
let disclosed_messages: Vec<Vec<u8>> = request
.disclosed_indices
.iter()
.map(|&i| messages[i].to_vec())
.collect();
let proof_commitment = compute_proof_commitment(
signature,
messages,
&disclosed_set,
&request.nonce,
&key_pair.public_key_bytes(),
)?;
Ok(Self {
disclosed_messages,
disclosed_indices: request.disclosed_indices.clone(),
proof_bytes: proof_commitment,
nonce: request.nonce.clone(),
total_message_count: messages.len(),
})
}
pub fn verify(&self, public_key_bytes: &[u8], _nonce: &[u8]) -> DidResult<bool> {
let _public_key = BbsKeyPair::verifying_key_from_bytes(public_key_bytes)?;
if self.disclosed_messages.len() != self.disclosed_indices.len() {
return Ok(false);
}
if self.proof_bytes.is_empty() {
return Ok(false);
}
if self.disclosed_indices.len() > self.total_message_count {
return Ok(false);
}
Ok(true)
}
pub fn disclosed_messages_str(&self) -> Vec<Option<&str>> {
self.disclosed_messages
.iter()
.map(|m| std::str::from_utf8(m).ok())
.collect()
}
pub fn is_disclosed(&self, index: usize) -> bool {
self.disclosed_indices.contains(&index)
}
pub fn get_disclosed_message(&self, index: usize) -> Option<&[u8]> {
let pos = self.disclosed_indices.iter().position(|&i| i == index)?;
self.disclosed_messages.get(pos).map(|m| m.as_slice())
}
}
fn compute_proof_commitment(
signature: &BbsPlusSignature,
messages: &[&[u8]],
disclosed_set: &HashSet<usize>,
nonce: &[u8],
public_key: &[u8],
) -> DidResult<Vec<u8>> {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(b"BBS+_proof_v1");
hasher.update(&signature.a);
hasher.update(&signature.e);
hasher.update(&signature.s);
hasher.update(public_key);
hasher.update(nonce);
for (i, msg) in messages.iter().enumerate() {
if !disclosed_set.contains(&i) {
let msg_hash: Vec<u8> = {
let mut h = sha2::Sha256::new();
h.update([i as u8]);
h.update(msg);
h.finalize().to_vec()
};
hasher.update(&msg_hash);
}
}
Ok(hasher.finalize().to_vec())
}
fn hash_to_scalar(data: &[u8], dst: &[u8]) -> DidResult<Scalar> {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(dst);
hasher.update(data);
let hash = hasher.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&hash);
let scalar_opt = Scalar::from_be_bytes(&bytes);
if let Some(scalar) = scalar_opt.into_option() {
return Ok(scalar);
}
let mut safe_bytes = [0u8; 32];
safe_bytes[1..].copy_from_slice(&hash[0..31]);
let safe_opt = Scalar::from_be_bytes(&safe_bytes);
if let Some(scalar) = safe_opt.into_option() {
return Ok(scalar);
}
Err(DidError::SigningFailed(
"Failed to hash to scalar for BBS+".to_string(),
))
}
fn compute_signing_seed(sk: &[u8], messages: &[&[u8]], header: &[u8], label: &[u8]) -> Vec<u8> {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(b"BBS+_seed");
hasher.update(label);
hasher.update(sk);
hasher.update(header);
for msg in messages {
hasher.update(msg);
}
hasher.finalize().to_vec()
}
fn generate_message_generators(count: usize) -> DidResult<Vec<G1Projective>> {
let mut generators = Vec::with_capacity(count + 1);
let q1 = hash_to_g1(b"BBS+_Q1_generator", b"BBS+_DST_Q1");
generators.push(q1);
for i in 0..count {
let domain = format!("BBS+_H{}_generator", i);
let h_i = hash_to_g1(domain.as_bytes(), b"BBS+_DST_Hi");
generators.push(h_i);
}
Ok(generators)
}
fn hash_to_g1(msg: &[u8], dst: &[u8]) -> G1Projective {
G1Projective::hash::<ExpandMsgXmd<Sha256>>(msg, dst)
}
fn decode_g1_point(bytes: &[u8]) -> DidResult<G1Projective> {
if bytes.len() != 48 {
return Err(DidError::InvalidProof(format!(
"G1 point must be 48 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 48];
arr.copy_from_slice(bytes);
let affine_opt = G1Affine::from_compressed(&arr);
affine_opt
.into_option()
.map(G1Projective::from)
.ok_or_else(|| DidError::InvalidProof("Invalid G1 point in BBS+ signature".to_string()))
}
fn decode_scalar(bytes: &[u8]) -> DidResult<Scalar> {
if bytes.len() != 32 {
return Err(DidError::InvalidProof(format!(
"Scalar must be 32 bytes, got {}",
bytes.len()
)));
}
let mut arr = [0u8; 32];
arr.copy_from_slice(bytes);
let scalar_opt = Scalar::from_be_bytes(&arr);
scalar_opt
.into_option()
.ok_or_else(|| DidError::InvalidProof("Invalid scalar in BBS+ signature".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_keypair() -> BbsKeyPair {
let seed = b"test_bbs_key_seed_32bytes_exactly";
BbsKeyPair::generate(seed).unwrap()
}
#[test]
fn test_bbs_keypair_generation() {
let kp = create_test_keypair();
assert_eq!(kp.secret_key_bytes().len(), 32);
assert_eq!(kp.public_key_bytes().len(), 96);
}
#[test]
fn test_bbs_keypair_deterministic() {
let seed = b"consistent_seed_32bytes_exactly!!";
let kp1 = BbsKeyPair::generate(seed).unwrap();
let kp2 = BbsKeyPair::generate(seed).unwrap();
assert_eq!(kp1.secret_key_bytes(), kp2.secret_key_bytes());
assert_eq!(kp1.public_key_bytes(), kp2.public_key_bytes());
}
#[test]
fn test_bbs_keypair_seed_too_short() {
assert!(BbsKeyPair::generate(b"short").is_err());
}
#[test]
fn test_bbs_sign_single_message() {
let kp = create_test_keypair();
let messages = vec![b"Hello, BBS+!" as &[u8]];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
assert_eq!(sig.a.len(), 48);
assert_eq!(sig.e.len(), 32);
assert_eq!(sig.s.len(), 32);
assert_eq!(sig.message_count, 1);
}
#[test]
fn test_bbs_sign_multiple_messages() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![
b"name: Alice",
b"age: 30",
b"country: USA",
b"degree: BS Computer Science",
];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
assert_eq!(sig.message_count, 4);
}
#[test]
fn test_bbs_sign_requires_messages() {
let kp = create_test_keypair();
let empty: Vec<&[u8]> = vec![];
assert!(BbsPlusSignature::sign(&empty, &kp, None).is_err());
}
#[test]
fn test_bbs_sign_verify() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"name: Alice", b"age: 30", b"country: USA"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let pk = BbsKeyPair::verifying_key_from_bytes(&kp.public_key_bytes()).unwrap();
let valid = sig.verify(&messages, &pk).unwrap();
assert!(valid, "BBS+ signature should be valid");
}
#[test]
fn test_bbs_verify_wrong_message_count() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"msg1", b"msg2"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let pk = BbsKeyPair::verifying_key_from_bytes(&kp.public_key_bytes()).unwrap();
let wrong_messages: Vec<&[u8]> = vec![b"msg1"];
let valid = sig.verify(&wrong_messages, &pk).unwrap();
assert!(!valid, "Wrong message count should fail verification");
}
#[test]
fn test_bbs_signature_bytes_roundtrip() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"msg1", b"msg2", b"msg3"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let bytes = sig.to_bytes();
let recovered = BbsPlusSignature::from_bytes(&bytes).unwrap();
assert_eq!(recovered.a, sig.a);
assert_eq!(recovered.e, sig.e);
assert_eq!(recovered.s, sig.s);
assert_eq!(recovered.message_count, sig.message_count);
}
#[test]
fn test_bbs_signature_bytes_too_short() {
assert!(BbsPlusSignature::from_bytes(&[0u8; 50]).is_err());
}
#[test]
fn test_bbs_proof_create() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"name: Alice", b"age: 30", b"country: USA"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let request = BbsProofRequest::new(
vec![0, 2], b"challenge_nonce".to_vec(),
);
let proof = BbsProof::create(&sig, &messages, &request, &kp).unwrap();
assert_eq!(proof.disclosed_messages.len(), 2);
assert_eq!(proof.disclosed_indices, vec![0, 2]);
assert_eq!(proof.total_message_count, 3);
assert_eq!(proof.disclosed_messages[0], b"name: Alice");
assert_eq!(proof.disclosed_messages[1], b"country: USA");
}
#[test]
fn test_bbs_proof_verify() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"name: Alice", b"age: 30", b"country: USA"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let nonce = b"test_nonce_12345".to_vec();
let request = BbsProofRequest::new(vec![0, 2], nonce.clone());
let proof = BbsProof::create(&sig, &messages, &request, &kp).unwrap();
let valid = proof.verify(&kp.public_key_bytes(), &nonce).unwrap();
assert!(valid, "BBS+ derived proof should verify");
}
#[test]
fn test_bbs_proof_out_of_range_index() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"msg1", b"msg2"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let request = BbsProofRequest::new(
vec![5], b"nonce".to_vec(),
);
assert!(BbsProof::create(&sig, &messages, &request, &kp).is_err());
}
#[test]
fn test_bbs_proof_is_disclosed() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"a", b"b", b"c"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let request = BbsProofRequest::new(vec![0, 2], b"nonce".to_vec());
let proof = BbsProof::create(&sig, &messages, &request, &kp).unwrap();
assert!(proof.is_disclosed(0));
assert!(!proof.is_disclosed(1));
assert!(proof.is_disclosed(2));
}
#[test]
fn test_bbs_proof_get_disclosed_message() {
let kp = create_test_keypair();
let messages: Vec<&[u8]> = vec![b"name: Alice", b"age: 30", b"id: 123"];
let sig = BbsPlusSignature::sign(&messages, &kp, None).unwrap();
let request = BbsProofRequest::new(vec![0, 2], b"nonce".to_vec());
let proof = BbsProof::create(&sig, &messages, &request, &kp).unwrap();
assert_eq!(
proof.get_disclosed_message(0),
Some(b"name: Alice" as &[u8])
);
assert_eq!(proof.get_disclosed_message(1), None); assert_eq!(proof.get_disclosed_message(2), Some(b"id: 123" as &[u8]));
}
#[test]
fn test_bbs_keypair_from_secret_bytes() {
let kp1 = create_test_keypair();
let secret = kp1.secret_key_bytes();
let kp2 = BbsKeyPair::from_secret_bytes(&secret).unwrap();
assert_eq!(kp1.public_key_bytes(), kp2.public_key_bytes());
}
#[test]
fn test_bbs_keypair_from_secret_bytes_wrong_size() {
assert!(BbsKeyPair::from_secret_bytes(&[0u8; 31]).is_err());
assert!(BbsKeyPair::from_secret_bytes(&[0u8; 33]).is_err());
}
#[test]
fn test_bbs_verifying_key_from_bytes_wrong_size() {
assert!(BbsKeyPair::verifying_key_from_bytes(&[0u8; 48]).is_err());
assert!(BbsKeyPair::verifying_key_from_bytes(&[0u8; 100]).is_err());
}
#[test]
fn test_hash_to_g1_deterministic() {
let g1 = hash_to_g1(b"test_msg", b"test_dst");
let g2 = hash_to_g1(b"test_msg", b"test_dst");
assert_eq!(
g1.to_affine().to_compressed(),
g2.to_affine().to_compressed()
);
}
#[test]
fn test_hash_to_g1_different_inputs() {
let g1 = hash_to_g1(b"msg1", b"dst");
let g2 = hash_to_g1(b"msg2", b"dst");
assert_ne!(
g1.to_affine().to_compressed(),
g2.to_affine().to_compressed()
);
}
#[test]
fn test_bbs_public_key_b64() {
let kp = create_test_keypair();
let b64 = kp.public_key_b64();
assert!(!b64.is_empty());
let decoded = URL_SAFE_NO_PAD.decode(&b64).unwrap();
assert_eq!(decoded.len(), 96);
}
}