open-standards/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/proof.rs
Transaction Science 6575ff1eb7 Ingest batch 3: 9 more crates across openpay / smart-byte / eoc
openpay/ (+3 crates)
  op-bnpl/        Affirm + Klarna + Afterpay/Clearpay rails. Full lifecycle
                  typestate (Initiated→Approved→Captured→Settled→Refunded).
                  Per-provider sandbox+production base URLs, regions (NA/EU/OC
                  for Klarna; US/AuNz/Gb/Ca/Eu for Afterpay). HMAC-SHA256
                  webhook verification with constant-time compare. 61 tests.
  op-batch/       NACHA ACH (94-char records, PPD/CCD/WEB/TEL/CTX, R01-R99
                  return codes, Same-Day ACH cutoffs); SEPA pain.001.001.09
                  (CT) + pain.008.001.08 (DD) with SCT-Inst flagging; Fedwire
                  MT103/MT202/CHIPS + pacs.008.001.12 / pacs.009.001.10 +
                  UETR; Bacs Standard 18. Per-rail cutoff scheduler. Exception
                  taxonomy unified across rails. 51 tests.
  op-screening/   OFAC SDN + EU consolidated + UN consolidated + HMT UK
                  consolidated + Australia DFAT + Canada SEMA + Japan MOF.
                  Levenshtein + Jaro-Winkler + Soundex + Metaphone + token
                  Jaccard scoring. Unicode NFKD normalisation + Cyrillic /
                  Arabic transliteration. Ed25519-signed BLAKE3-chained
                  audit log. BSA helpers: CTR + SAR pattern detection
                  (structuring, round-dollar, rapid-in-out). 43 tests.

smart-byte/smart-byte-rs/ (+3 crates)
  smart-byte-bbs/      BBS+ signatures (IETF draft-irtf-cfrg-bbs-signatures-08).
                       BLS12-381 keys, signatures, disclosure proofs with
                       cryptographic unlinkability. bbs-2023 Data Integrity
                       cryptosuite. AnonCreds-shaped link secrets +
                       predicate proofs. SecretKey zeroize-on-drop. 49 tests.
  smart-byte-didcomm/  DIDComm v2 messaging per DIF spec. Pack-encrypted
                       (ECDH-1PU + ECDH-ES via X25519), pack-signed (JWS),
                       pack-plaintext. Aries protocols: issue-credential v3,
                       present-proof v3, basic-message, trust-ping, discover-
                       features, out-of-band, coordinate-mediation, message-
                       pickup. Mediator role with pluggable storage. 23 tests.
  smart-byte-mdl/      ISO/IEC 18013-5 mobile driver's licence + 18013-7
                       online presentment. mDoc + MSO with salted-hash
                       selective disclosure (per item; SHA-256 digest tree).
                       org.iso.18013.5.1 namespace + AAMVA add-on. ES256
                       device-auth + issuer-auth. QR-code device-engagement
                       scaffolding. 28 tests.

eoc/eoc-rs/ (+3 crates)
  eoc-local/      Local-inference backends behind one NeuralBackend:
                  llama.cpp (GGUF, Q4/Q5/Q6/Q8/F16/F32 quantisation),
                  MLX (Apple Silicon, feature-gated), MLC-LLM (TVM, feature-
                  gated marker), ONNX Runtime. GGUF file-format reader.
                  HuggingFace tokenizers. Greedy / top-k / top-p / temperature /
                  Mirostat samplers. Model registry scanning ~/.eoc/models/.
                  Joule attribution via eoc-meter. 39 tests.
  eoc-rerank/     BM25 sparse + HNSW dense + RRF + min-max hybrid fusion.
                  Cohere v3.5 + Voyage v2 + bge-reranker v2-m3 + ms-marco
                  MiniLM + ColBERT v2 rerankers. Two-stage pipeline:
                  retrieve top-K hybrid → cross-encoder rerank → top-N.
                  KV-stage recovery shim into the cascade. 31 tests.
  eoc-eval/       15 canonical eval harnesses: MMLU, MMLU-Pro, GPQA,
                  HumanEval (pass@1 with python3 sandbox), BBH, IFEval (25
                  verifiable constraints), AlpacaEval, AGIEval, HellaSwag,
                  ARC (Easy + Challenge), TruthfulQA (MC1+MC2), BoolQ,
                  GSM8K, MATH (boxed-answer equivalence), Winogrande. Real
                  questions in BuiltinSamples. EvalRunner emits joules-per-
                  correct alongside accuracy. 49 tests.

Quality:
- cargo check across all three workspaces: clean
- cargo clippy --all-targets -- -D warnings on every new crate: clean
- 374 new tests, all passing
- #![forbid(unsafe_code)] across every new crate
- No unwrap/expect outside #[cfg(test)]

Rust 1.95.0 edition 2024 (inherited via root rust-toolchain.toml).
2026-05-24 01:09:35 -04:00

718 lines
23 KiB
Rust

//! BBS+ selective-disclosure proof of knowledge.
//!
//! Reference: `draft-irtf-cfrg-bbs-signatures-08` § 3.7 (`ProofGen`)
//! and § 3.8 (`ProofVerify`); Au, Susilo, Mu, "Constant-Size Dynamic
//! k-TAA" (CDL/BBS+); and Camenisch, Drijvers, Lehmann, "Anonymous
//! Attestation Using the Strong Diffie Hellman Assumption Revisited"
//! (the modern BBS+ proof of knowledge).
//!
//! ## Construction
//!
//! Given a signature `(A, e)` on messages `(m_1, ..., m_n)` with
//! generators `(H_0, H_1, ..., H_n)`, the holder proves
//!
//! ```text
//! "I know (e, m_{undisclosed}) and randomness (r1, r2, r3) such that
//! e(A', W) == e(A_bar, g2)
//! AND D == B * r1 + H_0 * r2
//! AND A_bar - D == A' * (-e) + H_0 * (-s') - sum_{i in U} H_i * m_i"
//! ```
//!
//! where:
//!
//! * `A' = A * r1` — randomised signature commitment.
//! * `A_bar = B * r1 - A' * e` — companion in the pairing identity.
//! * `D = B * r1 + H_0 * r2` — Pedersen commitment to `B * r1` with
//! additional blinding `r2`.
//! * `s' = r2 / r1` — the "scalar substitution" that links the two
//! relations.
//!
//! The Fiat-Shamir transcript hashes `(A', A_bar, D, T1, T2, public
//! key, nonce, disclosed messages)` to derive the challenge `c`. The
//! responses `(e_hat, r2_hat, r3_hat, s_hat, undisclosed_responses)`
//! are openings of the corresponding witnesses.
//!
//! Verifier reconstructs the commitments using the responses and the
//! recomputed challenge and accepts iff the recomputed challenge
//! matches `c` *and* the pairing identity holds. Two independent
//! presentations of the *same* signature randomise `r1, r2` afresh
//! and so produce different `(A', A_bar, D)` and different challenges
//! — this is the unlinkability property.
use std::collections::{BTreeMap, BTreeSet};
use bls12_381::{
G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Scalar,
multi_miller_loop,
};
use ff::Field;
use group::Group;
use rand::{CryptoRng, RngCore};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use subtle::ConstantTimeEq;
use crate::encode::{
DST_FIAT_SHAMIR, g1_to_bytes, g2_to_bytes, scalar_to_bytes,
};
use crate::error::BbsError;
use crate::keys::PublicKey;
use crate::sign::{Signature, compute_b};
/// A BBS+ selective-disclosure proof.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DisclosureProof {
/// `A' = A * r1`.
pub a_prime: G1Affine,
/// `A_bar = B * r1 - A' * e`.
pub a_bar: G1Affine,
/// `D = B * r1 + H_0 * r2`.
pub d: G1Affine,
/// Fiat-Shamir challenge.
pub c: Scalar,
/// Response for the `e` witness.
pub e_hat: Scalar,
/// Response for the `r2` witness.
pub r2_hat: Scalar,
/// Response for the `r3 = 1/r1` witness.
pub r3_hat: Scalar,
/// Response for the `s' = r2 * r3` witness.
pub s_hat: Scalar,
/// Messages the holder chose to disclose, keyed by their position
/// in the original message vector.
pub disclosed_messages: BTreeMap<usize, Scalar>,
/// Schnorr responses for each undisclosed message, in ascending
/// index order over the undisclosed positions.
pub undisclosed_responses: Vec<Scalar>,
}
/// Serde shadow with `Vec<u8>` placeholders for the curve types.
#[derive(Serialize, Deserialize)]
struct DisclosureProofShadow {
#[serde(with = "serde_bytes")]
a_prime: Vec<u8>,
#[serde(with = "serde_bytes")]
a_bar: Vec<u8>,
#[serde(with = "serde_bytes")]
d: Vec<u8>,
#[serde(with = "serde_bytes")]
c: Vec<u8>,
#[serde(with = "serde_bytes")]
e_hat: Vec<u8>,
#[serde(with = "serde_bytes")]
r2_hat: Vec<u8>,
#[serde(with = "serde_bytes")]
r3_hat: Vec<u8>,
#[serde(with = "serde_bytes")]
s_hat: Vec<u8>,
disclosed_messages: Vec<(u64, serde_bytes::ByteBuf)>,
undisclosed_responses: Vec<serde_bytes::ByteBuf>,
}
impl Serialize for DisclosureProof {
fn serialize<S: serde::Serializer>(
&self,
s: S,
) -> Result<S::Ok, S::Error> {
let shadow = DisclosureProofShadow {
a_prime: g1_to_bytes(&self.a_prime).to_vec(),
a_bar: g1_to_bytes(&self.a_bar).to_vec(),
d: g1_to_bytes(&self.d).to_vec(),
c: scalar_to_bytes(&self.c).to_vec(),
e_hat: scalar_to_bytes(&self.e_hat).to_vec(),
r2_hat: scalar_to_bytes(&self.r2_hat).to_vec(),
r3_hat: scalar_to_bytes(&self.r3_hat).to_vec(),
s_hat: scalar_to_bytes(&self.s_hat).to_vec(),
disclosed_messages: self
.disclosed_messages
.iter()
.map(|(k, v)| {
(
*k as u64,
serde_bytes::ByteBuf::from(scalar_to_bytes(v).to_vec()),
)
})
.collect(),
undisclosed_responses: self
.undisclosed_responses
.iter()
.map(|v| serde_bytes::ByteBuf::from(scalar_to_bytes(v).to_vec()))
.collect(),
};
shadow.serialize(s)
}
}
fn fixed_g1(bytes: &[u8]) -> Result<G1Affine, String> {
if bytes.len() != 48 {
return Err(format!("expected 48 G1 bytes, got {}", bytes.len()));
}
let mut b = [0u8; 48];
b.copy_from_slice(bytes);
crate::encode::g1_from_bytes(&b).map_err(|e| e.to_string())
}
fn fixed_scalar(bytes: &[u8]) -> Result<Scalar, String> {
if bytes.len() != 32 {
return Err(format!("expected 32 scalar bytes, got {}", bytes.len()));
}
let mut b = [0u8; 32];
b.copy_from_slice(bytes);
crate::encode::scalar_from_bytes(&b).map_err(|e| e.to_string())
}
impl<'de> Deserialize<'de> for DisclosureProof {
fn deserialize<D: serde::Deserializer<'de>>(
d: D,
) -> Result<Self, D::Error> {
let shadow = DisclosureProofShadow::deserialize(d)?;
let mut disclosed_messages = BTreeMap::new();
for (k, v) in shadow.disclosed_messages {
let scalar = fixed_scalar(&v).map_err(serde::de::Error::custom)?;
disclosed_messages.insert(k as usize, scalar);
}
let mut undisclosed_responses = Vec::with_capacity(
shadow.undisclosed_responses.len(),
);
for v in shadow.undisclosed_responses {
undisclosed_responses
.push(fixed_scalar(&v).map_err(serde::de::Error::custom)?);
}
Ok(DisclosureProof {
a_prime: fixed_g1(&shadow.a_prime)
.map_err(serde::de::Error::custom)?,
a_bar: fixed_g1(&shadow.a_bar)
.map_err(serde::de::Error::custom)?,
d: fixed_g1(&shadow.d).map_err(serde::de::Error::custom)?,
c: fixed_scalar(&shadow.c).map_err(serde::de::Error::custom)?,
e_hat: fixed_scalar(&shadow.e_hat)
.map_err(serde::de::Error::custom)?,
r2_hat: fixed_scalar(&shadow.r2_hat)
.map_err(serde::de::Error::custom)?,
r3_hat: fixed_scalar(&shadow.r3_hat)
.map_err(serde::de::Error::custom)?,
s_hat: fixed_scalar(&shadow.s_hat)
.map_err(serde::de::Error::custom)?,
disclosed_messages,
undisclosed_responses,
})
}
}
fn random_nonzero_scalar<R: RngCore + CryptoRng>(rng: &mut R) -> Scalar {
loop {
let mut bytes = [0u8; 64];
rng.fill_bytes(&mut bytes);
let s = Scalar::from_bytes_wide(&bytes);
if !bool::from(s.ct_eq(&Scalar::ZERO)) {
return s;
}
}
}
fn disclosed_partition(
n: usize,
disclosed: &[usize],
) -> Result<(BTreeSet<usize>, Vec<usize>), BbsError> {
let mut set = BTreeSet::new();
for &i in disclosed {
if i >= n {
return Err(BbsError::DisclosedIndexOutOfRange { index: i, len: n });
}
if !set.insert(i) {
return Err(BbsError::DisclosedIndexDuplicate(i));
}
}
let undisclosed: Vec<usize> = (0..n).filter(|i| !set.contains(i)).collect();
Ok((set, undisclosed))
}
/// Fiat-Shamir transcript over commitments + context. Hashes to a
/// scalar via the project-wide `hash_to_scalar` reduction (SHA-512
/// reduce) seeded with a SHA-256 fingerprint of the byte transcript.
#[allow(clippy::too_many_arguments)]
fn challenge_scalar(
a_prime: &G1Affine,
a_bar: &G1Affine,
d: &G1Affine,
t1: &G1Affine,
t2: &G1Affine,
public_key: &PublicKey,
disclosed: &BTreeMap<usize, Scalar>,
nonce: &[u8],
) -> Scalar {
let mut h = Sha256::new();
h.update(DST_FIAT_SHAMIR);
h.update(g1_to_bytes(a_prime));
h.update(g1_to_bytes(a_bar));
h.update(g1_to_bytes(d));
h.update(g1_to_bytes(t1));
h.update(g1_to_bytes(t2));
h.update(g2_to_bytes(&public_key.w));
h.update((disclosed.len() as u64).to_be_bytes());
for (idx, m) in disclosed {
h.update((*idx as u64).to_be_bytes());
h.update(scalar_to_bytes(m));
}
h.update((nonce.len() as u64).to_be_bytes());
h.update(nonce);
let digest = h.finalize();
crate::encode::hash_to_scalar(DST_FIAT_SHAMIR, &digest)
}
/// Create a selective-disclosure proof from a BBS+ signature.
///
/// * `signature` — the signer's `(A, e)`.
/// * `public_key` — issuer's BBS+ public key.
/// * `messages` — the *full* message vector that was originally
/// signed.
/// * `disclosed_indices` — positions in `messages` the holder is
/// willing to reveal.
/// * `nonce` — verifier-supplied freshness binding.
/// * `generators` — the same generator vector used by the signer.
/// * `rng` — cryptographic randomness.
pub fn create_proof<R: RngCore + CryptoRng>(
signature: &Signature,
public_key: &PublicKey,
messages: &[Scalar],
disclosed_indices: &[usize],
nonce: &[u8],
generators: &[G1Projective],
rng: &mut R,
) -> Result<DisclosureProof, BbsError> {
let n = messages.len();
if generators.len() < n + 1 {
return Err(BbsError::GeneratorCount {
have: generators.len(),
need: n + 1,
});
}
let (disclosed_set, undisclosed) = disclosed_partition(n, disclosed_indices)?;
// Build the commitment B that the signer used.
let b = compute_b(messages, generators)?;
let h0 = generators[0];
// Random blinding scalars. `r1` is drawn nonzero so its inverse
// exists; we still surface the negligible-probability failure
// rather than panic.
let r1 = random_nonzero_scalar(rng);
let r2 = random_nonzero_scalar(rng);
let Some(r3) = Option::<Scalar>::from(r1.invert()) else {
return Err(BbsError::ProofVerification(
"r1 inversion failed (impossible: r1 is nonzero)".into(),
));
};
let s_prime = r2 * r3; // r2 / r1
// Public proof commitments.
// A' = A * r1
// A_bar = B*r1 - A'*e (so e(A', W) == e(A_bar, g2))
// D = B*r1 + H_0*r2
let a_prime_p = G1Projective::from(signature.a) * r1;
let a_prime = G1Affine::from(a_prime_p);
let a_bar_p = b * r1 - a_prime_p * signature.e;
let a_bar = G1Affine::from(a_bar_p);
let d_p = b * r1 + h0 * r2;
let d = G1Affine::from(d_p);
// Schnorr blinding values.
let e_tilde = random_nonzero_scalar(rng);
let r2_tilde = random_nonzero_scalar(rng);
let r3_tilde = random_nonzero_scalar(rng);
let s_tilde = random_nonzero_scalar(rng);
let m_tildes: Vec<Scalar> = (0..undisclosed.len())
.map(|_| random_nonzero_scalar(rng))
.collect();
// Relation 1 (witness `e, r2`):
// A_bar - D == A'*(-e) + H_0*(-r2)
// Blinded T1:
// T1 = A'*(-e_tilde) + H_0*(-r2_tilde)
let mut t1_p = G1Projective::identity();
t1_p += a_prime_p * (-e_tilde);
t1_p += h0 * (-r2_tilde);
let t1 = G1Affine::from(t1_p);
// Relation 2 (witness `r3, s', m_i for i in undisclosed`):
// H_0 + sum_{i in D} H_{i+1}*m_i ==
// D*r3 - H_0*s' - sum_{i in U} H_{i+1}*m_i
// Blinded T2:
// T2 = D*r3_tilde - H_0*s_tilde - sum_{i in U} H_{i+1}*m_tilde_i
let mut t2_p = G1Projective::identity();
t2_p += d_p * r3_tilde;
t2_p -= h0 * s_tilde;
for (slot, &idx) in undisclosed.iter().enumerate() {
t2_p -= generators[idx + 1] * m_tildes[slot];
}
let t2 = G1Affine::from(t2_p);
// Fiat-Shamir challenge.
let disclosed_messages: BTreeMap<usize, Scalar> = disclosed_set
.iter()
.map(|&i| (i, messages[i]))
.collect();
let c = challenge_scalar(
&a_prime,
&a_bar,
&d,
&t1,
&t2,
public_key,
&disclosed_messages,
nonce,
);
// Schnorr responses: response = tilde + c * witness
let e_hat = e_tilde + c * signature.e;
let r2_hat = r2_tilde + c * r2;
let r3_hat = r3_tilde + c * r3;
let s_hat = s_tilde + c * s_prime;
let undisclosed_responses: Vec<Scalar> = undisclosed
.iter()
.zip(m_tildes.iter())
.map(|(&idx, &m_tilde)| m_tilde + c * messages[idx])
.collect();
Ok(DisclosureProof {
a_prime,
a_bar,
d,
c,
e_hat,
r2_hat,
r3_hat,
s_hat,
disclosed_messages,
undisclosed_responses,
})
}
/// Verify a selective-disclosure proof.
pub fn verify_proof(
proof: &DisclosureProof,
public_key: &PublicKey,
nonce: &[u8],
generators: &[G1Projective],
) -> Result<(), BbsError> {
// Determine n from disclosed indices + undisclosed responses count.
// The verifier knows the total message count because it equals
// `disclosed.len() + undisclosed_responses.len()`.
let n = proof.disclosed_messages.len() + proof.undisclosed_responses.len();
if generators.len() < n + 1 {
return Err(BbsError::GeneratorCount {
have: generators.len(),
need: n + 1,
});
}
// Reject degenerate A'.
if bool::from(G1Projective::from(proof.a_prime).is_identity()) {
return Err(BbsError::ProofVerification("A' is identity".into()));
}
// 1. Reconstruct the undisclosed-index set.
let mut disclosed_set = BTreeSet::new();
for &i in proof.disclosed_messages.keys() {
if i >= n {
return Err(BbsError::DisclosedIndexOutOfRange { index: i, len: n });
}
if !disclosed_set.insert(i) {
return Err(BbsError::DisclosedIndexDuplicate(i));
}
}
let undisclosed: Vec<usize> =
(0..n).filter(|i| !disclosed_set.contains(i)).collect();
if undisclosed.len() != proof.undisclosed_responses.len() {
return Err(BbsError::ProofVerification(
"undisclosed response count mismatch".into(),
));
}
let h0 = generators[0];
let a_prime_p = G1Projective::from(proof.a_prime);
let a_bar_p = G1Projective::from(proof.a_bar);
let d_p = G1Projective::from(proof.d);
// 2. Recompute T1 from responses.
// Relation: A_bar - D == A'*(-e) + H_0*(-r2)
// Reconstruction:
// T1' = -A'*e_hat - H_0*r2_hat + c*(D - A_bar)
let mut t1_recon = G1Projective::identity();
t1_recon -= a_prime_p * proof.e_hat;
t1_recon -= h0 * proof.r2_hat;
t1_recon += (d_p - a_bar_p) * proof.c;
// 3. Recompute T2 from responses.
// Relation: H_0 + sum_{D} H_{i+1}*m_i == D*r3 - H_0*s' - sum_{U} H_{i+1}*m_i
// Reconstruction:
// T2' = D*r3_hat - H_0*s_hat - sum_{U} H_{i+1}*m_hat_i
// - c*(H_0 + sum_{D} H_{i+1}*m_disclosed_i)
let mut t2_recon = G1Projective::identity();
t2_recon += d_p * proof.r3_hat;
t2_recon -= h0 * proof.s_hat;
for (slot, &idx) in undisclosed.iter().enumerate() {
t2_recon -= generators[idx + 1] * proof.undisclosed_responses[slot];
}
// Subtract c * (H_0 + sum disclosed)
let mut disclosed_lhs = h0;
for (&idx, m) in &proof.disclosed_messages {
disclosed_lhs += generators[idx + 1] * m;
}
t2_recon -= disclosed_lhs * proof.c;
let t1_aff = G1Affine::from(t1_recon);
let t2_aff = G1Affine::from(t2_recon);
// 4. Recompute the challenge.
let c_recon = challenge_scalar(
&proof.a_prime,
&proof.a_bar,
&proof.d,
&t1_aff,
&t2_aff,
public_key,
&proof.disclosed_messages,
nonce,
);
if !bool::from(c_recon.ct_eq(&proof.c)) {
return Err(BbsError::ProofVerification(
"challenge mismatch".into(),
));
}
// 5. Pairing identity: e(A', W) == e(A_bar, g2)
let prep_w = G2Prepared::from(public_key.w);
let prep_g2 = G2Prepared::from(G2Affine::generator());
let neg_a_bar = G1Affine::from(-G1Projective::from(proof.a_bar));
let gt = multi_miller_loop(&[
(&proof.a_prime, &prep_w),
(&neg_a_bar, &prep_g2),
])
.final_exponentiation();
if !bool::from(gt.is_identity()) {
return Err(BbsError::ProofVerification(
"pairing identity failed".into(),
));
}
let _ = G2Projective::generator(); // anchor the import (used elsewhere)
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::encode::message_to_scalar;
use crate::generators::message_generators;
use crate::keys::keygen;
use crate::sign::sign;
use rand::rngs::OsRng;
fn fixture(n: usize) -> (crate::keys::KeyPair, Vec<Scalar>, Vec<G1Projective>) {
let kp = keygen(&mut OsRng);
let msgs: Vec<Scalar> = (0..n)
.map(|i| message_to_scalar(format!("msg-{i}").as_bytes()))
.collect();
let gens = message_generators(b"smart-byte-bbs-proof-test", n + 2);
(kp, msgs, gens)
}
#[test]
fn disclose_subset_verifies() {
let (kp, msgs, gens) = fixture(5);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let proof = create_proof(
&sig,
&kp.public,
&msgs,
&[1, 3],
b"nonce-1",
&gens,
&mut OsRng,
)
.unwrap();
verify_proof(&proof, &kp.public, b"nonce-1", &gens).unwrap();
assert_eq!(proof.disclosed_messages.len(), 2);
assert_eq!(proof.undisclosed_responses.len(), 3);
}
#[test]
fn disclose_none_verifies() {
let (kp, msgs, gens) = fixture(3);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let proof = create_proof(
&sig,
&kp.public,
&msgs,
&[],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
verify_proof(&proof, &kp.public, b"n", &gens).unwrap();
}
#[test]
fn disclose_all_verifies() {
let (kp, msgs, gens) = fixture(3);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let proof = create_proof(
&sig,
&kp.public,
&msgs,
&[0, 1, 2],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
verify_proof(&proof, &kp.public, b"n", &gens).unwrap();
}
#[test]
fn wrong_nonce_fails() {
let (kp, msgs, gens) = fixture(4);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let proof = create_proof(
&sig,
&kp.public,
&msgs,
&[2],
b"good-nonce",
&gens,
&mut OsRng,
)
.unwrap();
assert!(
verify_proof(&proof, &kp.public, b"bad-nonce", &gens).is_err()
);
}
#[test]
fn wrong_public_key_fails() {
let (kp, msgs, gens) = fixture(4);
let other = keygen(&mut OsRng);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let proof = create_proof(
&sig,
&kp.public,
&msgs,
&[2],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
assert!(verify_proof(&proof, &other.public, b"n", &gens).is_err());
}
#[test]
fn tampered_disclosed_message_fails() {
let (kp, msgs, gens) = fixture(4);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let mut proof = create_proof(
&sig,
&kp.public,
&msgs,
&[1, 2],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
// Flip one disclosed value.
if let Some(v) = proof.disclosed_messages.get_mut(&1) {
*v += Scalar::ONE;
}
assert!(verify_proof(&proof, &kp.public, b"n", &gens).is_err());
}
#[test]
fn tampered_response_fails() {
let (kp, msgs, gens) = fixture(4);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let mut proof = create_proof(
&sig,
&kp.public,
&msgs,
&[1],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
proof.e_hat += Scalar::ONE;
assert!(verify_proof(&proof, &kp.public, b"n", &gens).is_err());
}
#[test]
fn unlinkability_two_proofs_are_distinct() {
let (kp, msgs, gens) = fixture(5);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let p1 = create_proof(
&sig,
&kp.public,
&msgs,
&[1, 3],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
let p2 = create_proof(
&sig,
&kp.public,
&msgs,
&[1, 3],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
// Same credential, identical disclosure, but the wire bits
// are *cryptographically distinct* — this is the BBS+
// unlinkability property.
assert_ne!(p1.a_prime, p2.a_prime);
assert_ne!(p1.a_bar, p2.a_bar);
assert_ne!(p1.d, p2.d);
assert_ne!(p1.c, p2.c);
// Both still verify.
verify_proof(&p1, &kp.public, b"n", &gens).unwrap();
verify_proof(&p2, &kp.public, b"n", &gens).unwrap();
}
#[test]
fn proof_serde_roundtrips() {
let (kp, msgs, gens) = fixture(3);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let proof = create_proof(
&sig,
&kp.public,
&msgs,
&[1],
b"n",
&gens,
&mut OsRng,
)
.unwrap();
let bytes = serde_cbor::to_vec(&proof).unwrap();
let back: DisclosureProof = serde_cbor::from_slice(&bytes).unwrap();
assert_eq!(back, proof);
verify_proof(&back, &kp.public, b"n", &gens).unwrap();
}
#[test]
fn out_of_range_disclosed_index_errors() {
let (kp, msgs, gens) = fixture(3);
let sig = sign(&msgs, &kp.secret, &kp.public, &gens).unwrap();
let err = create_proof(
&sig, &kp.public, &msgs, &[10], b"n", &gens, &mut OsRng,
)
.unwrap_err();
assert!(matches!(err, BbsError::DisclosedIndexOutOfRange { .. }));
}
}