zk_jwt 1.0.0
zk_jwt: ^1.0.0 copied to clipboard
Selective Disclosure JWT with a ZK-friendly Poseidon hash, backed by prebuilt Rust native binaries. No Rust toolchain required to consume it.
Affinidi ZK-JWT #
The Affinidi ZK-JWT package provides a Dart SDK for ZK-JWT, a Selective Disclosure JWT format that replaces SHA-256 with the ZK-friendly Poseidon hash. This makes disclosure digests directly usable as inputs to zero-knowledge proof circuits, so a holder can prove properties about a claim (e.g. balance ≥ 1000) without revealing the claim value itself, while still supporting classic SD-JWT-style selective disclosure.
⚠️ IMPORTANT SECURITY AND PRIVACY NOTE: This package is a cryptographic tool and does not process personal data outside of the structured data defined by the user. When integrated into a broader system that handles personally identifiable information (PII), users are solely responsible for ensuring that the entire use case complies with all applicable privacy laws and data protection obligations (e.g., GDPR).
Specification:
Before integrating, we encourage you to read the ZK-JWT Specification. It documents the credential format, the poseidon-v1 digest algorithm, the presentation protocol, and the zero-knowledge proof extension in detail.
Table of Contents #
- Core Concepts
- ZK-JWT Workflow Overview
- Supported Crypto
- Disclosure & Digest Format
- Requirements
- Installation
- Usage
- Testing & Native Symbols
- Support & Feedback
- Contributing
Core Concepts #
ZK-JWT extends the SD-JWT (Selective Disclosure JWT) model with a ZKP-friendly digest algorithm.
- Selective Disclosure JWT (SD-JWT): A JWT whose claims are individually hidden behind digests in a
_sdarray; the holder discloses only the claims (and their salts) a verifier needs to see. - Poseidon Hash: A permutation-based hash function optimized for algebraic circuit mathematics, used here instead of SHA-256 so digests can be recomputed cheaply inside a Circom/Groth16 circuit.
- Disclosure: A base64url-encoded
[salt, claimName, claimValue]array (or[salt, claimValue]for array elements). Its Poseidon digest is what appears in_sd. - Zero-Knowledge Proof (zk-proof): An optional, additional proof attached to a presentation that shows a disclosed claim satisfies a predicate (e.g. a threshold) without revealing the claim value.
ZK-JWT Workflow Overview #
The lifecycle of a ZK-JWT credential involves three roles:
- Issuer Flow (
createZKJWT): The issuer processes the payload's claims into Poseidon-hashed disclosures, builds the_sdarray andposeidon-v1-tagged JWT payload, and signs it with a caller-supplied signing function. - Holder Flow (
createPresentation/extractDisclosedClaims): The holder selects which disclosures to reveal for a given verifier, optionally attaches zero-knowledge proofs, and produces the presentation string. - Verifier Flow (
verifyPresentation): The verifier checks the JWT signature, validates every disclosed digest against_sd, optionally validates attached zk-proofs against a verification key, and returns the recovered claims.
Supported Crypto #
- Hash: Poseidon, computed over the BN254 scalar field.
- Crypto Engine: Poseidon hashing is performed via a Rust Foreign Function Interface (FFI) bridge (
affinidi-zkp-crypto-rs), the same native engine used byaffinidi-vc-zkp-dart. This ensures optimal performance, memory safety, and reliable integration into Dart/Flutter applications. - JWT signing: Left to the caller via
SignFunction/VerifyFunctioncallbacks - ZK-JWT does not mandate a specific signature algorithm.
Disclosure & Digest Format #
Every claim disclosure is hashed deterministically so digests are stable across languages and runtimes:
digest = Poseidon([claimNameDigest, salt, value])
Where:
| Component | Description |
|---|---|
claimNameDigest |
SHA-256(claimName), used to bind the digest to a specific claim name. |
salt |
Random per-disclosure salt (base64url), converted to a field element. |
value |
The claim value, converted to a field element (numbers/booleans map directly; strings and structured values are SHA-256 hashed). |
The resulting decimal-string digest is what appears in the JWT payload's _sd array, and is exactly what a Circom circuit recomputes to validate a disclosure or zero-knowledge predicate.
Requirements #
- Dart SDK version
^3.10.0(required for native-asset build hooks).
Installation #
Add the package to your pubspec.yaml file:
dependencies:
zk_jwt: ^<version_number>
Then run the command below to install the package:
dart pub get
Usage #
1. Issuer: Create a ZK-JWT Credential #
import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'package:zk_jwt/zk_jwt.dart';
Future<Uint8List> signJwt(String data) async {
// Replace with a real signer (ES256, EdDSA, ...).
return Uint8List.fromList(sha256.convert(utf8.encode(data)).bytes);
}
final payload = <String, dynamic>{
'iss': 'https://example.com/issuer',
'given_name': 'John',
'family_name': 'Doe',
'email': 'john.doe@example.com',
};
final issued = await createZKJWT(payload, signJwt);
final zkJwt = issued['zkJwt'] as String; // '<jwt>~<disclosure1>~<disclosure2>~...'
2. Holder: Create a Presentation #
The holder discloses only the claims a specific verifier needs.
final presentation = await createPresentation(zkJwt, <String>[
'given_name',
'email',
]);
Zero-knowledge proofs can be attached alongside disclosures:
final presentationWithProof = await createPresentation(
zkJwt,
<String>[], // no plain disclosures
<Map<String, dynamic>>[
<String, dynamic>{
'proof': proof,
'publicSignals': publicSignals, // [digest, claimNameDigest, ...]
'verificationKeyRefernce': 'vkey-balance-threshold',
'claimName': 'balance',
},
],
);
3. Verifier: Verify a Presentation #
Future<bool> verifyJwt(String jwt) async {
// Replace with real signature verification.
return jwt.split('.').length == 3;
}
final result = await verifyPresentation(presentation, verifyJwt);
if (result['valid'] == true) {
final claims = result['claims'] as Map<String, dynamic>;
print(claims['given_name']); // John
}
To also validate attached zero-knowledge proofs, supply pullVkeyFunction and verifyZkproof:
final result = await verifyPresentation(
presentation,
verifyJwt,
pullVkeyFunction: (reference) async => loadVerificationKey(reference),
verifyZkproof: (vkey, publicSignals, proof) async =>
myGroth16Verifier.verify(vkey, publicSignals, proof),
);
Testing & Native Symbols #
This project includes two test suites:
- Unit tests: Pure-Dart tests for encoding, field-element conversion, and disclosure/digest logic.
- Integration tests: Exercise the real Rust Poseidon FFI bridge end to end (hashing, full issuer → holder → verifier flow, zk-proof validation).
Run unit tests:
dart test
Run all tests (unit + integration):
dart test --run-skipped
Run with coverage:
dart run coverage:test_with_coverage -- --run-skipped
Debugging Native Crashes: If your application crashes within the native Rust code, download the necessary debug symbols for your specific build triple:
./tool/download_prebuild_symbols.sh --output-dir ./.native-symbols
See doc/native_build_and_hooks.md for details on how the native library is resolved, verified, and bundled by hook/build.dart.
Support & Feedback #
If you encounter any technical issues or have suggestions regarding the specification or implementation, please don't hesitate to contact us using this link.
Reporting Technical Issues #
For issues with the codebase, please open a detailed issue on GitHub. Include a title, clear description, and, ideally, an executable code sample demonstrating the failure.
Contributing #
Want to contribute?
Please review our CONTRIBUTING guidelines. We welcome contributions to improving the ZK-JWT specification and tooling.