@l8zk/sdk
v1.3.1
Published
Privacy-preserving verifiable credentials using zero-knowledge proofs. Production implementation of the OpenAC protocol.
Maintainers
Readme
@l8zk/sdk
Production-ready SDK for privacy-preserving verifiable credentials using zero-knowledge proofs.
Built on the OpenAC protocol by Privacy and Scaling Explorations (PSE), Ethereum Foundation.
Overview
The L8ZK SDK enables privacy-preserving credential verification where users can prove properties about their credentials (e.g., "I am over 18") without revealing the underlying data (e.g., exact birthdate). Each proof is cryptographically unlinkable, preventing tracking across verifiers.
Key Features
- Zero-Knowledge Proofs: Generates Spartan2 ZK proofs (~112KB prepare, ~41KB show)
- No Trusted Setup: Uses transparent Spartan protocol with Hyrax commitments
- SD-JWT Compatible: Works with Selective Disclosure JWT credentials
- Cross-Platform: Node.js support (macOS/Linux, x64/ARM64)
- Fast Performance: ~14s total (one-time setup) + ~100ms per presentation
- Unlinkable: Each proof is cryptographically reblinded to prevent correlation
- Auto-Download: Circom artifacts downloaded automatically on first use (~33MB)
Current Status
This SDK generates and verifies real cryptographic ZK proofs using the Spartan2 proving system. The proofs are:
- Cryptographically sound
- Properly reblinded for unlinkability
- Verified by the native Rust backend
Current Limitation: The SDK currently uses pre-compiled circuit inputs for proof generation. Custom credential data support requires circuit recompilation and is under active development. The API and proof flow are production-ready.
Security Properties
- Zero-Knowledge: Only policy satisfaction is revealed, not the actual credential data
- Unlinkability: Each presentation is cryptographically unique and cannot be correlated
- Soundness: Verifier is convinced only if the statement is true
- Device Binding: Credentials can be bound to a specific device to prevent transfer
Installation
npm install @l8zk/sdkThat's it! The SDK automatically:
- Installs the correct native binary for your platform (macOS/Linux, x64/ARM64)
- Downloads circom artifacts (~33MB) on first use
Requirements
- Node.js 18+
Platform Support
| Platform | Architecture | Package |
| -------- | ------------------------ | ------------------------ |
| macOS | Apple Silicon (M1/M2/M3) | @l8zk/sdk-darwin-arm64 |
| macOS | Intel | @l8zk/sdk-darwin-x64 |
| Linux | x64 | @l8zk/sdk-linux-x64 |
| Linux | ARM64 | @l8zk/sdk-linux-arm64 |
Platform binaries are installed automatically via npm's optional dependencies.
Quick Start
Basic Usage (Node.js)
import { OpenAC, generateKeyPair, base64UrlEncode } from "@l8zk/sdk";
// Helper: Create a test SD-JWT credential
function createTestCredential(): string {
const keys = generateKeyPair();
const now = Math.floor(Date.now() / 1000);
const header = { alg: "ES256", typ: "vc+sd-jwt" };
const payload = {
iss: "https://test-issuer.example.com",
sub: "did:example:user123",
iat: now,
exp: now + 365 * 24 * 60 * 60,
cnf: { jwk: keys.publicKey },
_sd: [],
};
const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)));
const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)));
const signatureB64 = base64UrlEncode(new Uint8Array(64).fill(1));
const nameDisclosure = base64UrlEncode(
new TextEncoder().encode(JSON.stringify(["salt1", "name", "Alice"]))
);
const ageDisclosure = base64UrlEncode(
new TextEncoder().encode(JSON.stringify(["salt2", "roc_birthday", "19901215"]))
);
return `${headerB64}.${payloadB64}.${signatureB64}~${nameDisclosure}~${ageDisclosure}`;
}
async function main() {
// Step 1: Create/obtain an SD-JWT credential
const credential = createTestCredential();
// Step 2: Prepare credential (one-time, ~6s)
const handle = await OpenAC.prepare({
credential,
deviceBinding: true,
});
// Step 3: Generate proof (per presentation, ~100ms)
const nonce = OpenAC.generateNonce();
const proof = await handle.show({
policy: { age: { gte: 18 } },
nonce,
});
// Step 4: Verify proof (verifier side, ~34ms)
const result = await OpenAC.verify(proof, { age: { gte: 18 } }, { expectedNonce: nonce });
console.log(result.valid); // true
}
main();Complete Example: Age Verification
import { OpenAC, generateKeyPair, base64UrlEncode } from "@l8zk/sdk";
// Helper: Create an SD-JWT credential (in production, this comes from an issuer)
function createCredential(): string {
const keys = generateKeyPair();
const now = Math.floor(Date.now() / 1000);
const header = { alg: "ES256", typ: "vc+sd-jwt" };
const payload = {
iss: "https://federal-republic-of-germany.gov",
sub: "did:example:alice-schmidt",
iat: now,
exp: now + 365 * 24 * 60 * 60,
cnf: { jwk: keys.publicKey },
_sd: [],
};
const headerB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(header)));
const payloadB64 = base64UrlEncode(new TextEncoder().encode(JSON.stringify(payload)));
const signatureB64 = base64UrlEncode(new Uint8Array(64).fill(1));
// Disclosures contain the actual claims
const birthdayDisclosure = base64UrlEncode(
new TextEncoder().encode(JSON.stringify(["salt1", "roc_birthday", "19900515"]))
);
const nationalityDisclosure = base64UrlEncode(
new TextEncoder().encode(JSON.stringify(["salt2", "nationality", "DE"]))
);
return `${headerB64}.${payloadB64}.${signatureB64}~${birthdayDisclosure}~${nationalityDisclosure}`;
}
async function main() {
// User: Prepare credential with ZK proof capability
const credential = createCredential();
const wallet = await OpenAC.prepare({
credential,
deviceBinding: true,
});
// Verifier: Request proof of age >= 18
const challenge = OpenAC.generateNonce();
// User: Generate privacy-preserving proof
const presentation = await wallet.show({
policy: { age: { gte: 18 } },
nonce: challenge,
});
// Verifier: Verify the proof
const verification = await OpenAC.verify(
presentation,
{ age: { gte: 18 } },
{ expectedNonce: challenge }
);
if (verification.valid) {
console.log("Access granted: User is 18+");
// Verifier knows: User is 18+
// Verifier does NOT know: Exact age, birthdate, name, nationality
} else {
console.log("Access denied");
}
}
main();Advanced: Multiple Policies
// Crypto exchange onboarding
const proof = await wallet.show({
policy: {
age: { gte: 18 },
nationality: { nin: ["KP", "IR", "SY"] }, // Not in sanctioned countries
countryCode: { in: ["US", "CA", "GB", "DE", "FR"] }, // Supported regions
},
nonce: exchangeChallenge,
});
// EU residency verification
const euProof = await wallet.show({
policy: {
countryCode: {
in: ["AT", "BE", "BG", "HR", "CY", "CZ", "DK", "EE", "FI", "FR", "DE"],
},
},
nonce: serviceChallenge,
});API Reference
OpenAC.prepare(options)
Prepares a credential for zero-knowledge presentations. This is a one-time operation per credential (~6s).
Parameters:
{
credential: string; // SD-JWT credential
deviceBinding?: boolean; // Bind to device (default: false)
storage?: StorageAdapter; // Storage backend (default: memory)
}Returns: Promise<CredentialHandle>
Example:
const handle = await OpenAC.prepare({
credential: sdJwtString,
deviceBinding: true,
});handle.show(options)
Generates a zero-knowledge presentation proof (~100ms).
Parameters:
{
policy: PolicyPredicates; // What to prove
nonce: string; // Verifier challenge
disclosures?: string[]; // Optional explicit disclosures
}Returns: Promise<Presentation>
Example:
const proof = await handle.show({
policy: { age: { gte: 21 } },
nonce: verifierNonce,
});OpenAC.verify(proof, policy?, options?)
Verifies a zero-knowledge presentation proof (~34ms).
Parameters:
proof: Proof | SerializedProof; // Proof to verify
policy?: Policy; // Expected policy (optional)
options?: {
expectedNonce?: string; // Challenge nonce to verify
trustedIssuers?: string[]; // Allowed issuer URLs
}Returns: Promise<VerificationResult>
Example:
const result = await OpenAC.verify(proof, { age: { gte: 21 } }, { expectedNonce: verifierNonce });Policy Predicates
The SDK supports rich policy expressions:
Comparison Operators
{
age: { gte: 18 }, // Greater than or equal
age: { gt: 21 }, // Greater than
age: { lte: 65 }, // Less than or equal
age: { lt: 100 }, // Less than
}Equality
{
countryCode: "DE", // Exact match
nationality: "US",
}Set Membership
{
nationality: { in: ["DE", "FR", "IT"] }, // Must be in set
countryCode: { nin: ["KP", "IR"] }, // Must NOT be in set
}Range Queries
{
income: { gte: 50000, lt: 100000 }, // Between 50k and 100k
age: { gte: 18, lte: 65 }, // Between 18 and 65
}Combined Policies
{
age: { gte: 18 },
nationality: { in: ["US", "CA", "GB"] },
income: { gte: 50000 },
countryCode: { nin: ["KP", "IR", "SY"] },
}Storage Adapters
Memory Storage (Default)
import { MemoryAdapter } from "@l8zk/sdk";
const handle = await OpenAC.prepare({
credential,
storage: new MemoryAdapter(),
});IndexedDB (Browser)
import { IndexedDBAdapter } from "@l8zk/sdk";
const handle = await OpenAC.prepare({
credential,
storage: new IndexedDBAdapter("my-wallet"),
});Custom Storage
import { StorageAdapter } from "@l8zk/sdk";
class MyStorage implements StorageAdapter {
async get(key: string): Promise<string | null> {
// Your implementation
}
async set(key: string, value: string): Promise<void> {
// Your implementation
}
async delete(key: string): Promise<void> {
// Your implementation
}
async keys(): Promise<string[]> {
// Your implementation
}
async clear(): Promise<void> {
// Your implementation
}
}How It Works
The SDK uses a two-phase approach for efficient ZK proofs:
- First Run: Downloads circom artifacts (~33MB) to
~/.l8zk/circom/ - Subsequent Runs: Uses cached artifacts for instant startup
The native Rust binary (ecdsa-spartan2) is included in platform-specific npm packages and installed automatically.
Development Setup (Optional)
If you want to build from source or modify the circuits:
# 1. Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# 2. Install Circom
# See: https://docs.circom.io/getting-started/installation/
# 3. Initialize submodules
npm run submodule:init
# 4. Build circuits
cd wallet-unit-poc/circom
yarn && yarn compile:jwt && yarn compile:show
# 5. Build Rust binary
cd ../ecdsa-spartan2
cargo build --releasePerformance
Benchmarks on Apple M1 Pro:
| Operation | Time | Description | | -------------- | ------ | ---------------------- | | Prepare Setup | ~6s | One-time circuit setup | | Prepare Prove | ~3.6s | Generate prepare proof | | Prepare Verify | ~1.8s | Verify prepare proof | | Show Prove | ~100ms | Generate presentation | | Show Verify | ~34ms | Verify presentation |
Total end-to-end flow: ~13s (one-time) + ~100ms (per presentation)
Architecture
┌─────────────────────────────────────────────────────────────┐
│ L8ZK SDK │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Prepare │ │ Show │ │ Verify │ │
│ │ (Setup) │ │ (Present) │ │ (Check) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Native Backend │ │
│ │ (Spartan2 ZK) │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘Components
- Credential Parser: SD-JWT parsing and validation
- Prover: ZK proof generation (prepare/show phases)
- Verifier: Proof verification and policy checking
- Storage: Cross-platform credential storage
- Native Backend: Rust-based Spartan2 + ECDSA proving system
Examples
See the examples/ directory for complete working examples:
examples/age-verification/- Basic age verificationexamples/crypto-onboarding/- Crypto exchange KYCexamples/eu-residency/- EU residency verificationexamples/full-demo/- Complete end-to-end demo
Run examples:
npx tsx examples/full-demo/index.tsSecurity Considerations
Privacy Guarantees
- Zero-Knowledge: Only the policy satisfaction is revealed
- Unlinkability: Each proof is cryptographically reblinded
- No Correlation: Verifiers cannot link presentations from the same user
What Verifiers Learn
When proving age >= 18:
- ✅ Verifier knows: User satisfies the policy
- ❌ Verifier does NOT know: Exact age, birthdate, name, or other attributes
Threat Model
- Honest Verifier: Verifier follows protocol but may try to learn extra information
- Malicious Prover: Cannot create valid proofs for false statements
- Collusion: Multiple verifiers cannot link presentations
Best Practices
- Always use nonces: Prevents replay attacks
- Verify issuer: Check
trustedIssuerslist - Rotate credentials: Periodically refresh credentials
- Secure storage: Use encrypted storage for sensitive data
- Device binding: Enable for high-security use cases
Browser Support
The SDK works in modern browsers with WebAssembly support:
import { OpenAC } from "@l8zk/sdk";
import { IndexedDBAdapter } from "@l8zk/sdk";
// Browser usage
const handle = await OpenAC.prepare({
credential: sdJwtString,
storage: new IndexedDBAdapter("wallet"),
});
const proof = await handle.show({
policy: { age: { gte: 18 } },
nonce: challenge,
});React Native Support
import { OpenAC } from "@l8zk/sdk";
import AsyncStorage from "@react-native-async-storage/async-storage";
// Custom storage adapter for React Native
class RNStorage implements StorageAdapter {
async get(key: string) {
return await AsyncStorage.getItem(key);
}
async set(key: string, value: string) {
await AsyncStorage.setItem(key, value);
}
// ... implement other methods
}
const handle = await OpenAC.prepare({
credential,
storage: new RNStorage(),
});Troubleshooting
Native Backend Not Found
If you see "Native backend not available":
- Ensure you're on a supported platform (macOS or Linux, x64 or ARM64)
- Try reinstalling:
rm -rf node_modules && npm install - Check that the platform package was installed:
ls node_modules/@l8zk/
Circom Artifacts Download Failed
If artifact download fails:
- Check internet connection
- Clear cache and retry:
rm -rf ~/.l8zk/circom - The SDK will re-download on next run
Permission Denied (EACCES)
If you see binary permission errors:
- The postinstall script should fix this automatically
- Manual fix:
chmod +x node_modules/@l8zk/sdk-*/bin/ecdsa-spartan2
Performance Issues
For slow proof generation:
- First run downloads ~33MB of artifacts - subsequent runs are faster
- Proof generation is CPU-intensive (~6s for prepare, ~100ms for show)
- Consider caching prepared credentials
Contributing
We welcome contributions! See CONTRIBUTING.md for guidelines.
Development Setup
git clone https://github.com/rahulbarmann/l8zk
cd sdk
npm install
npm run submodule:init
npm testRunning Tests
npm test # Run all tests
npm run test:watch # Watch mode
npm run test:coverage # With coverageCredits
This SDK is built on the OpenAC protocol developed by the zkID team at Privacy and Scaling Explorations (PSE), Ethereum Foundation.
Core Technology
- Spartan: Transparent SNARK protocol (no trusted setup)
- Hyrax: Polynomial commitment scheme
- ECDSA: Signature verification in zero-knowledge
- SD-JWT: Selective Disclosure JSON Web Tokens
License
MIT License - see LICENSE for details.
Built on OpenAC by PSE / Ethereum Foundation - https://github.com/privacy-scaling-explorations/zkID
Support
- GitHub Issues: https://github.com/rahulbarmann/l8zk/issues
- Documentation: https://github.com/rahulbarmann/l8zk
- Examples: https://github.com/rahulbarmann/l8zk/tree/main/examples
Changelog
See CHANGELOG.md for version history and release notes.
