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).
283 lines
9.1 KiB
Rust
283 lines
9.1 KiB
Rust
//! W3C `bbs-2023` Data Integrity cryptosuite.
|
|
//!
|
|
//! Reference: W3C Verifiable Credential Data Integrity 1.0 + the
|
|
//! `vc-di-bbs` companion specification (Editor's Draft, 2024-12).
|
|
//!
|
|
//! ## Scope
|
|
//!
|
|
//! `bbs-2023` is a Data Integrity cryptosuite that encodes BBS+
|
|
//! signatures over an *RDF dataset canonicalisation* of the credential.
|
|
//! Full canonicalisation requires URDNA2015 + JSON-LD expansion which
|
|
//! drag in heavy dependencies (the `rdf-canon` feature in
|
|
//! `smart-byte-vc` gates that path). This module ships the
|
|
//! *JCS variant* of the cryptosuite — sign the JCS-canonical encoding
|
|
//! of the credential minus its `proof` array — so the smart-byte
|
|
//! reference implementation can issue and verify BBS+-secured VCs
|
|
//! without the URDNA2015 toolchain.
|
|
//!
|
|
//! This matches the pragmatic convention adopted by AnonCreds 2.0 and
|
|
//! by the IETF SD-JWT-BBS interop suite: BBS+ over a stable canonical
|
|
//! byte representation of the issuer-asserted claims, with the message
|
|
//! vector being one scalar per top-level claim.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::encode::message_to_scalar;
|
|
use crate::error::BbsError;
|
|
use crate::generators::message_generators;
|
|
use crate::keys::{PublicKey, SecretKey};
|
|
use crate::proof::{DisclosureProof, create_proof, verify_proof};
|
|
use crate::sign::{Signature, sign, verify};
|
|
|
|
/// Cryptosuite identifier as recorded in
|
|
/// [`DataIntegrityProof::cryptosuite`].
|
|
pub const CRYPTOSUITE_BBS_2023: &str = "bbs-2023";
|
|
|
|
/// Domain string used when deriving generators for the cryptosuite.
|
|
/// Includes the cryptosuite name so generators from different suites
|
|
/// cannot collide.
|
|
pub const CRYPTOSUITE_GENERATOR_DOMAIN: &[u8] = b"smart-byte-bbs-2023";
|
|
|
|
/// A `Bbs2023Suite` proof payload — what travels in
|
|
/// `DataIntegrityProof::proof_value` for an issuance.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Bbs2023IssuanceProof {
|
|
/// The compact BBS+ signature `(A, e)` (80 bytes).
|
|
#[serde(with = "serde_bytes_array_80")]
|
|
pub signature: [u8; 80],
|
|
/// The number of messages signed. Required so the verifier can
|
|
/// reproduce the generator vector.
|
|
pub message_count: u32,
|
|
}
|
|
|
|
/// A `Bbs2023Suite` proof payload — what travels in
|
|
/// `DataIntegrityProof::proof_value` for a holder presentation
|
|
/// (selective-disclosure proof).
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct Bbs2023DisclosureProof {
|
|
/// CBOR-encoded [`crate::proof::DisclosureProof`].
|
|
#[serde(with = "serde_bytes")]
|
|
pub disclosure: Vec<u8>,
|
|
/// Total message count of the underlying signed credential.
|
|
pub message_count: u32,
|
|
}
|
|
|
|
/// Custom serde shim for fixed-size `[u8; 80]` over byte string.
|
|
mod serde_bytes_array_80 {
|
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
|
|
|
/// Serialise the 80-byte array as a byte string.
|
|
pub fn serialize<S: Serializer>(
|
|
bytes: &[u8; 80],
|
|
s: S,
|
|
) -> Result<S::Ok, S::Error> {
|
|
serde_bytes::Bytes::new(bytes).serialize(s)
|
|
}
|
|
|
|
/// Deserialise the 80-byte array from a byte string.
|
|
pub fn deserialize<'de, D: Deserializer<'de>>(
|
|
d: D,
|
|
) -> Result<[u8; 80], D::Error> {
|
|
let bb: serde_bytes::ByteBuf = serde_bytes::ByteBuf::deserialize(d)?;
|
|
if bb.len() != 80 {
|
|
return Err(serde::de::Error::custom(format!(
|
|
"expected 80 bytes, got {}",
|
|
bb.len()
|
|
)));
|
|
}
|
|
let mut out = [0u8; 80];
|
|
out.copy_from_slice(&bb);
|
|
Ok(out)
|
|
}
|
|
}
|
|
|
|
/// Trait surface for proof-suite implementations, aligned with the
|
|
/// trait that `smart-byte-vc::proof` would expect for cryptosuite
|
|
/// pluggability. `smart-byte-vc` currently inlines its
|
|
/// `eddsa-jcs-2022` handling, so this trait is local to the BBS+
|
|
/// crate; downstream crates can adopt it without re-exporting.
|
|
pub trait ProofSuite {
|
|
/// The cryptosuite identifier as it appears in `DataIntegrityProof::cryptosuite`.
|
|
fn name(&self) -> &'static str;
|
|
}
|
|
|
|
/// Marker type for the `bbs-2023` cryptosuite.
|
|
pub struct Bbs2023Suite;
|
|
|
|
impl ProofSuite for Bbs2023Suite {
|
|
fn name(&self) -> &'static str {
|
|
CRYPTOSUITE_BBS_2023
|
|
}
|
|
}
|
|
|
|
/// Convert an ordered list of arbitrary claim byte strings into the
|
|
/// BBS+ scalar message vector.
|
|
pub fn claims_to_messages<I, B>(claims: I) -> Vec<bls12_381::Scalar>
|
|
where
|
|
I: IntoIterator<Item = B>,
|
|
B: AsRef<[u8]>,
|
|
{
|
|
claims
|
|
.into_iter()
|
|
.map(|c| message_to_scalar(c.as_ref()))
|
|
.collect()
|
|
}
|
|
|
|
/// Issue a BBS+ signature over `claims`. Generators are derived from
|
|
/// [`CRYPTOSUITE_GENERATOR_DOMAIN`] and the claim count.
|
|
pub fn issue_bbs_2023(
|
|
claims: &[&[u8]],
|
|
secret_key: &SecretKey,
|
|
public_key: &PublicKey,
|
|
) -> Result<Bbs2023IssuanceProof, BbsError> {
|
|
let msgs = claims_to_messages(claims.iter().copied());
|
|
let gens = message_generators(
|
|
CRYPTOSUITE_GENERATOR_DOMAIN,
|
|
msgs.len() + 2,
|
|
);
|
|
let sig = sign(&msgs, secret_key, public_key, &gens)?;
|
|
Ok(Bbs2023IssuanceProof {
|
|
signature: sig.to_bytes(),
|
|
message_count: msgs.len() as u32,
|
|
})
|
|
}
|
|
|
|
/// Verify a `bbs-2023` issuance against the full claim vector. Used
|
|
/// when the verifier has the original (undisclosed) credential.
|
|
pub fn verify_bbs_2023_issuance(
|
|
proof: &Bbs2023IssuanceProof,
|
|
claims: &[&[u8]],
|
|
public_key: &PublicKey,
|
|
) -> Result<(), BbsError> {
|
|
if proof.message_count as usize != claims.len() {
|
|
return Err(BbsError::ProofVerification(format!(
|
|
"message count {} != claims len {}",
|
|
proof.message_count,
|
|
claims.len()
|
|
)));
|
|
}
|
|
let sig = Signature::from_bytes(&proof.signature)?;
|
|
let msgs = claims_to_messages(claims.iter().copied());
|
|
let gens = message_generators(
|
|
CRYPTOSUITE_GENERATOR_DOMAIN,
|
|
msgs.len() + 2,
|
|
);
|
|
verify(&msgs, &sig, public_key, &gens)
|
|
}
|
|
|
|
/// Create a holder's selective-disclosure proof from a BBS+ issuance.
|
|
pub fn create_bbs_2023_disclosure(
|
|
issuance: &Bbs2023IssuanceProof,
|
|
claims: &[&[u8]],
|
|
disclosed_indices: &[usize],
|
|
public_key: &PublicKey,
|
|
nonce: &[u8],
|
|
rng: &mut (impl rand::RngCore + rand::CryptoRng),
|
|
) -> Result<Bbs2023DisclosureProof, BbsError> {
|
|
if issuance.message_count as usize != claims.len() {
|
|
return Err(BbsError::ProofVerification(format!(
|
|
"issuance asserts {} messages but holder provided {}",
|
|
issuance.message_count,
|
|
claims.len()
|
|
)));
|
|
}
|
|
let sig = Signature::from_bytes(&issuance.signature)?;
|
|
let msgs = claims_to_messages(claims.iter().copied());
|
|
let gens = message_generators(
|
|
CRYPTOSUITE_GENERATOR_DOMAIN,
|
|
msgs.len() + 2,
|
|
);
|
|
let proof = create_proof(
|
|
&sig,
|
|
public_key,
|
|
&msgs,
|
|
disclosed_indices,
|
|
nonce,
|
|
&gens,
|
|
rng,
|
|
)?;
|
|
let disclosure = serde_cbor::to_vec(&proof)?;
|
|
Ok(Bbs2023DisclosureProof {
|
|
disclosure,
|
|
message_count: issuance.message_count,
|
|
})
|
|
}
|
|
|
|
/// Verify a holder's selective-disclosure proof.
|
|
pub fn verify_bbs_2023_disclosure(
|
|
proof: &Bbs2023DisclosureProof,
|
|
public_key: &PublicKey,
|
|
nonce: &[u8],
|
|
) -> Result<DisclosureProof, BbsError> {
|
|
let dp: DisclosureProof = serde_cbor::from_slice(&proof.disclosure)?;
|
|
let gens = message_generators(
|
|
CRYPTOSUITE_GENERATOR_DOMAIN,
|
|
proof.message_count as usize + 2,
|
|
);
|
|
verify_proof(&dp, public_key, nonce, &gens)?;
|
|
Ok(dp)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::keys::keygen;
|
|
use rand::rngs::OsRng;
|
|
|
|
#[test]
|
|
fn cryptosuite_issuance_roundtrip() {
|
|
let kp = keygen(&mut OsRng);
|
|
let claims: Vec<&[u8]> = vec![
|
|
b"id:did:example:alice",
|
|
b"name:Alice",
|
|
b"birth:1990-01-01",
|
|
b"country:NZ",
|
|
];
|
|
let iss = issue_bbs_2023(&claims, &kp.secret, &kp.public).unwrap();
|
|
verify_bbs_2023_issuance(&iss, &claims, &kp.public).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn cryptosuite_disclosure_roundtrip() {
|
|
let kp = keygen(&mut OsRng);
|
|
let claims: Vec<&[u8]> = vec![
|
|
b"id",
|
|
b"name",
|
|
b"age",
|
|
b"country",
|
|
b"role",
|
|
];
|
|
let iss = issue_bbs_2023(&claims, &kp.secret, &kp.public).unwrap();
|
|
let disc = create_bbs_2023_disclosure(
|
|
&iss,
|
|
&claims,
|
|
&[1, 3],
|
|
&kp.public,
|
|
b"verifier-nonce",
|
|
&mut OsRng,
|
|
)
|
|
.unwrap();
|
|
let verified = verify_bbs_2023_disclosure(
|
|
&disc,
|
|
&kp.public,
|
|
b"verifier-nonce",
|
|
)
|
|
.unwrap();
|
|
assert_eq!(verified.disclosed_messages.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn cryptosuite_name_matches_spec() {
|
|
assert_eq!(Bbs2023Suite.name(), "bbs-2023");
|
|
}
|
|
|
|
#[test]
|
|
fn issuance_serde_cbor_roundtrip() {
|
|
let kp = keygen(&mut OsRng);
|
|
let claims: Vec<&[u8]> = vec![b"a", b"b"];
|
|
let iss = issue_bbs_2023(&claims, &kp.secret, &kp.public).unwrap();
|
|
let bytes = serde_cbor::to_vec(&iss).unwrap();
|
|
let back: Bbs2023IssuanceProof = serde_cbor::from_slice(&bytes).unwrap();
|
|
assert_eq!(back, iss);
|
|
}
|
|
}
|