baseid_bbs/
lib.rs

1//! # baseid-bbs
2//!
3//! BBS+ signatures with unlinkable selective disclosure for BaseID.
4//!
5//! Provides:
6//! - BBS+ key generation (BLS12-381 G2)
7//! - Multi-message signing (each claim is a separate "message")
8//! - Zero-knowledge proof derivation with selective disclosure
9//! - Proof verification (verifier sees only disclosed claims)
10//! - Predicate evaluation (range proofs, set membership)
11//! - Full credential lifecycle (issue, verify, present) via `BbsLifecycle`
12//!
13//! Reference: IETF draft-irtf-cfrg-bbs-signatures
14
15pub mod credential;
16pub mod error;
17pub mod keys;
18pub mod lifecycle;
19pub mod predicates;
20pub mod proof;
21pub mod signing;
22
23pub use credential::{BbsClaim, BbsCredential, BbsDerivedProof};
24pub use keys::BbsKeyPair;
25pub use lifecycle::{verify_derived_proof, BbsLifecycle};
26
27#[cfg(test)]
28mod tests {
29    use super::*;
30    use baseid_core::claims::{ClaimSet, DisclosureSelection, PredicateType};
31    use baseid_core::lifecycle::*;
32    use baseid_core::types::CredentialFormat;
33    use serde_json::json;
34
35    #[test]
36    fn key_generation() {
37        let kp = BbsKeyPair::generate().unwrap();
38        assert!(!kp.public_key.is_empty());
39        assert!(!kp.secret_key.is_empty());
40    }
41
42    #[test]
43    fn key_roundtrip() {
44        let kp = BbsKeyPair::generate().unwrap();
45        let restored = BbsKeyPair::from_bytes(&kp.secret_key, &kp.public_key).unwrap();
46        assert_eq!(kp.public_key, restored.public_key);
47    }
48
49    #[test]
50    fn sign_and_verify() {
51        let kp = BbsKeyPair::generate().unwrap();
52        let messages = vec![
53            b"claim1: Alice".to_vec(),
54            b"claim2: age=30".to_vec(),
55            b"claim3: CA".to_vec(),
56        ];
57
58        let sig = signing::bbs_sign(&kp, &messages, None).unwrap();
59        assert_eq!(sig.len(), 80);
60
61        let valid = signing::bbs_verify(&kp.public_key, &sig, &messages, None).unwrap();
62        assert!(valid, "signature should be valid");
63    }
64
65    #[test]
66    fn verify_wrong_messages_fails() {
67        let kp = BbsKeyPair::generate().unwrap();
68        let messages = vec![b"claim1".to_vec(), b"claim2".to_vec()];
69        let sig = signing::bbs_sign(&kp, &messages, None).unwrap();
70
71        let wrong = vec![b"claim1".to_vec(), b"TAMPERED".to_vec()];
72        let valid = signing::bbs_verify(&kp.public_key, &sig, &wrong, None).unwrap();
73        assert!(!valid, "tampered messages should fail");
74    }
75
76    #[test]
77    fn proof_selective_disclosure() {
78        let kp = BbsKeyPair::generate().unwrap();
79        let messages = vec![
80            b"name: Alice".to_vec(),
81            b"age: 30".to_vec(),
82            b"ssn: 123-45-6789".to_vec(),
83        ];
84        let sig = signing::bbs_sign(&kp, &messages, None).unwrap();
85
86        // Disclose only message 0 (name)
87        let disclosed = [0usize];
88        let proof_bytes =
89            proof::bbs_proof_gen(&kp.public_key, &sig, &messages, &disclosed, None, None).unwrap();
90        assert!(!proof_bytes.is_empty());
91
92        // Verify with only the disclosed message
93        let disclosed_msgs = vec![(0, b"name: Alice".to_vec())];
94        let valid =
95            proof::bbs_proof_verify(&kp.public_key, &proof_bytes, &disclosed_msgs, None, None)
96                .unwrap();
97        assert!(valid, "selective disclosure proof should verify");
98    }
99
100    #[test]
101    fn proof_unlinkability() {
102        let kp = BbsKeyPair::generate().unwrap();
103        let messages = vec![b"claim1".to_vec(), b"claim2".to_vec()];
104        let sig = signing::bbs_sign(&kp, &messages, None).unwrap();
105
106        let proof1 =
107            proof::bbs_proof_gen(&kp.public_key, &sig, &messages, &[0], None, None).unwrap();
108        let proof2 =
109            proof::bbs_proof_gen(&kp.public_key, &sig, &messages, &[0], None, None).unwrap();
110
111        // Two proofs from same credential should be different (unlinkable)
112        assert_ne!(
113            proof1, proof2,
114            "proofs should be unlinkable (different bytes)"
115        );
116
117        // Both should verify
118        let disclosed = vec![(0, b"claim1".to_vec())];
119        assert!(proof::bbs_proof_verify(&kp.public_key, &proof1, &disclosed, None, None).unwrap());
120        assert!(proof::bbs_proof_verify(&kp.public_key, &proof2, &disclosed, None, None).unwrap());
121    }
122
123    #[test]
124    fn lifecycle_issue_verify() {
125        let kp = BbsKeyPair::generate().unwrap();
126        let lifecycle = BbsLifecycle::new(kp);
127
128        let mut claims = ClaimSet::new();
129        claims.insert("", "given_name", json!("Alice"));
130        claims.insert("", "family_name", json!("Smith"));
131        claims.insert("", "age", json!(30));
132
133        let options = IssuanceOptions {
134            credential_id: Some("urn:uuid:test".to_string()),
135            types: vec![
136                "VerifiableCredential".to_string(),
137                "IdentityCredential".to_string(),
138            ],
139            valid_from: None,
140            valid_until: None,
141            status: None,
142        };
143
144        let issued = lifecycle
145            .issue("did:key:issuer", Some("did:key:holder"), &claims, &options)
146            .unwrap();
147        assert_eq!(issued.format, CredentialFormat::Bbs);
148
149        let outcome = lifecycle.verify(&issued.data).unwrap();
150        assert!(outcome.valid, "issued credential should verify");
151        assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("Alice")));
152        assert!(!outcome.unlinkable, "full verification is not unlinkable");
153    }
154
155    #[test]
156    fn lifecycle_present_selective_disclosure() {
157        let kp = BbsKeyPair::generate().unwrap();
158        let lifecycle = BbsLifecycle::new(kp);
159
160        let mut claims = ClaimSet::new();
161        claims.insert("", "given_name", json!("Alice"));
162        claims.insert("", "family_name", json!("Smith"));
163        claims.insert("", "birth_date", json!("1994-01-15"));
164        claims.insert("", "ssn", json!("123-45-6789"));
165
166        let options = IssuanceOptions::default();
167
168        let issued = lifecycle
169            .issue("did:key:issuer", None, &claims, &options)
170            .unwrap();
171
172        // Present: reveal name, hide SSN and birth_date
173        let disclosure = DisclosureSelection::new()
174            .reveal("given_name")
175            .reveal("family_name")
176            .hide("birth_date")
177            .hide("ssn");
178
179        let pres_options = PresentationOptions {
180            nonce: Some("verifier-nonce-123".to_string()),
181            audience: None,
182            holder_did: None,
183        };
184
185        let presented = lifecycle
186            .present(&issued.data, &disclosure, &pres_options)
187            .unwrap();
188        assert_eq!(presented.format, CredentialFormat::Bbs);
189        assert!(presented.unlinkable, "BBS+ presentations are unlinkable");
190
191        // Verify the derived proof
192        let outcome = verify_derived_proof(&presented.data).unwrap();
193        assert!(outcome.valid, "derived proof should verify");
194        assert!(outcome.unlinkable, "derived proof is unlinkable");
195        // Only disclosed claims should be present
196        assert_eq!(outcome.claims.get("", "given_name"), Some(&json!("Alice")));
197        assert_eq!(outcome.claims.get("", "family_name"), Some(&json!("Smith")));
198        assert_eq!(outcome.claims.get("", "ssn"), None, "SSN should be hidden");
199        assert_eq!(
200            outcome.claims.get("", "birth_date"),
201            None,
202            "birth_date should be hidden"
203        );
204    }
205
206    #[test]
207    fn lifecycle_predicate_evaluation() {
208        let kp = BbsKeyPair::generate().unwrap();
209        let lifecycle = BbsLifecycle::new(kp);
210
211        let mut claims = ClaimSet::new();
212        claims.insert("", "name", json!("Alice"));
213        claims.insert("", "age", json!(25));
214        claims.insert("", "country", json!("CA"));
215
216        let options = IssuanceOptions::default();
217        let issued = lifecycle
218            .issue("did:key:issuer", None, &claims, &options)
219            .unwrap();
220
221        // Present with predicate: age > 18
222        let disclosure = DisclosureSelection::new()
223            .reveal("name")
224            .predicate("age", PredicateType::GreaterThan(json!(18)))
225            .hide("country");
226
227        let pres_options = PresentationOptions {
228            nonce: None,
229            audience: None,
230            holder_did: None,
231        };
232        let presented = lifecycle
233            .present(&issued.data, &disclosure, &pres_options)
234            .unwrap();
235
236        let derived = BbsDerivedProof::from_bytes(&presented.data).unwrap();
237        // Check that the predicate was evaluated and satisfied
238        assert_eq!(derived.predicate_results.len(), 1);
239        assert_eq!(derived.predicate_results[0].0, "age");
240        assert!(
241            derived.predicate_results[0].1,
242            "age > 18 should be true for age=25"
243        );
244    }
245
246    #[test]
247    fn predicate_age_over_18() {
248        assert!(predicates::evaluate_predicate(
249            &json!(25),
250            &PredicateType::GreaterThan(json!(18))
251        ));
252        assert!(!predicates::evaluate_predicate(
253            &json!(16),
254            &PredicateType::GreaterThan(json!(18))
255        ));
256        assert!(predicates::evaluate_predicate(
257            &json!(18),
258            &PredicateType::GreaterThanOrEqual(json!(18))
259        ));
260    }
261
262    #[test]
263    fn predicate_set_membership() {
264        let set = vec![json!("CA"), json!("US"), json!("MX")];
265        assert!(predicates::evaluate_predicate(
266            &json!("CA"),
267            &PredicateType::InSet(set.clone())
268        ));
269        assert!(!predicates::evaluate_predicate(
270            &json!("UK"),
271            &PredicateType::InSet(set.clone())
272        ));
273        assert!(predicates::evaluate_predicate(
274            &json!("UK"),
275            &PredicateType::NotInSet(set)
276        ));
277    }
278}