open-standards/smart-byte/smart-byte-rs/crates/smart-byte-zk/src/anoncreds.rs
Transaction Science e11c20b1eb ingest wave A3: 3 crates · op-ewallets-asia + smart-byte-zk + eoc-rag
OpenPay (1 crate, 3277 LOC, 18 tests)
  op-ewallets-asia    APAC e-wallet rail adapters. Alipay Cross-Border v3,
                      WeChat Pay (JSAPI/Native/H5/MicroPay) with constant-time
                      HMAC notify, UPI 2.0 (collect/intent/mandate/VPA),
                      Paytm, GrabPay, GoPay (Midtrans), Touch'n Go.
                      EMVCo MPM TLV + CRC-16/CCITT-FALSE encoder for
                      PromptPay (TH), PayNow (SG), DuitNow (MY), QRIS (ID).
                      Transport-free traits — operators inject HTTP + RSA-SHA256
                      signers (keeps POS firmware / WASM consumers reqwest-free).

Smart Byte (1 crate, 1310 LOC, 15 tests)
  smart-byte-zk       Selective-disclosure ZK primitives.
                      Real: Bulletproofs over Ristretto255 (range proofs at
                      8/16/32/64-bit), RangePredicate / InequalityPredicate /
                      SetMembershipPredicate all delegating to Bulletproofs.
                      Stubs (documented): Groth16, PLONK — deterministic
                      placeholder proofs satisfying ZkScheme trait for future
                      arkworks integration. AnonCred sketch + VerifiablePresentation
                      builder.

EOC (1 crate, ~3000 LOC, 36 tests)
  eoc-rag             RAG orchestrator. Naive (Lewis 2020), HyDE (Gao 2022),
                      RAG-Fusion + RRF (Cormack 2009), Self-RAG (Asai 2023)
                      with Retrieve/ISREL/ISSUP/ISUSE critique tokens,
                      Corrective RAG (Yan 2024) with retrieval evaluator
                      + external fallback, Adaptive RAG (Jeong 2024)
                      complexity router. Chunking: sentence-window, semantic,
                      recursive, late-chunking. Span-backed citations with
                      3-mode enforcement. SelfCheckGPT consistency guard.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 12:08:49 -04:00

194 lines
7 KiB
Rust

//! Anonymous-credential sketch with BBS-like structure.
//!
//! This module is intentionally a **sketch** — a credential-shape
//! envelope plus a holder-side link-secret commitment, enough to wire
//! into [`crate::presentation::VerifiablePresentation`] without
//! standing up a full BBS+ stack here. (The real, audited BBS+
//! signatures live in the sibling `smart-byte-bbs` crate.)
//!
//! What this module *does* provide:
//!
//! * [`LinkSecret`] — a per-holder 32-byte secret with a stable
//! Pedersen commitment, used to bind multiple presentations to the
//! same holder without revealing the holder identifier.
//! * [`AnonCredAttribute`] — an attribute slot, either disclosed
//! (cleartext) or hidden (carrying a Pedersen commitment).
//! * [`AnonCred`] — a credential envelope of attributes plus an
//! opaque signature blob (deferred to a downstream BBS+ /
//! AnonCreds-v2 backend).
//! * [`AnonCredPresentation`] — a presentation envelope with hidden
//! attributes replaced by predicate-proof references and disclosed
//! attributes echoed in cleartext.
use curve25519_dalek::ristretto::{CompressedRistretto, RistrettoPoint};
use curve25519_dalek::scalar::Scalar;
use rand::rngs::OsRng;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::error::ZkError;
/// Holder-bound 32-byte secret. Two presentations from the same
/// holder commit to the same scalar but use independent blinding, so
/// the wire commitments are unlinkable.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LinkSecret(pub Scalar);
impl LinkSecret {
/// Sample a fresh link secret from `OsRng`.
pub fn random() -> Self {
Self(Scalar::random(&mut OsRng))
}
/// Derive a link secret deterministically from a seed (e.g. a
/// hash of a holder DID + nonce). Useful for backup / recovery.
pub fn from_seed(seed: &[u8]) -> Self {
let mut h = Sha256::new();
h.update(b"smart-byte-zk/anoncreds/link-secret");
h.update(seed);
let bytes = h.finalize();
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(&bytes);
Self(Scalar::from_bytes_mod_order_wide(&wide))
}
/// Pedersen-commit to this link secret under a fresh blinding,
/// returning `(commitment, blinding)`.
pub fn commit(&self) -> (CompressedRistretto, Scalar) {
let pc = ::bulletproofs::PedersenGens::default();
let blinding = Scalar::random(&mut OsRng);
let c: RistrettoPoint = pc.commit(self.0, blinding);
(c.compress(), blinding)
}
}
/// Per-credential attribute slot.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AnonCredAttribute {
/// A disclosed attribute, in cleartext.
Disclosed {
/// Attribute name.
name: String,
/// Attribute value, encoded as UTF-8.
value: String,
},
/// A hidden attribute, represented by a Pedersen commitment.
Hidden {
/// Attribute name.
name: String,
/// Compressed Pedersen commitment.
#[serde(with = "serde_bytes")]
commitment: [u8; 32],
},
}
/// AnonCreds-style credential envelope.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AnonCred {
/// Credential identifier (e.g. issuer-scoped serial).
pub cred_id: String,
/// Issuer identifier (e.g. DID URL).
pub issuer: String,
/// Attribute slots.
pub attributes: Vec<AnonCredAttribute>,
/// Opaque signature blob — to be filled in by a downstream BBS+ /
/// AnonCreds-v2 backend. The shape of these bytes is deliberately
/// left to that backend.
#[serde(with = "serde_bytes")]
pub signature: Vec<u8>,
/// Pedersen commitment to the holder's [`LinkSecret`].
#[serde(with = "serde_bytes")]
pub link_commitment: [u8; 32],
}
/// Holder-side presentation envelope. Hidden attributes carry a
/// per-attribute predicate-proof reference (an opaque byte blob
/// indexed by the predicate scheme), disclosed attributes are echoed
/// in cleartext, and the link commitment is fresh.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AnonCredPresentation {
/// Credential identifier the presentation derives from.
pub cred_id: String,
/// Issuer identifier the credential is bound to.
pub issuer: String,
/// Disclosed attributes echoed in cleartext.
pub disclosed: Vec<(String, String)>,
/// Predicate-proof references for hidden attributes.
pub predicates: Vec<PredicateRef>,
/// Fresh Pedersen commitment to the holder's link secret. Two
/// presentations from the same holder will produce *different*
/// wire bytes here, but a verifier with the holder's public
/// link-commitment template can correlate them at audit time.
#[serde(with = "serde_bytes")]
pub link_commitment: [u8; 32],
}
/// Reference to a predicate proof carried in
/// [`crate::presentation::VerifiablePresentation`].
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PredicateRef {
/// Attribute name the predicate is over.
pub attribute: String,
/// Predicate kind tag: `"range"`, `"inequality"`,
/// `"set-membership"`.
pub kind: String,
/// Index into the presentation's `proofs` vector.
pub proof_index: usize,
}
/// Build a fresh presentation envelope from an [`AnonCred`].
pub fn present(cred: &AnonCred, link: &LinkSecret) -> Result<AnonCredPresentation, ZkError> {
let mut disclosed = Vec::new();
for attr in &cred.attributes {
if let AnonCredAttribute::Disclosed { name, value } = attr {
disclosed.push((name.clone(), value.clone()));
}
}
let (link_commit, _blinding) = link.commit();
Ok(AnonCredPresentation {
cred_id: cred.cred_id.clone(),
issuer: cred.issuer.clone(),
disclosed,
predicates: Vec::new(),
link_commitment: link_commit.to_bytes(),
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn link_secret_deterministic_from_seed() {
let a = LinkSecret::from_seed(b"did:example:alice");
let b = LinkSecret::from_seed(b"did:example:alice");
assert_eq!(a, b);
let c = LinkSecret::from_seed(b"did:example:bob");
assert_ne!(a, c);
}
#[test]
fn present_echoes_disclosed_attributes() {
let link = LinkSecret::random();
let (commit, _) = link.commit();
let cred = AnonCred {
cred_id: "cred-1".into(),
issuer: "did:example:issuer".into(),
attributes: vec![
AnonCredAttribute::Disclosed {
name: "given_name".into(),
value: "Alice".into(),
},
AnonCredAttribute::Hidden {
name: "age".into(),
commitment: [7u8; 32],
},
],
signature: vec![0u8; 8],
link_commitment: commit.to_bytes(),
};
let pres = present(&cred, &link).expect("present");
assert_eq!(pres.disclosed.len(), 1);
assert_eq!(pres.disclosed[0].0, "given_name");
}
}