Gated Relationships via Anonymous Credential Exchange
Warning: This is experimental cryptographic software. Do not use in production.
A Rust toolkit for building privacy-preserving relationship systems using anonymous credentials. Grace enables entities to form cryptographically-verified associations while maintaining privacy through credential-gated encryption.
Grace provides a complete system for:
- Anonymous Credentials: Issue and verify credentials without revealing identity
- Gated Encryption: Encrypt messages that only authorized credential holders can decrypt
- Association Graphs: Build verifiable relationship networks between entities
- Lazy Revocation: Efficiently revoke entities with automatic credential refresh
┌─────────────────────────────────────────────────────────────────────────────┐
│ grace (facade) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Domain │ │ Entity │ │ KMS │ │ Store │ │
│ │ Handle │ │ Builder │ │ (Keys) │ │ (Persistence) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
│
┌───────────────────────────┼───────────────────────────┐
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ grace-primitives │ │ grace-crypto │ │ grace-common │
│ │ │ │ │ │
│ - Domain │ │ - SAGA (BBS MAC) │ │ - Point, Scalar │
│ - Entity │ │ - MKVAC │ │ - Hash functions │
│ - Policy │ │ - Gated-KEM │ │ - Serialization │
│ - Header │ │ │ │ │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
| Crate | Description |
|---|---|
grace |
High-level API with DomainHandle, EntityBuilder, storage, and KMS |
grace-primitives |
Core domain types: Entity, Policy, Header, Domain |
grace-crypto |
Cryptographic primitives: SAGA, MKVAC, Gated-KEM |
grace-common |
Shared utilities: Point, Scalar, hash functions |
Add Grace to your Cargo.toml:
[dependencies]
grace = { path = "path/to/grace" }With optional features:
[dependencies]
grace = { path = "path/to/grace", features = ["file-kms", "redb-store"] }use std::sync::Arc;
use grace::{
DomainBuilder, EntityBuilder, InsecureMemoryKms, Kms, MemoryStore,
AssociationPolicy, EntityMetadata, ScalarExt, Scalar,
};
use rand::SeedableRng;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut rng = rand::rngs::StdRng::from_entropy();
// 1. Create a domain with in-memory storage
let store = Arc::new(MemoryStore::new());
let kms = InsecureMemoryKms::new(&mut rng);
let (master_key, _) = kms.generate_master_key()?;
let domain = DomainBuilder::new()
.with_store(store)
.with_master_key(master_key)
.with_num_attrs(2)
.with_capacity(100)`
.create(&mut rng)?;
// 2. Register entities
let alice_attrs = vec![Scalar::rand(&mut rng), Scalar::rand(&mut rng)];
let mut alice = EntityBuilder::new()
.with_domain(domain.clone())
.with_attributes(&alice_attrs)
.register(&mut rng)?;
let bob_attrs = vec![Scalar::rand(&mut rng), Scalar::rand(&mut rng)];
let mut bob = EntityBuilder::new()
.with_domain(domain.clone())
.with_attributes(&bob_attrs)
.register(&mut rng)?;
// 3. Alice creates a header for Bob
let header = domain.create_header(
&mut rng,
alice.credential(),
AssociationPolicy::entity(bob.id()),
&EntityMetadata::with_name("Alice"),
)?;
// 4. Bob decrypts and attests to Alice
let (sender, metadata, _, signature) = domain.decrypt_header(bob.credential(), &header)?;
println!("Bob received message from: {} ({:?})", sender, metadata.name);
let attestation = bob.create_attestation(alice.id(), signature);
domain.process_attestation(&attestation)?;
// 5. Association is now established
assert!(domain.has_association(bob.id(), alice.id())?);
Ok(())
}Grace includes comprehensive examples:
use grace::{run_complete_flow, run_basic_example};
// Full demonstration of all features
run_complete_flow().expect("complete flow failed");
// Minimal example with ephemeral storage
run_basic_example().expect("basic example failed");The Domain is the central authority that:
- Maintains the entity-relationship graph
- Issues and refreshes entity credentials
- Processes attestations and manages associations
- Handles entity revocation
// Create with DomainBuilder
let domain = DomainBuilder::new()
.with_store(store)
.with_master_key(master_key)
.with_id("my-domain")
.with_num_attrs(2) // Attributes per credential
.with_capacity(1000) // Expected entity count
.create(&mut rng)?;
// Or create ephemeral (no persistence)
let ephemeral_domain = DomainBuilder::new()
.ephemeral()
.create(&mut rng)?;Entities are participants in the system with:
- A unique identifier
- A credential (signing key + gated credential)
- Optional persistent storage
// Register through DomainHandle (recommended)
let entity = EntityBuilder::new()
.with_domain(domain.clone())
.with_attributes(&attrs)
.register(&mut rng)?;
// Load existing entity
let loaded = EntityBuilder::new()
.with_domain(domain.clone())
.load(entity_id)?;
// Ephemeral entity (no persistence)
let temp_entity = EntityBuilder::new()
.with_domain(domain.clone())
.ephemeral()
.with_attributes(&attrs)
.register(&mut rng)?;Headers are encrypted messages that only authorized entities can decrypt:
// Single target
let policy = AssociationPolicy::entity(bob.id());
// Multiple targets (OR - any can decrypt)
let policy = AssociationPolicy::any_of(&[bob.id(), charlie.id()]);
// Broadcast (everyone can decrypt)
let policy = AssociationPolicy::broadcast();
// Complex policies with AND
let policy = AssociationPolicy::entity(bob.id())
& AssociationPolicy::entity(charlie.id()); // Both required
// Create header with policy
let header = domain.create_header(
&mut rng,
sender.credential(),
policy,
&EntityMetadata::with_name("Sender").with_attribute("key", "value"),
)?;The association flow establishes relationships between entities:
sequenceDiagram
participant Alice
participant Domain
participant Bob
Alice->>Domain: create_header(policy: Bob)
Domain-->>Alice: EntityHeader
Alice->>Bob: Send header
Bob->>Domain: decrypt_header(header)
Domain-->>Bob: (sender_id, metadata, secret, signature)
Bob->>Bob: create_attestation(alice_id, signature)
Bob->>Domain: process_attestation(attestation)
Note over Domain: Association (Bob → Alice) recorded
Alice->>Domain: refresh_credential()
Note over Alice: Now has Bob's decryption key
After attestations, entities refresh credentials to gain new rights:
// Alice can now decrypt messages from Bob
let new_cred = domain.refresh_credential(&mut rng, alice.credential(), &alice_attrs)?;
// Check rights
assert!(new_cred.gated_credential.has_right(bob.id().0));Revoke entities with automatic association cleanup:
let result = domain.revoke_entity(bob.id(), Some("Reason".to_string()))?;
println!("Revoked at epoch: {}", result.new_epoch);
println!("Associations removed: {}", result.associations_removed);
println!("Affected entities: {:?}", result.affected_entities);
// Affected entities should refresh their credentials
// to remove the revoked entity's rightsGrace supports multiple storage backends:
In-memory storage for testing and ephemeral use:
let store = Arc::new(MemoryStore::new());Persistent embedded database:
use grace::RedbStore;
let store = Arc::new(RedbStore::open("data/grace.redb")?);Append-only store with Merkle proofs for auditability:
use grace::MerkleStore;
let store = MerkleStore::new();
store.append(b"data")?;
let proof = store.prove(0)?;
assert!(proof.verify(&store.root(), b"data"));In-memory KMS for development:
let kms = InsecureMemoryKms::new(&mut rng);
let (master_key, wrapped) = kms.generate_master_key()?;Password-protected file-based KMS using Argon2:
use grace::FileKms;
// Create new KMS
let kms = FileKms::create("keys/master.key", "strong-password")?;
let (master_key, _) = kms.generate_master_key()?;
// Load existing
let kms = FileKms::open("keys/master.key", "strong-password")?;
let master_key = kms.unwrap_key(&wrapped_key)?;| Feature | Description |
|---|---|
file-kms |
Password-based file KMS with Argon2 |
redb-store |
Persistent storage with redb |
merkle-store |
Append-only Merkle tree storage |
async |
Async trait support |
Hierarchical credential issuance with delegation:
use grace::saga::{RootIssuer, HierarchicalParams};
let params = HierarchicalParams::new(num_attrs, max_delegation_levels);
let root = RootIssuer::new(&mut rng, params);
let sub_issuer = root.delegate(&mut rng, level)?;Anonymous credential presentation:
use grace::mkvac;
let (issuer_sk, issuer_pk) = mkvac::issuer_keygen(&mut rng, &pp);
let cred = mkvac::issue_cred(&mut rng, &pp, &issuer_sk, &attrs)?;
let (presentation, proof) = mkvac::show(&mut rng, &pp, &cred, &context)?;Credential-gated encryption:
use grace::gated_kem;
let (master, public) = gated_kem::setup(&mut rng, &rights, num_attrs)?;
let cred = gated_kem::issue_credential(&mut rng, &master, &user_rights, &attrs)?;
let (secret, ciphertext) = gated_kem::encaps(&mut rng, &public, target_right)?;
let (decrypted, proof) = gated_kem::decaps(&cred, &ciphertext)?;- Experimental: This software has not been audited. Do not use in production.
- Key Management: Use
FileKmswith strong passwords in non-test environments. - Revocation: Is lazy - affected entities must refresh credentials to remove revoked rights.
- Serialization: Domain reconstruction from seed is deterministic - protect the seed.
Grace/
├── grace/ # Main facade crate
│ └── src/
│ ├── domain/ # DomainHandle, DomainBuilder
│ ├── entity/ # Entity, EntityBuilder
│ ├── example/ # Usage examples
│ ├── kms/ # Key management (Memory, File)
│ ├── store/ # Storage (Memory, Redb, Merkle)
│ ├── sealed.rs # Encryption utilities
│ └── lib.rs
├── grace-primitives/ # Core domain types
│ └── src/
│ ├── domain.rs # Domain logic
│ ├── entity.rs # EntityCredential, EntitySecret
│ ├── header.rs # EntityHeader, SealedPayload
│ └── types.rs # Policy, EntityId, etc.
├── grace-crypto/ # Cryptographic schemes
│ └── src/
│ ├── saga/ # BBS-style MAC
│ ├── mkvac/ # Anonymous credentials
│ └── gated_kem/ # Credential-gated KEM
└── grace-common/ # Shared primitives
└── src/
└── lib.rs # Point, Scalar, hash
Run all tests:
cargo test --workspace --all-featuresRun specific test:
cargo test --workspace --all-features test_complete_flow_exampleMIT OR Apache-2.0