20 releases (5 breaking)
Uses new Rust 2024
| 0.6.0 | Apr 18, 2026 |
|---|---|
| 0.5.0 | Mar 12, 2026 |
| 0.3.4 | Dec 4, 2025 |
| 0.3.0 | Nov 29, 2025 |
| 0.2.0 | Jul 8, 2025 |
#2134 in Cryptography
5,120 downloads per month
Used in 23 crates
(5 directly)
555KB
12K
SLoC
affinidi-data-integrity
A production-grade implementation of the W3C Data Integrity specification for the Affinidi Trust Development Kit. Sign and verify cryptographic proofs over JSON and JSON-LD documents using classical (Ed25519) and post-quantum (ML-DSA, SLH-DSA) algorithms.
- Unified API — one
sign()/verify_with_public_key()/verify()entry point. - Post-quantum ready — ML-DSA-44 and SLH-DSA-SHA2-128s behind the
post-quantumfeature flag (W3Cdi-quantum-safev0.3, experimental). - First-class remote signers — KMS / HSM backends implement the same
Signertrait, with aprepare_sign_inputhelper for protocols that hash out-of-band. - Hybrid / multi-proof —
sign_multiandverify_multiwithVerifyPolicy::{RequireAll, RequireAny, RequireThreshold(n)}for witness schemes and gradual PQC migration. - Spec-shape validation —
verify_conformancecatches malformed-but-cryptographically-valid proofs separately from signature verification.
Cryptosuites
| Cryptosuite | Alg | Canonicalization | Feature | Status |
|---|---|---|---|---|
eddsa-jcs-2022 |
Ed25519 | JCS (RFC 8785) | (default) | W3C Rec |
eddsa-rdfc-2022 |
Ed25519 | RDFC-1.0 (URDNA2015) | (default) | W3C Rec |
bbs-2023 |
BBS+ / BLS12-381 | JCS (selective disclosure) | bbs-2023 |
W3C WD |
mldsa44-jcs-2024 |
ML-DSA-44 (FIPS 204) | JCS | ml-dsa / post-quantum |
Experimental |
mldsa44-rdfc-2024 |
ML-DSA-44 | RDFC-1.0 | ml-dsa / post-quantum |
Experimental |
slhdsa128-jcs-2024 |
SLH-DSA-SHA2-128s (FIPS 205) | JCS | slh-dsa / post-quantum |
Experimental |
slhdsa128-rdfc-2024 |
SLH-DSA-SHA2-128s | RDFC-1.0 | slh-dsa / post-quantum |
Experimental |
Default choice: eddsa-jcs-2022 — it is produced automatically for any Ed25519 Signer unless overridden via SignOptions::with_cryptosuite(...). Prefer JCS over RDFC for new designs; RDFC is ~4× slower and mainly needed for JSON-LD interop.
Feature flags
| Feature | Default | Enables |
|---|---|---|
bbs-2023 |
off | bbs-2023 cryptosuite (BLS12-381 selective disclosure) |
ml-dsa |
off | ML-DSA (FIPS 204) primitives + mldsa44-*-2024 cryptosuites |
slh-dsa |
off | SLH-DSA-SHA2-128s (FIPS 205) primitives + slhdsa128-*-2024 cryptosuites |
post-quantum |
off | umbrella — enables both ml-dsa and slh-dsa |
For verifiers: enable broadly. You don't pick what you're asked to verify, so you want the largest compatible surface.
For signers: enable narrowly. Pick the exact suites you produce, to reduce binary size and auditable attack surface.
Installation
[dependencies]
affinidi-data-integrity = "0.5"
# With post-quantum cryptosuites:
# affinidi-data-integrity = { version = "0.5", features = ["post-quantum"] }
Quickstart
use affinidi_data_integrity::{DataIntegrityProof, SignOptions, VerifyOptions};
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
# async fn demo() -> Result<(), affinidi_data_integrity::DataIntegrityError> {
let secret = Secret::generate_ed25519(Some("did:key:z6Mk...#key-0"), None);
let doc = json!({ "name": "Alice" });
// Sign — the library auto-picks `eddsa-jcs-2022` for Ed25519 keys.
let proof = DataIntegrityProof::sign(&doc, &secret, SignOptions::new()).await?;
// Verify — pass the public-key bytes directly.
proof.verify_with_public_key(&doc, secret.get_public_bytes(), VerifyOptions::new())?;
# Ok(()) }
Post-quantum — same API, ML-DSA key
# #[cfg(feature = "ml-dsa")]
# async fn pqc() -> Result<(), affinidi_data_integrity::DataIntegrityError> {
use affinidi_data_integrity::{DataIntegrityProof, SignOptions};
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
let secret = Secret::generate_ml_dsa_44(Some("did:key:zMl...#k"), None);
// Signer::cryptosuite() auto-selects mldsa44-jcs-2024 from the key type.
let proof = DataIntegrityProof::sign(&json!({"pqc": true}), &secret, SignOptions::new()).await?;
# Ok(()) }
Resolving the public key via DID
# async fn verify_via_did() -> Result<(), affinidi_data_integrity::DataIntegrityError> {
use affinidi_data_integrity::{DidKeyResolver, VerifyOptions};
# let proof: affinidi_data_integrity::DataIntegrityProof = todo!();
# let doc = serde_json::json!({});
// Works for did:key out-of-the-box; plug in a custom resolver for did:web / did:webvh.
proof.verify(&doc, &DidKeyResolver, VerifyOptions::new()).await?;
# Ok(()) }
Remote signer (KMS / HSM)
Implement the Signer trait — exactly the same trait that local keys use. See examples/remote_signer_ed25519.rs and examples/remote_signer_ml_dsa.rs for full worked examples with a mock signing service. For protocols that hash out-of-band, prepare_sign_input() returns the exact bytes the remote side must sign.
Multi-proof (hybrid migration, witness threshold)
# #[cfg(feature = "ml-dsa")]
# async fn multi() -> Result<(), affinidi_data_integrity::DataIntegrityError> {
use affinidi_data_integrity::{
DataIntegrityProof, DidKeyResolver, SignOptions, VerifyOptions,
VerifyPolicy, verify_multi,
};
use affinidi_data_integrity::signer::Signer;
# let classical: affinidi_secrets_resolver::secrets::Secret = todo!();
# let pqc: affinidi_secrets_resolver::secrets::Secret = todo!();
# let doc = serde_json::json!({});
// Ed25519 + ML-DSA proofs on the same credential.
let signers: Vec<&dyn Signer> = vec![&classical, &pqc];
let proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new()).await?;
// Accept the credential if at least one proof verifies — tolerates a
// verifier that only understands one of the two suites.
let result = verify_multi(&proofs, &doc, &DidKeyResolver, VerifyOptions::new(), VerifyPolicy::RequireAny).await;
result.into_result()?;
# Ok(()) }
Caching ML-DSA for issuer-scale workloads
ML-DSA signing expands the FIPS 204 matrix on every call (~80–100 µs for ML-DSA-44). For issuers signing thousands of credentials with the same key, wrap in CachingSigner:
use affinidi_data_integrity::CachingSigner;
let signer = CachingSigner::new(secret);
// First sign expands and caches; subsequent signs reuse the expanded key.
Benchmarks show ~33% latency reduction per sign for ML-DSA-44 on cached paths.
Performance
Apple M4 Pro, --release, cargo bench -p affinidi-data-integrity --features post-quantum:
| Cryptosuite | Sign | Sign (CachingSigner) | Proof size |
|---|---|---|---|
eddsa-jcs-2022 |
46 µs | — | 89 B |
eddsa-rdfc-2022 |
198 µs | — | 89 B |
mldsa44-jcs-2024 |
373 µs | 248 µs (‑33%) | ~3306 B |
mldsa44-rdfc-2024 |
~500 µs | ~375 µs (‑25%) | ~3306 B |
slhdsa128-jcs-2024 |
117 ms | — | ~10730 B |
CachingSigner<S> caches the expanded ML-DSA matrix after the first sign. Subsequent signs skip the ~80–100 µs re-expansion. Ed25519 and SLH-DSA don't have expansion-cacheable state.
SLH-DSA trades signature speed for tiny keys (32 B public, 64 B private) and stateless-hash security — use it when signing rarely and long-term unforgeability matters more than throughput.
Migration from ≤0.5.3 to 0.5.4
0.5.4 introduces a unified sign/verify API. The old entry points remain as #[deprecated] thin wrappers for one minor version (planned removal in 0.6.0). Breaking changes stay within pre-1.0 minor-version semantics and don't require downstream crate republishes that pin ^0.5.
| Old (0.5) | New (0.6) |
|---|---|
DataIntegrityProof::sign_jcs_data(&doc, ctx, &signer, created) |
DataIntegrityProof::sign(&doc, &signer, SignOptions::new().with_context(ctx).with_created(ts)) |
sign_jcs_data_with_suite(suite, ...) |
DataIntegrityProof::sign(..., SignOptions::new().with_cryptosuite(suite)) |
sign_rdfc_data(...) |
same as sign — RDFC is derived from the suite |
verify_data_with_public_key(...) |
proof.verify_with_public_key(&doc, &pk, VerifyOptions::new()) |
Also: DataIntegrityError gained structured variants (KeyTypeMismatch, InvalidSignature, etc.). The old InputDataError(String) / CryptoError(String) / VerificationError(String) / SecretsError(String) / RdfEncodingError(String) variants are kept as #[deprecated] for the same deprecation window.
See CHANGELOG.md for the full breaking-change list.
Security considerations
- Post-quantum suites are experimental. W3C
di-quantum-safeis v0.3 and explicitly "do not use in production"; NIST FIPS 204 / 205 are final, but the Data Integrity profile on top is still moving. Classical Ed25519 is the only option currently recommended for production-grade VCs. - Deterministic signing. All supported suites (Ed25519, ML-DSA, SLH-DSA) sign deterministically in this crate — the same input produces the same signature. This is required for W3C interop and for reproducible test vectors. If you need randomised signatures for side-channel reasons, drop to the underlying primitive crates directly.
- Zeroize coverage. Private key bytes held by
Secretare zeroized on drop via itsZeroizeOnDropderive. The ML-DSA crate zeroizes its expanded matrix (we enable itszeroizefeature). Intermediate stack copies inside this crate are wrapped inZeroizing. Your ownSignerimplementations should match this. - Timing channels. Ed25519 and ML-DSA signing are constant-time in the underlying RustCrypto crates. SLH-DSA has branching tied to public material only. BBS-2023 has selective-disclosure-specific side-channel considerations — see its spec.
- See SECURITY.md for vulnerability reporting.
Out of scope
- JOSE / JWS / JWT post-quantum. Waiting for IETF draft stability. When those standards land they will live in sibling crates (
affinidi-data-integrity-jose, etc.), not this crate. - COSE / mdoc post-quantum. Same — waiting on IETF.
- FALCON, SQIsign, HAWK, Kyber/ML-KEM. Either not NIST-finalised or not in the current
di-quantum-safecryptosuite set.
Related crates
affinidi-crypto— classical and post-quantum primitives.affinidi-encoding— multicodec / multibase.affinidi-secrets-resolver— key material and multikey codec.affinidi-rdf-encoding— RDFC-1.0 canonicalization.
License
Dependencies
~16–24MB
~358K SLoC