Status: pre-launch. This evidence reflects implemented code and deployed infrastructure. Provii is not yet serving end-user production traffic, so production operational metrics and audit history are not yet available.
Cryptographic Implementation Evidence
Document Version: 1.0
Last Updated: 2026-02-14
Scope: UC-071 through UC-082 (12 cryptographic controls)
Primary Repository: provii-crypto/
Executive Summary
This document provides evidence for all 12 cryptographic controls (UC-071 through UC-082) across the Provii cryptographic implementation. The evidence demonstrates strong implementation of zero knowledge proof systems, key management, commitment schemes, and signature schemes.
Key Findings
Strengths:
- Production-grade Groth16 ZK-SNARK implementation using bellman library
- Cryptographically secure randomness sources (OsRng, getrandom)
- Pedersen commitment scheme with proper domain separation
- Custom RedJubjub signature scheme with deterministic nonces
- Extensive test coverage including property-based testing
- 25 fuzzing targets covering all cryptographic attack surfaces
- Proper key generation tooling with security validations
Critical Security Notes:
- Custom RedJubjub implementation is NOT Zcash-compatible (by design)
- Trusted setup parameters require secure generation (toxic waste handling)
- No formal cryptographic audit completed yet (UC-082 gap)
Control Status Summary
| Control | Status | Key Evidence |
|---|---|---|
| UC-071: Cryptographic Standards | β IMPLEMENTED | Groth16, BLS12-381, JubJub curve |
| UC-072: Key Management | β IMPLEMENTED | Issuer key generation, JWKS distribution |
| UC-073: Key Generation | β IMPLEMENTED | OsRng for all key material |
| UC-074: Key Storage | π PARTIAL | Documented recommendations, no HSM enforcement |
| UC-075: Key Rotation | β IMPLEMENTED | Key rotation via provii-issuer POST /admin/keys/rotate |
| UC-076: Trusted Setup | β IMPLEMENTED | Parameter generation with fingerprinting |
| UC-077: Circuit Security | β IMPLEMENTED | Constraint system with validation |
| UC-078: Randomness Sources | β IMPLEMENTED | OsRng, getrandom for Wasm |
| UC-079: Signature Security | β IMPLEMENTED | RedJubjub with deterministic nonces |
| UC-080: Commitment Security | β IMPLEMENTED | Pedersen with 128-bit randomness |
| UC-081: Proof System Security | β IMPLEMENTED | Groth16 soundness, zero knowledge |
| UC-082: Third-Party Audit | β GAP | No formal audit completed |
Control Evidence
UC-071: Cryptographic Standards Compliance
Status: β IMPLEMENTED
Requirement: Use industry-standard, peer-reviewed cryptographic algorithms and protocols. Avoid custom or unproven cryptography.
Evidence:
1. Groth16 ZK-SNARK System
Standard: Groth16 proof system (industry standard for ZK-SNARKs)
Library: bellman version 0.14.0
Reference: provii-crypto/Cargo.toml
bellman = { version = "0.14", default-features = false }
bls12_381 = "0.8"
Implementation: provii-crypto/crypto-circuit-age/src/lib.rs
The implementation uses the standard Groth16 proving system with BLS12-381 pairing-friendly elliptic curve, which is widely peer-reviewed and used in production systems (Zcash, Filecoin).
2. BLS12-381 Elliptic Curve
Standard: BLS12-381 pairing-friendly curve Security Level: ~128-bit security Usage: Pairing curve for Groth16 proofs
3. JubJub Elliptic Curve
Standard: JubJub twisted Edwards curve (embedded in BLS12-381 scalar field)
Library: jubjub crate
Usage: RedJubjub signatures, Pedersen commitments
Reference: Used in Zcash Sapling protocol
4. Pedersen Commitments
Standard: Pedersen commitment scheme (computationally binding, perfectly hiding)
Implementation: provii-crypto/crypto-commit/src/lib.rs:39-66
pub fn pedersen_commit_dob(dob_days: i32, r_bits: &[bool]) -> [u8; 32] {
// Bias dob_days for unsigned comparison in circuit
let biased = bias_for_circuit(dob_days);
// Convert biased value to little-endian bits.
let mut dob_bits = vec![];
let mut value = biased;
for _ in 0..32 {
dob_bits.push((value & 1) != 0);
value >>= 1;
}
// Concatenate the date bits with the randomness bits.
let mut in_bits = dob_bits;
in_bits.extend_from_slice(r_bits);
// Hash with the Pedersen generator set used by the circuit.
let point = pedersen_hash(
Personalization::NoteCommitment,
in_bits.into_iter()
);
// Convert the resulting point to bytes.
point.to_bytes()
}
Uses the same Pedersen hash implementation as Zcash (sapling_crypto::pedersen_hash).
5. Hash Functions
BLAKE2s-256: (IETF RFC 7693). 256-bit output, collision-resistant Usage: Signature domain separation, VK fingerprinting, parameter hashing
SHA-256: Used for RP challenge computation (rp_challenge = SHA-256(origin || nonce || DST))
Usage: Relying party challenge binding in verification flow
The RP binding pipeline has two distinct steps:
- rp_challenge = SHA-256(origin || nonce || βzerokp.challenge.v1β), which binds the relying party
- rp_hash = Blake2s-256(rp_challenge), which produces the 256-bit circuit public input
SHA-256 was chosen for step 1 because it is widely available in browser WebCrypto (SubtleCrypto.digest). Blake2s-256 was chosen for step 2 because it is efficient inside the Groth16 arithmetic circuit.
Important Exception - Custom RedJubjub:
While most cryptography is standards-based, Provii implements a custom RedJubjub variant that is explicitly NOT compatible with Zcash:
Source: provii-crypto/crypto-sig-redjubjub/src/lib.rs:1-10
//! RedJubjub-like signatures for Provii credentials (v2)
//!
//! β οΈ This is a custom scheme inspired by RedJubjub. It is **not** Zcash-compatible.
//! We operate strictly in the prime-order subgroup (`SubgroupPoint`) on Jubjub.
//! Domain separation is done by prefixing BLAKE2s-256 inputs with fixed tags.
//!
//! CRITICAL: This implementation aligns scalar field handling with the circuit:
//! - Challenge computation uses Jubjub scalar field reduction
//! - Signature scalars remain in Jubjub scalar field
//! - The circuit must match this exact reduction
Justification: Custom signature scheme is needed for circuit compatibility (scalar field alignment).
Findings:
- Primary cryptography uses industry-standard algorithms (Groth16, BLS12-381, JubJub, Pedersen, BLAKE2s)
- One intentional exception: custom RedJubjub for circuit compatibility
- All standard components use peer-reviewed libraries
Gaps:
- Custom RedJubjub should be formally audited (see UC-082)
- Document rationale for RedJubjub customization
UC-072: Key Management
Status: β IMPLEMENTED
Requirement: Implement secure key lifecycle management including generation, distribution, storage, rotation, and destruction.
Evidence:
1. Issuer Signing Key Generation
Tool: provii-crypto/tools/keygen/src/main.rs
Key generation tool implements:
- Cryptographically secure random key generation (OsRng)
- Key validation (on-curve, non-zero checks)
- Multiple export formats (hex, base64url)
- Test signature verification before deployment
// Generate a new keypair.
println!("π Generating new keypair...");
let (sk_bytes, vk_bytes) = generate_keypair();
// Encode the key material for storage.
let sk_b64 = URL_SAFE_NO_PAD.encode(&sk_bytes);
let vk_b64 = URL_SAFE_NO_PAD.encode(&vk_bytes);
2. Verification Key Distribution
Mechanism: JWKS (JSON Web Key Set)
Distribution: Served from provii-issuer at /v1/jwks.json and /.well-known/jwks.json
JWKS Entry Format:
{
"kty": "OKP",
"crv": "JUBJUB",
"kid": "provii:2025-09",
"use": "sig",
"alg": "RedJubjub",
"x": "<base64url-encoded-vk>",
"name": "Provii Issuer (September 2025)",
"status": "active"
}
3. Key Formats
Signing Key (Secret):
- Length: 32 bytes
- Format: JubJub scalar field element
- Encoding: Base64url (for storage), hex (for debugging)
- Storage: Cloudflare KV with key
issuer:{issuer_kid}:key:{kid}
Verification Key (Public):
- Length: 32 bytes
- Format: Compressed JubJub subgroup point
- Encoding: Base64url
- Distribution: JWKS, embedded in credentials
4. Key Identifier (kid) Scheme
Format: provii:YYYY-MM
Example: provii:2025-09
Enables:
- Key rotation tracking
- Multi-key support for gradual rotation
- Credential key binding (kid in signed credential)
5. Key Storage Recommendations
Documentation: provii-crypto/tools/keygen/src/main.rs:100-104
println!("\nπ Next Steps:");
println!("1. Store the KV entry in ISSUER_KEYS namespace");
println!("2. Add the JWKS entry via provii-issuer /v1/jwks.json endpoint");
println!("3. The raw VK bytes are used directly in proofs (no hashing)");
println!("4. Re-issue credentials with the new keypair");
Storage Backend: Cloudflare Workers KV (encrypted at rest by Cloudflare)
6. Key Rotation Support
Mechanism: Multi-key JWKS with KeyStatus enum (Active/Deprecated/Disabled)
Implementation: provii-issuer/src/key_rotation.rs. POST /admin/keys/rotate endpoint
Process:
- Generate new keypair with new
kidvia rotation endpoint - New key status set to
Active; old key status changed toDeprecated - Start issuing credentials with new key
- Keep deprecated key active for verification of existing credentials
- Mark old key as
Disabledafter grace period (default 90 days) - Remove old key after all credentials expire
Status: Fully implemented in provii-issuer with automated rotation endpoint.
Findings:
- Complete key generation tooling with security validations
- JWKS-based verification key distribution
- Key identifier scheme supports rotation
- Storage recommendations documented
Gaps:
- No HSM integration for signing key protection (see UC-074)
- No automated key destruction procedures
UC-073: Key Generation
Status: β IMPLEMENTED
Requirement: Generate all cryptographic keys using cryptographically secure random number generators.
Evidence:
1. Issuer Signing Key Generation
Source: provii-crypto/crypto-sig-redjubjub/src/lib.rs:97-103
/// Generate a new random signing key using a CSPRNG.
pub fn random() -> Self {
let mut rng = OsRng;
let scalar = JubjubScalar::random(&mut rng);
println!("DEBUG [Off-circuit]: Generated random signing key");
SigningKey { scalar }
}
RNG: OsRng (OS-provided cryptographically secure RNG)
2. Circuit Parameter Generation
Source: provii-crypto/crypto-circuit-age/examples/gen_params.rs:221-222
let mut rng = OsRng;
let params: Parameters<Bls12> = generate_random_parameters::<Bls12, _, _>(param_gen_circuit, &mut rng)?;
RNG: OsRng for toxic waste generation during trusted setup
3. Commitment Randomness Generation
Source: provii-crypto/crypto-commit/src/lib.rs:86-98
/// Generate random bits for commitment randomness.
pub fn generate_commitment_randomness<R: RngCore>(rng: &mut R, num_bits: usize) -> Vec<bool> {
let mut bits = Vec::with_capacity(num_bits);
let mut byte = 0u8;
for i in 0..num_bits {
if i % 8 == 0 {
byte = rng.next_u32() as u8;
}
bits.push(((byte >> (i % 8)) & 1) == 1);
}
bits
}
Usage: Accepts any RngCore implementer, typically called with OsRng or thread_rng() (which uses OsRng on modern systems).
4. Nonce Generation
Source: provii-crypto/crypto-protocol/src/nonce.rs:6-25
/// Generate a secure random nonce using a cryptographically secure RNG.
pub fn generate_nonce() -> [u8; 32] {
let mut bytes = [0u8; 32];
#[cfg(target_arch = "wasm32")]
{
// Use `getrandom`, which hooks into the Web Crypto API.
getrandom::getrandom(&mut bytes).expect("failed to generate random nonce");
}
#[cfg(not(target_arch = "wasm32"))]
{
// Use `OsRng` instead of `thread_rng` to maintain cryptographic security.
use rand::rngs::OsRng;
use rand::RngCore;
OsRng.fill_bytes(&mut bytes);
}
bytes
}
RNG Sources:
- Native platforms:
OsRng(uses OS entropy) - WebAssembly:
getrandom(uses Web Crypto API)
5. Entropy Validation
Source: provii-crypto/crypto-protocol/src/nonce.rs:27-45
/// Validate that the nonce has sufficient entropy (not all zeros).
pub fn validate_nonce(nonce: &[u8; 32]) -> bool {
nonce.iter().any(|&b| b != 0)
}
/// Check that the nonce has sufficient entropy (at least eight unique bytes).
pub fn has_sufficient_entropy(nonce: &[u8; 32]) -> bool {
let mut seen = [false; 256];
let mut uniq = 0u8;
for &b in nonce {
if !seen[b as usize] {
seen[b as usize] = true;
uniq += 1;
}
}
uniq >= 8
}
Validation: Nonces must have at least 8 unique bytes (prevents weak RNG detection).
6. Test-Only Deterministic RNG
Source: provii-crypto/crypto-sig-redjubjub/src/lib.rs:105-110
/// Generate with provided RNG (useful for tests).
pub fn random_with_rng<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
SigningKey {
scalar: JubjubScalar::random(rng),
}
}
Usage: Test code can use seeded RNG for reproducibility, but requires CryptoRng trait bound to prevent misuse.
Findings:
- All production key generation uses
OsRng(cryptographically secure) - WebAssembly uses
getrandomβ Web Crypto API - Nonce validation checks for sufficient entropy
- Test code properly isolated with trait bounds
Gaps: None identified
UC-074: Key Storage
Status: π PARTIAL
Requirement: Store cryptographic keys securely using hardware security modules (HSMs) or encrypted key stores. Protect keys from unauthorized access.
Evidence:
1. Current Storage Implementation
Backend: Cloudflare Workers KV
Key: issuer:{issuer_kid}:key:{kid}
Value:
{
"sk": "<base64url-encoded-signing-key>",
"vk": "<base64url-encoded-verification-key>"
}
Security Properties:
- Encryption at rest (Cloudflare managed)
- Access control via Cloudflare Workers bindings
- No direct internet access to KV store
2. Storage Recommendations
Source: provii-crypto/README.md:111-118
## Security Notes
- This workspace targets production-grade cryptography. Treat keys and
transcripts as sensitive.
- The RedJubjub variant implemented here is custom and specific to Provii. It
is **not** compatible with the Zcash reference implementation.
- Groth16 proving requires sound parameter generation; never reuse parameters
across distinct circuits.
3. Key Access Controls
Access Pattern:
- Signing key: Only accessed by provii-issuer (Cloudflare Worker)
- Verification key: Public (distributed via JWKS)
Worker Binding: Issuer service has KV binding to ISSUER_KEYS namespace
4. HSM Recommendations (Not Implemented)
Documentation Gap: No explicit HSM integration or recommendations documented.
Industry Best Practice:
- Use HSM for signing key storage in production
- Options: AWS CloudHSM, Azure Key Vault HSM, YubiHSM
- Keep signing keys in HSM, never export
Findings:
- Current storage uses Cloudflare KV (encrypted at rest, access-controlled)
- No HSM integration for signing key protection
- Verification keys appropriately public
Gaps:
- No HSM integration (HIGH PRIORITY for production)
- Document HSM integration recommendations
- Consider AWS KMS or Azure Key Vault for issuer signing keys
- No key backup/recovery procedures documented
Recommendations:
- Implement HSM storage for issuer signing keys
- Document key backup and recovery procedures
- Implement key access logging
- Regular key access audits
UC-075: Key Rotation
Status: β IMPLEMENTED
Requirement: Implement key rotation procedures to limit the lifetime of cryptographic keys.
Evidence:
1. Key Identifier Scheme Supports Rotation
Format: provii:YYYY-MM
Design:
- Time-based key identifiers
- Credentials include
kidin signed message - Verifiers can look up correct key by
kid
Source: provii-crypto/tools/keygen/src/main.rs:31-32
let kid = format!("provii:{}", chrono::Utc::now().format("%Y-%m"));
let kv_key = format!("issuer:{}:key:{}", issuer_kid, kid);
2. JWKS Supports Multiple Keys
JWKS Structure:
{
"keys": [
{
"kid": "provii:2025-09",
"x": "<vk1>",
"status": "active"
},
{
"kid": "provii:2025-10",
"x": "<vk2>",
"status": "active"
},
{
"kid": "provii:2025-08",
"x": "<vk_old>",
"status": "deprecated"
}
]
}
Rotation Process (Implemented via POST /admin/keys/rotate):
- Call rotation endpoint to generate new keypair with new
kid - New key marked
Active; old key automatically set toDeprecated - Issuer signs new credentials with active key
- Grace period: Both active and deprecated keys valid for verification
- Mark old key as
Disabledafter grace period (default 90 days) - After all credentials expire, remove old key
3. Credential Expiration Supports Rotation
Credential Lifetime: Controlled by iat (issued at) and exp (expiration) timestamps
Source: provii-crypto/crypto-circuit-age/examples/gen_params.rs:115-117
// Valid timestamps
let iat = 1735000000u64; // ~Dec 2024
let exp = 1766536000u64; // ~Dec 2025
Strategy: Issue credentials with limited lifetime, rotate keys annually (365-day default)
4. Rotation Configuration
Implemented:
- Key rotation frequency: 365-day default
- Grace period duration: 30 days (deprecated keys remain valid)
- Rotation endpoint: POST
/admin/keys/rotate - KeyStatus lifecycle: Active β Deprecated β Disabled
Findings:
- Key rotation fully implemented in provii-issuer
- Multi-key JWKS with KeyStatus enum
- Automated rotation via admin API endpoint
Gaps:
- No emergency key rotation procedures documented
- No rotation monitoring/alerting
Recommendations:
- Document emergency rotation runbook
- Add monitoring for key age/status
- Test rotation process in staging environment
UC-076: Trusted Setup (Groth16)
Status: β IMPLEMENTED
Requirement: Perform secure trusted setup ceremony for Groth16 parameters. Handle toxic waste appropriately.
Evidence:
1. Parameter Generation Tool
Source: provii-crypto/crypto-circuit-age/examples/gen_params.rs
Process:
eprintln!("\nπ§ Generating trusted setup parameters...");
eprintln!(" This may take several minutes for large circuits.");
eprintln!(" Using shape-only circuit (no witness) for parameter generation...");
// Create a circuit without witness for parameter generation
let param_gen_circuit = AgeCircuit {
public: public.clone(),
witness: None, // No witness during param gen!
};
let mut rng = OsRng;
let params: Parameters<Bls12> = generate_random_parameters::<Bls12, _, _>(param_gen_circuit, &mut rng)?;
Key Properties:
- Uses
OsRngfor randomness (toxic waste) - No witness baked into parameters (circuit shape only)
- Generates both proving key (PK) and verifying key (VK)
2. Parameter Fingerprinting
VK Fingerprint Computation:
/// Compute a Blake2s fingerprint of the verifying key.
fn vk_fingerprint(vk: &VerifyingKey<Bls12>) -> [u8; 32] {
let mut bytes = Vec::new();
vk.write(&mut bytes).expect("Failed to serialize verifying key");
let mut hasher = Blake2s256::new();
hasher.update(&bytes);
hasher.finalize().into()
}
/// Compute VK ID using domain separator
fn compute_vk_id(vk: &VerifyingKey<Bls12>) -> u32 {
let mut bytes = Vec::new();
vk.write(&mut bytes).expect("Failed to serialize verifying key");
let mut hasher = Blake2s256::new();
hasher.update(b"zerokp.vk.id.v1");
hasher.update(&bytes);
let result = hasher.finalize();
u32::from_le_bytes([result[0], result[1], result[2], result[3]])
}
Purpose: Detect parameter tampering, ensure prover and verifier use matching parameters
3. Circuit Constants Hash
Source: provii_crypto_circuit_age::compute_circuit_constants_hash()
let constants_hash = compute_circuit_constants_hash();
eprintln!("\nπ Circuit constants hash: {}", constants_hash);
Purpose: Fingerprint circuit structure (domain separators, generators, etc.)
4. Parameter Validation
Self-Test After Generation:
// FIRST: Do the satisfiability check
{
use bellman::gadgets::test::TestConstraintSystem;
let test_circ = AgeCircuit {
public: test_public.clone(),
witness: Some(test_witness.clone()),
};
let mut tcs = TestConstraintSystem::<bls12_381::Scalar>::new();
test_circ.synthesize(&mut tcs).expect("synthesis in test constraint system");
if !tcs.is_satisfied() {
eprintln!("\nβ Circuit not satisfied!");
if let Some(unsatisfied) = tcs.which_is_unsatisfied() {
eprintln!(" Failed constraint: {}", unsatisfied);
}
eprintln!(" Total constraints: {}", tcs.num_constraints());
std::process::exit(1);
} else {
eprintln!("\nβ
All constraints satisfied in test");
}
}
// THEN: Create proof and verify
match create_random_proof(test_circuit, ¶ms, &mut rng) {
Ok(proof) => {
eprintln!(" β
Proof generation successful");
let pvk = prepare_verifying_key(¶ms.vk);
let public_inputs = assemble_public_inputs_canonical(...);
match verify_proof(&pvk, &proof, &public_inputs) {
Ok(()) => {
eprintln!("\n β
Proof verification successful");
eprintln!(" β
Parameters are valid and can create verifiable proofs!");
}
Err(e) => {
eprintln!("\n β FATAL: Proof verification failed: {:?}", e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!(" β FATAL: Could not create proof with generated parameters!");
std::process::exit(1);
}
}
Validation Steps:
- Constraint satisfiability check
- Test proof generation
- Proof verification
- Exit with error if any step fails
5. Parameter Manifest
Generated Metadata:
{
"vk_id": 12345678,
"vk_fingerprint_blake2s": "abc123...",
"circuit_constants_hash": "def456...",
"pk_filename": "age_pk.12345678.bin",
"pk_size_bytes": 45678901,
"pk_blake2s_hash": "789abc...",
"vk_filename": "vk.12345678.bin",
"vk_size_bytes": 1234,
"shape": {
"kid_bytes": 14,
"schema_bytes": 12,
"constraints": 99084,
"public_inputs": 8
},
"generated_at": "2025-01-15T12:34:56Z",
"bellman_version": "0.14.0"
}
6. Toxic Waste Handling
Current Implementation:
- Randomness generated by
OsRng - Randomness discarded after parameter generation
- No persistent storage of toxic waste
Multi-Party Ceremony (Not Implemented):
Documentation: Not explicitly recommended in codebase
Industry Best Practice:
- Single-party setup acceptable for low-value applications
- Multi-party ceremony recommended for high-value applications
- Powers of Tau ceremonies for production systems
Findings:
- Complete parameter generation tooling
- Fingerprinting and validation
- Self-testing after generation
- Toxic waste properly discarded
Gaps:
- No multi-party ceremony support (LOW PRIORITY for current use case)
- Document recommended setup procedure
- No parameter versioning/migration strategy
Recommendations:
- Document parameter generation procedures
- For production: Consider multi-party ceremony
- Implement parameter versioning
- Create parameter backup/recovery procedures
UC-077: Circuit Security
Status: β IMPLEMENTED
Requirement: Design and implement ZK circuits with proper constraint systems, boundary checks, and validation.
Evidence:
1. Age Verification Circuit Structure
Source: provii-crypto/crypto-circuit-age/src/lib.rs
Circuit Constraints:
- Total constraints: ~99,084
- Public inputs: 8 field elements (direction, cutoff_days, rp_hash(2), issuer_vk(2), nullifier(2))
- Witness inputs: DOB, randomness, signature, credential fields
2. Constraint System Components
Key Constraints:
- Age Check:
cutoff_days >= dob_days - Commitment Verification: Pedersen commitment check
- Signature Verification: RedJubjub signature verification
- Nullifier Derivation: Pedersen-based nullifier
- Domain Separation: RP challenge binding
3. Constraint Satisfiability Testing
Source: provii-crypto/crypto-circuit-age/tests/layout_guard.rs
#[test]
fn test_circuit_satisfiability() {
use bellman::gadgets::test::TestConstraintSystem;
let test_circ = AgeCircuit {
public: test_public,
witness: Some(test_witness),
};
let mut tcs = TestConstraintSystem::<bls12_381::Scalar>::new();
test_circ.synthesize(&mut tcs).expect("synthesis");
if !tcs.is_satisfied() {
if let Some(unsatisfied) = tcs.which_is_unsatisfied() {
panic!("Failed constraint: {}", unsatisfied);
}
}
assert!(tcs.is_satisfied());
}
Testing: Every circuit change tested for constraint satisfiability
4. Public Input Validation
Source: provii-crypto/crypto-public-inputs/src/lib.rs
Bit 254 Safety:
The BLS12-381 scalar field is 255 bits, but only 254 bits are safe to pack:
// Enforce bit 254 = 0 for all chunks to ensure safe field element packing
// BLS12-381 scalar field is 255 bits, but we only use 254 bits per element
Validation: All public inputs checked to ensure bit 254 = 0
5. Circuit Optimisations
Documented Optimisations:
- RP Hash Off-Circuit: Compute
rp_hash = Blake2s-256(rp_challenge)whererp_challenge = SHA-256(origin || nonce || DST)outside circuit
- Saves ~20,000 constraints
- Direct VK Bytes: Use raw VK bytes instead of hash
- Saves ~10,000 constraints
- Pedersen Nullifier: Replace Blake2s with Pedersen hash
- Saves ~25,000 constraints
Total Savings: ~55,000 constraints from key optimisations (current circuit: ~99,084 constraints)
6. Circuit Shape Validation
Source: provii-crypto/crypto-circuit-age/tests/layout_guard.rs
Shape Parameters:
kidlength: Exactly 14 bytesschemalength: Exactly 12 bytes- Public inputs: Exactly 8 elements (
PUBLIC_INPUTS_LEN: usize = 8)
Validation: Tests ensure circuit shape remains constant (prevents parameter mismatch)
7. Property-Based Testing
Source: provii-crypto/crypto-commit/src/lib.rs:507-594
proptest! {
/// Property: pedersen_commit_dob hiding property (different randomness produces different commitment)
#[test]
fn prop_pedersen_commit_dob_hiding_property(
dob_days in 0i32..50000,
r_bits1 in prop::collection::vec(any::<bool>(), 192),
r_bits2 in prop::collection::vec(any::<bool>(), 192)
) {
prop_assume!(r_bits1 != r_bits2);
let commitment1 = pedersen_commit_dob(dob_days, &r_bits1);
let commitment2 = pedersen_commit_dob(dob_days, &r_bits2);
prop_assert_ne!(commitment1, commitment2, "Hiding property");
}
/// Property: pedersen_commit_dob binding property (different dob produces different commitment)
#[test]
fn prop_pedersen_commit_dob_binding_property(
dob_days1 in 0i32..50000,
dob_days2 in 0i32..50000,
r_bits in prop::collection::vec(any::<bool>(), 192)
) {
prop_assume!(dob_days1 != dob_days2);
let commitment1 = pedersen_commit_dob(dob_days1, &r_bits);
let commitment2 = pedersen_commit_dob(dob_days2, &r_bits);
prop_assert_ne!(commitment1, commitment2, "Binding property");
}
}
Properties Tested:
- Commitment hiding property
- Commitment binding property
- Nullifier uniqueness
- Randomness quality
Findings:
- constraint system with validation
- Constraint satisfiability testing
- Public input validation (bit 254 safety)
- Circuit optimisations documented
- Property-based testing for cryptographic properties
Gaps: None identified
UC-078: Randomness Sources
Status: β IMPLEMENTED
Requirement: Use cryptographically secure random number generators for all randomness needs.
Evidence:
1. Native Platform Randomness: OsRng
Source: provii-crypto/crypto-protocol/src/nonce.rs:10-22
#[cfg(not(target_arch = "wasm32"))]
{
// Use `OsRng` instead of `thread_rng` to maintain cryptographic security.
use rand::rngs::OsRng;
use rand::RngCore;
OsRng.fill_bytes(&mut bytes);
}
OsRng Sources:
- Linux:
/dev/urandom(getrandom syscall) - macOS:
SecRandomCopyBytes - Windows:
BCryptGenRandom
Security: System entropy pool, cryptographically secure
2. WebAssembly Randomness: getrandom
Source: provii-crypto/crypto-protocol/src/nonce.rs:10-14
#[cfg(target_arch = "wasm32")]
{
// Use `getrandom`, which hooks into the Web Crypto API.
getrandom::getrandom(&mut bytes).expect("failed to generate random nonce");
}
WebAssembly Sources:
- Browser:
crypto.getRandomValues()(Web Crypto API) - Node.js:
crypto.randomBytes()(Node crypto module)
Security: Browser/runtime entropy, cryptographically secure
3. RNG Trait Bounds
Source: provii-crypto/crypto-sig-redjubjub/src/lib.rs:106-110
/// Generate with provided RNG (useful for tests).
pub fn random_with_rng<R: CryptoRng + RngCore>(rng: &mut R) -> Self {
SigningKey {
scalar: JubjubScalar::random(rng),
}
}
Trait Bounds:
RngCore: Provides random bytesCryptoRng: Marker trait for cryptographically secure RNGs
Purpose: Type system enforces CSPRNG usage (prevents accidental use of weak RNGs)
4. Test RNG (Deterministic)
Source: provii-crypto/crypto-circuit-age/examples/gen_params.rs:122-126
// Create a deterministic RNG with a fixed seed
let mut rng = StdRng::seed_from_u64(12345);
let (sk_bytes, issuer_vk_bytes) = generate_keypair_with_rng(&mut rng);
Usage: Test code only, enables reproducible test vectors
Safety: Test RNG still requires CryptoRng trait bound (enforced at compile time)
5. Entropy Validation
Source: provii-crypto/crypto-protocol/src/nonce.rs:32-45
/// Check that the nonce has sufficient entropy (at least eight unique bytes).
pub fn has_sufficient_entropy(nonce: &[u8; 32]) -> bool {
let mut seen = [false; 256];
let mut uniq = 0u8;
for &b in nonce {
if !seen[b as usize] {
seen[b as usize] = true;
uniq += 1;
}
}
uniq >= 8
}
Validation: Nonces must have at least 8 unique bytes (detects weak RNGs)
6. Deterministic Nonce Derivation (Signatures)
Source: provii-crypto/crypto-sig-redjubjub/src/lib.rs:197-218
/// Deterministic nonce derivation: r = H("ProviiRJ/nonce" || sk_bytes || msg_hash)
fn nonce_from(sk_bytes: &[u8; 32], msg_hash: &[u8]) -> JubjubScalar {
let mut hasher = Blake2s256::new();
hasher.update(PROVII_RJ_NONCE_TAG);
hasher.update(sk_bytes);
hasher.update(msg_hash);
let digest: [u8; 32] = hasher.finalize().into();
// Wide reduction into Jubjub scalar field
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(&digest);
let nonce = JubjubScalar::from_bytes_wide(&wide);
nonce
}
Purpose: Signature nonces derived deterministically (prevents nonce reuse vulnerabilities)
Security: Uses BLAKE2s hash of secret key and message (RFC 6979 style)
7. Fuzzing Coverage
Source: provii-crypto/FUZZING.md:289-308
#### fuzz_nonce_operations
**Fuzzes**: `validate_nonce()`, `has_sufficient_entropy()`
**What it tests**:
- All-zero nonces (should fail)
- Single non-zero byte (should pass validate_nonce)
- Entropy checking (requires 8+ unique bytes)
- Same-byte patterns (should fail entropy)
- Random patterns (should pass)
- Edge cases (7 vs 8 unique bytes)
**Why critical**: Prevents predictable nonces and replay attacks
Findings:
- OsRng for native platforms (system entropy)
- getrandom for WebAssembly (Web Crypto API)
- Type system enforces CSPRNG usage (CryptoRng trait)
- Entropy validation for nonces
- Deterministic signature nonces (prevents nonce reuse)
- Fuzzing coverage for nonce operations
Gaps: None identified
UC-079: Signature Scheme Security
Status: β IMPLEMENTED
Requirement: Implement secure digital signature schemes with proper key sizes, nonce generation, and verification.
Evidence:
1. RedJubjub Signature Scheme
Type: Custom Schnorr-style signature on JubJub curve Key Size: 32 bytes (JubJub scalar field) Signature Size: 64 bytes (32-byte R point + 32-byte s scalar)
Source: provii-crypto/crypto-sig-redjubjub/src/lib.rs
2. Signature Generation
Algorithm:
1. Derive nonce: r = H("ProviiRJ/nonce" || sk || msg_hash)
2. Compute R = r * G
3. Compute challenge: c = H("ProviiRJ" || R || VK || msg_hash)
4. Compute s = r + c * sk
5. Output signature: (R, s)
Implementation: Lines 258-300
fn sign_hash_internal(hash: &[u8], signing_key: &SigningKey) -> Signature {
let g = get_spending_key_generator();
// Deterministic nonce for safety and reproducibility
let nonce = nonce_from(&signing_key.to_bytes(), hash);
// R = nonce * G
let r_point = g * nonce;
// Public key
let vk_point = g * signing_key.scalar;
// Challenge - computed to match circuit exactly
let c = hash_challenge(&r_point.to_bytes(), &vk_point.to_bytes(), hash);
// s = nonce + c * sk (in Jubjub scalar field)
let s = nonce + (c * signing_key.scalar);
Signature { r: r_point, s }
}
3. Signature Verification
Verification Equation: s * G == R + c * VK
Implementation: Lines 330-363
fn verify_hash_internal(hash: &[u8], sig: &Signature, vk: &VerificationKey) -> Result<(), RedJubjubError> {
let g = get_spending_key_generator();
// Recompute challenge
let c = hash_challenge(&sig.r.to_bytes(), &vk.point.to_bytes(), hash);
// Verify equation: s * G == R + c * VK
let lhs = g * sig.s;
let rhs = sig.r + (vk.point * c);
if lhs == rhs {
Ok(())
} else {
Err(RedJubjubError::VerificationFailed)
}
}
4. Deterministic Nonce Generation
RFC 6979 Style: nonce = H(sk || msg)
Implementation: Lines 197-218
fn nonce_from(sk_bytes: &[u8; 32], msg_hash: &[u8]) -> JubjubScalar {
let mut hasher = Blake2s256::new();
hasher.update(PROVII_RJ_NONCE_TAG);
hasher.update(sk_bytes);
hasher.update(msg_hash);
let digest: [u8; 32] = hasher.finalize().into();
// Wide reduction into Jubjub scalar field
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(&digest);
let nonce = JubjubScalar::from_bytes_wide(&wide);
nonce
}
Security: Prevents nonce reuse (which would leak secret key)
5. Domain Separation
Constants:
const PROVII_RJ_PERSONALIZATION: &[u8; 8] = b"ProviiRJ";
const PROVII_RJ_NONCE_TAG: &[u8] = b"ProviiRJ/nonce";
Purpose: Prevent cross-protocol attacks (signatures not valid in other contexts)
6. Challenge Computation
Hash Function: BLAKE2s-256 with personalization
Implementation: Lines 220-255
fn hash_challenge(r_bytes: &[u8; 32], vk_bytes: &[u8; 32], msg_hash: &[u8]) -> JubjubScalar {
let hash = Params::new()
.hash_length(32)
.personal(PROVII_RJ_PERSONALIZATION)
.to_state()
.update(r_bytes)
.update(vk_bytes)
.update(msg_hash)
.finalize();
// CRITICAL: Reduce to Jubjub scalar field
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(hash.as_bytes());
let c = JubjubScalar::from_bytes_wide(&wide);
c
}
Critical: Scalar field reduction matches circuit implementation
7. Credential Signing
Prehash Computation:
let prehash = cred_v2_prehash_bytes(
cred_msg.v,
&cred_msg.kid,
&cred_msg.c,
cred_msg.iat,
cred_msg.exp,
&cred_msg.schema,
);
Then Hash and Sign:
let mut h = Blake2s256::new();
h.update(&prehash);
let msg_hash: [u8; 32] = h.finalize().into();
let sig = sign_hash_internal(&msg_hash, &key);
Binding: Signature covers all credential fields (v, kid, commitment, timestamps, schema)
8. RP Challenge Binding (Optional)
Signature with RP Challenge:
pub fn sign_cred_v2_with_rp(
cred_msg: &CredMsgV2,
rp: &[u8; 32],
signing_key: &[u8; 32],
) -> Result<[u8; 64], RedJubjubError> {
// Hash credential
let prehash = cred_v2_prehash_bytes(...);
// Hash credential + RP challenge
let mut h = Blake2s256::new();
h.update(&prehash);
h.update(rp);
let msg_hash: [u8; 32] = h.finalize().into();
let sig = sign_hash_internal(&msg_hash, &key);
Ok(sig.to_bytes())
}
Purpose: Bind signature to specific relying party (prevents credential transfer)
9. Security Properties
Unforgeability: Based on discrete log hardness on JubJub curve
- Attacker cannot forge signature without secret key
- Security reduction to discrete log problem
Non-Malleability: Signature includes challenge c = H(R || VK || msg)
- Attacker cannot modify signature without breaking hash function
Replay Prevention:
- Signature includes message hash (prevents replay)
- RP challenge binding (prevents cross-site replay)
10. Testing Coverage
Unit Tests: 40+ tests in crypto-sig-redjubjub
Test Categories:
- Basic equation verification
- Tamper detection (modified message, wrong key)
- RP challenge binding
- Serialization round-trip
- Edge cases (zero values, max values)
Fuzzing: fuzz_sig_verify target
Property-Based Tests:
#[test]
fn test_signature_verification_equation() {
let g = get_spending_key_generator();
let sk = JubjubScalar::from(5u64);
let vk = g * sk;
let nonce = JubjubScalar::from(7u64);
let r = g * nonce;
let c = JubjubScalar::from(3u64);
let s = nonce + (c * sk);
let lhs = g * s;
let rhs = r + (vk * c);
assert_eq!(lhs, rhs, "basic Schnorr equation must hold");
}
Findings:
- Custom Schnorr-style signature scheme on JubJub
- Deterministic nonce generation (prevents nonce reuse)
- Domain separation (prevents cross-protocol attacks)
- RP challenge binding (prevents credential transfer)
- testing and fuzzing
Gaps:
- Custom scheme should be formally audited (see UC-082)
- Document security proofs/reductions
Recommendations:
- Formal security audit of RedJubjub implementation
- Document security assumptions and reductions
- Consider using standardized Ed25519 if circuit allows
UC-080: Commitment Scheme Security
Status: β IMPLEMENTED
Requirement: Implement secure commitment schemes with proper randomness and hiding/binding properties.
Evidence:
1. Pedersen Commitment Scheme
Type: Pedersen commitment on JubJub curve Security: Computationally binding, perfectly hiding
Source: provii-crypto/crypto-commit/src/lib.rs:11-45
/// Compute a Pedersen commitment for date of birth.
///
/// This matches the circuit's implementation exactly:
/// - Uses the same generator points as zcash_primitives NoteCommitment
/// - Returns 32 bytes (compressed Jubjub point)
/// - Input: dob_days as i32 (biased via XOR 0x8000_0000), randomness as bool vector
pub fn pedersen_commit_dob(dob_days: i32, r_bits: &[bool]) -> [u8; 32] {
// Bias dob_days for unsigned comparison in circuit
let biased = bias_for_circuit(dob_days);
// Convert biased value to little-endian bits.
let mut dob_bits = vec![];
let mut value = biased;
for _ in 0..32 {
dob_bits.push((value & 1) != 0);
value >>= 1;
}
// Concatenate the date bits with the randomness bits.
let mut in_bits = dob_bits;
in_bits.extend_from_slice(r_bits);
// Hash with the Pedersen generator set used by the circuit.
let point = pedersen_hash(
Personalization::NoteCommitment,
in_bits.into_iter()
);
// Convert the resulting point to bytes.
point.to_bytes()
}
Commitment Function: C = PedersenHash(bias(dob_days) || r_bits)
Note: dob_days is i32 (signed, supporting dates before Unix epoch). The bias_for_circuit() function applies XOR with 0x8000_0000 to preserve ordering when compared as unsigned in the circuit.
2. Randomness Generation
Randomness: Exactly 128 bits (circuit-enforced; the circuit rejects any other length)
Source: provii-crypto/crypto-commit/src/lib.rs:86-98
/// Generate random bits for commitment randomness.
pub fn generate_commitment_randomness<R: RngCore>(rng: &mut R, num_bits: usize) -> Vec<bool> {
let mut bits = Vec::with_capacity(num_bits);
let mut byte = 0u8;
for i in 0..num_bits {
if i % 8 == 0 {
byte = rng.next_u32() as u8;
}
bits.push(((byte >> (i % 8)) & 1) == 1);
}
bits
}
Usage:
let mut rng = thread_rng();
let r_bits = generate_commitment_randomness(&mut rng, 128); // 128 bits (circuit-enforced)
let commitment = pedersen_commit_dob(dob_days, &r_bits);
3. Pedersen Generators
Personalization: NoteCommitment (same as Zcash Sapling)
Source: sapling_crypto::pedersen_hash::Personalization::NoteCommitment
Generator Points: Fixed points on JubJub curve
- Derived from BLAKE2s hash with domain separation
- Nothing-up-my-sleeve construction
4. Nullifier Derivation
Purpose: Unique nullifier per commitment (prevents double-spending/replay)
Source: provii-crypto/crypto-commit/src/lib.rs:47-76
/// Compute Pedersen-based nullifier matching the circuit implementation
pub fn pedersen_nullifier(c_bytes: &[u8; 32]) -> [u8; 32] {
const NULLIFIER_DST: &[u8] = b"provii.nullifier.pedersen.v1";
// Convert the domain and commitment bytes to little-endian bits.
let mut bits = Vec::new();
// Encode the domain separator bits.
for byte in NULLIFIER_DST {
for i in 0..8 {
bits.push(((byte >> i) & 1) != 0);
}
}
// Append the commitment bytes as bits.
for byte in c_bytes {
for i in 0..8 {
bits.push(((byte >> i) & 1) != 0);
}
}
// Hash with Pedersen using the MerkleTree personalization.
let point = pedersen_hash(
Personalization::MerkleTree(0),
bits.into_iter()
);
point.to_bytes()
}
Nullifier Function: N = PedersenHash(DST || C)
Domain Separator: "provii.nullifier.pedersen.v1"
5. Security Properties
Hiding Property: Commitment does not reveal DOB
- Perfectly Hiding: Information-theoretic security
- Without randomness, commitment reveals DOB
- With randomness, commitment is uniformly random
Binding Property: Cannot find two DOBs with same commitment
- Computationally Binding: Based on discrete log hardness
- Attacker cannot find
(dob1, r1) β (dob2, r2)with same commitment - Security reduction to discrete log problem on JubJub
6. Property-Based Testing
Hiding Property Test:
proptest! {
#[test]
fn prop_pedersen_commit_dob_hiding_property(
dob_days in 0i32..50000,
r_bits1 in prop::collection::vec(any::<bool>(), 192),
r_bits2 in prop::collection::vec(any::<bool>(), 192)
) {
prop_assume!(r_bits1 != r_bits2);
let commitment1 = pedersen_commit_dob(dob_days, &r_bits1);
let commitment2 = pedersen_commit_dob(dob_days, &r_bits2);
prop_assert_ne!(commitment1, commitment2, "Hiding property");
}
}
Binding Property Test:
proptest! {
#[test]
fn prop_pedersen_commit_dob_binding_property(
dob_days1 in 0i32..50000,
dob_days2 in 0i32..50000,
r_bits in prop::collection::vec(any::<bool>(), 192)
) {
prop_assume!(dob_days1 != dob_days2);
let commitment1 = pedersen_commit_dob(dob_days1, &r_bits);
let commitment2 = pedersen_commit_dob(dob_days2, &r_bits);
prop_assert_ne!(commitment1, commitment2, "Binding property");
}
}
7. Circuit Compatibility
Critical: Host-side and circuit-side implementations must match exactly
Host Implementation: crypto-commit/src/lib.rs
Circuit Implementation: crypto-circuit-age/src/lib.rs (Pedersen gadgets)
Validation: End-to-end tests verify matching implementations
8. Fuzzing Coverage
Source: provii-crypto/FUZZING.md
Target: fuzz_commit
What it tests:
- Various DOB values (0 to 50,000 days)
- Various randomness lengths (0 to 512 bits)
- Determinism (same inputs β same output)
- Output validation (32 bytes, not all zeros)
- Nullifier computation
- Edge cases
Findings:
- Pedersen commitment implementation matching Zcash
- 128-bit randomness (circuit-enforced, perfectly hiding)
- Pedersen-based nullifier with domain separation
- Property-based testing for hiding/binding properties
- Circuit compatibility validation
- Fuzzing coverage
Gaps: None identified
UC-081: Proof System Security
Status: β IMPLEMENTED
Requirement: Ensure zero knowledge proof system provides soundness, completeness, and zero knowledge properties.
Evidence:
1. Groth16 Security Properties
Soundness: Attacker cannot create valid proof for false statement
- Security: Computational soundness based on q-SDH assumption
- Probability: Negligible (2^-128) for 128-bit security level
- Reduction: Proven in Groth16 paper (Eurocrypt 2016)
Completeness: Honest prover can always create valid proof for true statement
- Property: Expected to succeed for valid witness (a formal cryptographic property of Groth16)
- Tested: Every proof generation tested for validity
Zero knowledge: Proof reveals nothing except statement truth
- Property: Perfect zero knowledge
- Simulator: Groth16 simulator exists (proven in paper)
2. Non-Malleability
Groth16 Proofs: Not malleable by design
- Proof includes pairing checks
- Cannot modify proof without breaking pairing equations
Public Input Binding: Public inputs part of verification
- Attacker cannot change public inputs without breaking verification
- Public inputs committed in proof
3. Replay Prevention
Mechanism: Nullifier system
Source: provii-crypto/crypto-commit/src/lib.rs:47-76
pub fn pedersen_nullifier(c_bytes: &[u8; 32]) -> [u8; 32] {
// Nullifier = PedersenHash("provii.nullifier.pedersen.v1" || C)
...
}
Verifier Checks:
- Proof is valid for public inputs
- Nullifier is in public inputs
- Nullifier not seen before (database check)
Protection: Same credential cannot be used twice (nullifier collision)
4. Relying Party Binding
Mechanism: RP challenge in public inputs
Source: Circuit accepts rp_hash as public input
Process:
- Relying party generates challenge:
rp_challenge = SHA-256(origin || nonce || "zerokp.challenge.v1") - Prover computes:
rp_hashfrom challenge for circuit input - Prover includes
rp_hashin public inputs (2 field elements) - Proof binds to specific RP challenge
Protection: Proof cannot be replayed to different relying party
5. Public Input Assembly
Source: provii-crypto/crypto-public-inputs/src/lib.rs
Public Inputs (8 field elements):
direction(1 field element. AgeDirection: Over/Under)cutoff_days(1 field element. age threshold as i32, biased)rp_hash(2 field elements. 32 bytes split into 2 Γ 254-bit chunks)issuer_vk(2 field elements. 32 bytes split into 2 Γ 254-bit chunks)cred_nullifier(2 field elements. 32 bytes split into 2 Γ 254-bit chunks)
Canonical Packing:
pub fn assemble_public_inputs_canonical(
direction: bool,
cutoff_days: i32,
rp_hash: [u8; 32],
issuer_vk_bytes: [u8; 32],
cred_nullifier: [u8; 32],
) -> Vec<Scalar> {
let mut pi = Vec::with_capacity(8);
// 1. direction (Over = true, Under = false)
pi.push(if direction { Scalar::one() } else { Scalar::zero() });
// 2. cutoff_days (biased for unsigned comparison)
pi.push(Scalar::from(bias_for_circuit(cutoff_days) as u64));
// 3. rp_hash (split into 2 field elements, 254 bits each)
// ... bit packing with bit 254 = 0 for safety ...
// 4. issuer_vk_bytes (split into 2 field elements)
// ... bit packing ...
// 5. cred_nullifier (split into 2 field elements)
// ... bit packing ...
pi
}
Safety: Bit 254 = 0 for all packed field elements (prevents overflow)
6. Proof Verification
Source: provii-crypto/crypto-verifier/src/lib.rs
Verification Process:
pub fn verify_age_snark(
proof_bytes: &[u8],
direction: bool,
cutoff_days: i32,
rp_hash: [u8; 32],
issuer_vk_bytes: [u8; 32],
cred_nullifier: [u8; 32],
vk_id: u32,
vk_bytes: &[u8],
) -> Result<VerifyResult, Error> {
// 1. Load verifying key
let pvk = load_vk(vk_bytes)?;
// 2. Deserialize proof
let proof = Proof::read(proof_bytes)?;
// 3. Assemble public inputs (8 elements)
let public_inputs = assemble_public_inputs_canonical(
direction,
cutoff_days,
rp_hash,
issuer_vk_bytes,
cred_nullifier,
);
// 4. Verify proof
verify_proof(&pvk, &proof, &public_inputs)?;
Ok(VerifyResult { ... })
}
Pairing Check: Groth16 verifier performs pairing check:
e(A, B) = e(Ξ±, Ξ²) * e(L, Ξ³) * e(C, Ξ΄)
where L = Ξ£(public_input[i] * IC[i])
7. Testing Coverage
Constraint Satisfiability:
#[test]
fn test_circuit_satisfiability() {
let mut tcs = TestConstraintSystem::<bls12_381::Scalar>::new();
test_circ.synthesize(&mut tcs).expect("synthesis");
assert!(tcs.is_satisfied());
}
Proof Generation and Verification:
#[test]
fn test_proof_generation_and_verification() {
let proof = create_random_proof(circuit, ¶ms, &mut rng).unwrap();
let public_inputs = assemble_public_inputs_canonical(...);
assert!(verify_proof(&pvk, &proof, &public_inputs).is_ok());
}
Public Input Sensitivity:
#[test]
fn test_proof_fails_with_wrong_public_inputs() {
let proof = create_random_proof(circuit, ¶ms, &mut rng).unwrap();
let wrong_cutoff = cutoff_days + 1;
let wrong_public_inputs = assemble_public_inputs_canonical(wrong_cutoff, ...);
assert!(verify_proof(&pvk, &proof, &wrong_public_inputs).is_err());
}
8. Fuzzing Coverage
Source: provii-crypto/FUZZING.md
Targets:
fuzz_proof_verify: Proof deserialization and verificationfuzz_public_inputs_assemble: Public input assemblyfuzz_verifier_load_vk: Verifying key loading
Coverage:
- Malformed proofs (truncated, invalid points)
- Public input malleability
- Bit 254 safety
- VK deserialization
9. End-to-End Security
Full Flow:
- Issuance:
- Issuer signs credential with RedJubjub
- Credential includes commitment
C = Pedersen(dob, r)
- Proof Generation:
- Prover creates proof for age threshold
- Proof includes nullifier
N = PedersenHash(C) - Proof binds to RP challenge
- Verification:
- Verifier checks proof validity
- Verifier checks nullifier not seen before
- Verifier checks RP challenge matches
- Replay Prevention:
- Nullifier stored in database
- Second proof with same nullifier rejected
Findings:
- Groth16 provides soundness, completeness, zero knowledge
- Non-malleability from proof structure
- Replay prevention via nullifiers
- RP binding via challenge
- testing and fuzzing
Gaps: None identified
UC-082: Third-Party Cryptographic Audit
Status: β GAP
Requirement: Obtain independent third-party cryptographic audit of all custom cryptographic implementations.
Evidence:
1. Current Status
Formal Audit: NOT COMPLETED
Components Requiring Audit:
- Custom RedJubjub Signature Scheme
- Location:
provii-crypto/crypto-sig-redjubjub/ - Criticality: HIGH
- Reason: Custom cryptographic scheme (not Zcash-compatible)
- Review Scope:
- Signature generation algorithm
- Nonce derivation (deterministic)
- Challenge computation
- Scalar field reduction
- Circuit compatibility
- Age Verification Circuit
- Location:
provii-crypto/crypto-circuit-age/ - Criticality: HIGH
- Reason: Core zero knowledge circuit
- Review Scope:
- Constraint system completeness
- Soundness of age check
- Commitment verification
- Signature verification gadget
- Public input assembly
- Pedersen Commitment Implementation
- Location:
provii-crypto/crypto-commit/ - Criticality: MEDIUM
- Reason: Uses Zcash library but custom application
- Review Scope:
- Host-circuit consistency
- Randomness generation
- Nullifier derivation
- Public Input Packing
- Location:
provii-crypto/crypto-public-inputs/ - Criticality: MEDIUM
- Reason: Critical for proof security
- Review Scope:
- Bit 254 safety
- Field element packing
- Canonical representation
2. Self-Validation Mechanisms
Testing Coverage:
- Unit tests: 200+ tests across all crates
- Integration tests: End-to-end proof generation and verification
- Property-based tests: Commitment hiding/binding properties
- Fuzzing: 25 fuzzing targets covering all attack surfaces
Documentation:
Source: provii-crypto/FUZZING.md
## Status
**Total fuzzing targets**: 25
**Modules covered**: All cryptographic modules
**Status**: All 25 targets compile and run successfully
Code Review:
- internal review by Maelstrom AI (sole operator)
- Reference implementation comparison (Zcash for Pedersen, Groth16 papers)
3. Audit Recommendations
Recommended Audit Firms:
- Trail of Bits
- Expertise: ZK proofs, cryptographic implementations
- Notable audits: Zcash, Filecoin, zkSync
- NCC Group
- Expertise: Cryptography, secure systems
- Notable audits: Various blockchain protocols
- Kudelski Security
- Expertise: Cryptographic implementations
- Notable audits: Cryptographic libraries, ZK systems
- Least Authority
- Expertise: Privacy tech, zero knowledge
- Notable audits: Zcash, Tor
Audit Scope:
Duration: 4-6 weeks
Deliverables:
- Security assessment report
- Vulnerability findings (if any)
- Remediation recommendations
- Code quality assessment
- Best practices compliance
Focus Areas:
- Custom RedJubjub signature scheme
- Circuit constraint system
- Host-circuit consistency
- Randomness generation
- Key management
- Public input assembly
- Nullifier system
- Replay prevention
4. Pre-Audit Checklist
Completed:
- β test suite
- β Fuzzing infrastructure (25 targets)
- β Code documentation
- β README files for all crates
- β Security notes in README
Remaining:
- β Formal specification document
- β Threat model documentation
- β Security assumptions documentation
- β Architecture security documentation
5. Interim Mitigations
Until Audit Completed:
- Limited Deployment:
- Deploy to low-value use cases first
- Gradual rollout with monitoring
- Responsible Disclosure:
- Security researchers can report via security@maelstrom.au
- Public acknowledgment for responsible disclosures
- Ongoing Security Review:
- Regular internal code review
- Security-focused testing
- Fuzzing campaigns
- Monitoring:
- Track proof verification failures
- Monitor for unusual patterns
- Incident response procedures
Findings:
- No formal cryptographic audit completed
- Custom RedJubjub requires audit (HIGH PRIORITY)
- self-validation in place (testing, fuzzing)
- Internal review completed
Gaps:
- No third-party cryptographic audit (CRITICAL GAP)
- No formal specification document
- No documented threat model
Recommendations:
Immediate (1-2 months):
- Document formal specification of RedJubjub
- Document threat model and security assumptions
- Prepare audit package
Short-term (3-6 months): 4. Engage audit firm for RedJubjub and circuit review 5. Address audit findings 6. Prepare for external cryptographic review / published audit findings (when commercially justified)
Long-term (6-12 months): 7. Annual security audits 8. Responsible disclosure programme 9. Public security documentation
Code References
Primary Implementation Files
Cryptographic Primitives
| File | Purpose | Lines of Code |
|---|---|---|
provii-crypto/crypto-sig-redjubjub/src/lib.rs | RedJubjub signatures | 1,500+ |
provii-crypto/crypto-commit/src/lib.rs | Pedersen commitments | 600+ |
provii-crypto/crypto-circuit-age/src/lib.rs | Age verification circuit | 1,764 |
provii-crypto/crypto-protocol/src/nonce.rs | Nonce generation | 130 |
Key Management
| File | Purpose | Lines of Code |
|---|---|---|
provii-crypto/tools/keygen/src/main.rs | Issuer key generation | 108 |
provii-crypto/crypto-circuit-age/examples/gen_params.rs | Trusted setup | 525 |
Testing & Security
| File | Purpose | Lines of Code |
|---|---|---|
provii-crypto/FUZZING.md | Fuzzing documentation | 694 |
provii-crypto/README.md | Workspace documentation | 131 |
Key Functions
UC-071: Cryptographic Standards
// Groth16 proof system
bellman::groth16::generate_random_parameters()
bellman::groth16::create_random_proof()
bellman::groth16::verify_proof()
// Pedersen commitments (Zcash-compatible)
sapling_crypto::pedersen_hash::pedersen_hash()
UC-072/073: Key Generation
// Issuer key generation (crypto-sig-redjubjub/src/lib.rs:97-103)
fn SigningKey::random() -> Self {
let mut rng = OsRng;
let scalar = JubjubScalar::random(&mut rng);
SigningKey { scalar }
}
// Verification key derivation (crypto-sig-redjubjub/src/lib.rs:113-124)
fn SigningKey::verification_key(&self) -> VerificationKey {
let g = get_spending_key_generator();
let point = g * self.scalar;
VerificationKey { point }
}
UC-078: Randomness Sources
// Native platform (crypto-protocol/src/nonce.rs:10-22)
#[cfg(not(target_arch = "wasm32"))]
{
use rand::rngs::OsRng;
use rand::RngCore;
OsRng.fill_bytes(&mut bytes);
}
// WebAssembly (crypto-protocol/src/nonce.rs:10-14)
#[cfg(target_arch = "wasm32")]
{
getrandom::getrandom(&mut bytes).expect("failed to generate random nonce");
}
UC-079: Signature Security
// Deterministic nonce (crypto-sig-redjubjub/src/lib.rs:197-218)
fn nonce_from(sk_bytes: &[u8; 32], msg_hash: &[u8]) -> JubjubScalar {
let mut hasher = Blake2s256::new();
hasher.update(PROVII_RJ_NONCE_TAG);
hasher.update(sk_bytes);
hasher.update(msg_hash);
let digest: [u8; 32] = hasher.finalize().into();
let mut wide = [0u8; 64];
wide[..32].copy_from_slice(&digest);
JubjubScalar::from_bytes_wide(&wide)
}
// Signature generation (crypto-sig-redjubjub/src/lib.rs:258-300)
fn sign_hash_internal(hash: &[u8], signing_key: &SigningKey) -> Signature {
let nonce = nonce_from(&signing_key.to_bytes(), hash);
let r_point = g * nonce;
let c = hash_challenge(&r_point.to_bytes(), &vk_point.to_bytes(), hash);
let s = nonce + (c * signing_key.scalar);
Signature { r: r_point, s }
}
UC-080: Commitment Security
// Pedersen commitment (crypto-commit/src/lib.rs:39-66)
pub fn pedersen_commit_dob(dob_days: i32, r_bits: &[bool]) -> [u8; 32] {
let mut in_bits = dob_bits;
in_bits.extend_from_slice(r_bits);
let point = pedersen_hash(
Personalization::NoteCommitment,
in_bits.into_iter()
);
point.to_bytes()
}
// Nullifier derivation (crypto-commit/src/lib.rs:47-76)
pub fn pedersen_nullifier(c_bytes: &[u8; 32]) -> [u8; 32] {
const NULLIFIER_DST: &[u8] = b"provii.nullifier.pedersen.v1";
let mut bits = Vec::new();
// Domain separator bits
for byte in NULLIFIER_DST { ... }
// Commitment bits
for byte in c_bytes { ... }
let point = pedersen_hash(
Personalization::MerkleTree(0),
bits.into_iter()
);
point.to_bytes()
}
Security Analysis
Strengths
- Industry-Standard Primitives:
- Groth16 ZK-SNARK (peer-reviewed, production-proven)
- BLS12-381 curve (128-bit security)
- Pedersen commitments (Zcash-compatible)
- BLAKE2s-256 hash function (IETF standard)
- Cryptographically Secure Randomness:
- OsRng for all key generation
- getrandom for WebAssembly
- Entropy validation for nonces
- Deterministic signature nonces (prevents nonce reuse)
- Testing:
- 200+ unit tests
- Property-based testing (hiding/binding properties)
- 25 fuzzing targets (257% increase)
- End-to-end integration tests
- Security-Focused Design:
- Domain separation (prevents cross-protocol attacks)
- Nullifier system (prevents replay)
- RP challenge binding (prevents credential transfer)
- Bit 254 safety (prevents field overflow)
- Code Quality:
- Extensive documentation
- Clear naming conventions
- Separation of concerns (modular crates)
- Type safety (CryptoRng trait bounds)
Weaknesses
- Custom RedJubjub Implementation:
- Not Zcash-compatible (intentional but requires justification)
- No formal security proof
- Should be audited by cryptographic experts
- No Formal Audit:
- Critical gap for production deployment
- Custom cryptography should always be audited
- Interim mitigations (testing, fuzzing) are good but not sufficient
- Key Storage:
- No HSM integration for issuer signing keys
- Cloudflare KV provides encryption at rest but not HSM-level security
- No documented key backup/recovery procedures
- Key Rotation:
- Rotation implemented via provii-issuer (POST /admin/keys/rotate, 365-day default)
- No documented emergency rotation runbook
- No rotation monitoring/alerting
- Trusted Setup:
- Single-party setup (acceptable for current use case)
- For high-value applications, multi-party ceremony recommended
- No parameter versioning/migration strategy
Threat Analysis
Threat: Signature Forgery
Attack: Attacker tries to forge issuer signature on credential
Mitigations:
- β Deterministic nonce generation (prevents nonce reuse)
- β Challenge includes R, VK, message (prevents malleability)
- β Signature verification in circuit
- β No formal security proof (audit needed)
Residual Risk: MEDIUM (custom scheme needs audit)
Threat: Commitment Collision
Attack: Attacker finds two DOBs with same commitment
Mitigations:
- β Pedersen commitment (computationally binding)
- β 128-bit randomness (circuit-enforced, perfectly hiding)
- β Property-based testing for binding property
- β Uses Zcash-compatible implementation
Residual Risk: LOW (standard Pedersen commitment)
Threat: Proof Replay
Attack: Attacker reuses proof at different relying party
Mitigations:
- β Nullifier system (prevents same-RP replay)
- β RP challenge binding (prevents cross-RP replay)
- β Nullifier database check in verifier
Residual Risk: LOW (strong replay prevention)
Threat: Weak Randomness
Attack: Attacker predicts random values (nonces, commitments)
Mitigations:
- β OsRng (OS entropy pool)
- β getrandom for Wasm (Web Crypto API)
- β Entropy validation (at least 8 unique bytes)
- β CryptoRng trait bounds (type safety)
Residual Risk: LOW (cryptographically secure RNG)
Threat: Parameter Tampering
Attack: Attacker uses malicious Groth16 parameters
Mitigations:
- β VK fingerprinting (Blake2s hash)
- β Circuit constants hash
- β Parameter validation (self-test)
- β Manifest with metadata
Residual Risk: LOW (fingerprinting detects tampering)
Threat: Key Compromise
Attack: Attacker steals issuer signing key
Mitigations:
- β Secure key generation (OsRng)
- π Cloudflare KV storage (encrypted at rest)
- β No HSM integration
- β Key rotation implemented (365-day default via provii-issuer)
Residual Risk: MEDIUM (no HSM, rotation operational)
Recommendation: Implement HSM storage and key rotation (HIGH PRIORITY)
Gaps and Recommendations
Critical Gaps
1. No Third-Party Cryptographic Audit (UC-082)
Impact: HIGH Priority: CRITICAL
Gap:
- Custom RedJubjub signature scheme not audited
- Age verification circuit not reviewed by experts
- No independent validation of security claims
Recommendation:
- Engage reputable audit firm (Trail of Bits, NCC Group, etc.)
- Focus on custom RedJubjub and circuit constraints
- Complete audit before production deployment
- Budget: $50k-$100k for audit
Timeline: 3-6 months
2. No HSM Integration for Signing Keys (UC-074)
Impact: HIGH Priority: HIGH
Gap:
- Issuer signing keys stored in Cloudflare KV (not HSM)
- Keys potentially extractable by Cloudflare employees
- No HSM-level protection
Recommendation:
- Integrate with AWS CloudHSM, Azure Key Vault HSM, or YubiHSM
- Keep signing keys in HSM, never export
- Issuer service calls HSM for signing operations
Timeline: 2-3 months
High Priority Gaps
3. Key Rotation Operational Gaps (UC-075)
Impact: LOW Priority: MEDIUM
Status: Key rotation is implemented via provii-issuer POST /admin/keys/rotate with KeyStatus lifecycle (Active β Deprecated β Disabled). Default rotation period is 365 days (annual).
Remaining Gaps:
- No documented emergency rotation runbook
- No rotation monitoring/alerting
Recommendation:
- Document emergency rotation runbook
- Add alerting for key age exceeding rotation period
- Test rotation in staging environment
Timeline: 1 month
4. No Formal Security Specification (UC-082)
Impact: MEDIUM Priority: HIGH
Gap:
- No formal specification of RedJubjub scheme
- No documented threat model
- No security assumptions documented
Recommendation:
- Write formal specification for RedJubjub
- Document threat model and attack surfaces
- Document security assumptions (discrete log hardness, etc.)
- Create security architecture document
Timeline: 1 month
Medium Priority Gaps
5. No Multi-Party Trusted Setup (UC-076)
Impact: LOW Priority: MEDIUM
Gap:
- Single-party parameter generation
- Toxic waste not distributed
- Acceptable for current use case but not ideal
Recommendation:
- For current low-value use case: Accept single-party setup
- For future high-value deployments: Consider multi-party ceremony
- Document trust assumptions
Timeline: Future (not urgent)
6. No Key Backup/Recovery Procedures (UC-074)
Impact: MEDIUM Priority: MEDIUM
Gap:
- No documented key backup procedures
- No key recovery plan
- Risk of key loss
Recommendation:
- Document key backup procedures
- Implement encrypted key backups
- Test recovery procedures
- Store backups in secure location (separate from primary)
Timeline: 1 month
Low Priority Gaps
7. No Parameter Versioning/Migration (UC-076)
Impact: LOW Priority: LOW
Gap:
- No strategy for parameter updates
- No migration plan for circuit changes
Recommendation:
- Document parameter versioning scheme
- Create migration plan for circuit updates
- Test migration procedures
Timeline: Future (not urgent)
Cross-References
Related ISMS Documentation
- Core Architecture:
/trust/core/provii-crypto.mdx - Unified Control Matrix:
/trust/compliance/requirements/unified-control-matrix.md - Evidence Mapping:
/trust/compliance/requirements/evidence-mapping.md
Related Evidence
To be collected:
- Infrastructure security evidence (Cloudflare Workers security)
- API security evidence (provii-issuer, provii-verifier)
- Mobile security evidence (the Provii mobile wallet (client) repository under the MaelstromAI GitHub enterprise, proof generation)
External References
Standards:
- Groth16 paper: Jens Groth, βOn the Size of Pairing-based Non-interactive Argumentsβ, Eurocrypt 2016
- BLS12-381: Barreto-Lynn-Scott curve construction
- BLAKE2s: IETF RFC 7693
- RFC 6979: Deterministic ECDSA (for nonce derivation inspiration)
Libraries:
- bellman: https://github.com/zkcrypto/bellman
- jubjub: https://github.com/zkcrypto/jubjub
- bls12_381: https://github.com/zkcrypto/bls12_381
- sapling_crypto: https://github.com/zcash-hackworks/sapling-crypto
Audit Firms:
- Trail of Bits: https://www.trailofbits.com/
- NCC Group: https://www.nccgroup.com/
- Kudelski Security: https://www.kudelskisecurity.com/
- Least Authority: https://leastauthority.com/
Conclusion
The Provii cryptographic implementation demonstrates strong security engineering practices with use of industry-standard cryptographic primitives (Groth16, BLS12-381, Pedersen commitments) and extensive testing/fuzzing infrastructure. The implementation is well-documented and follows cryptographic best practices for randomness generation, key management, and proof system security.
Key Strengths:
- Production-grade ZK-SNARK implementation
- Cryptographically secure randomness sources
- testing (200+ tests, 25 fuzzing targets)
- Security-focused design (domain separation, replay prevention)
Critical Gap:
- No formal third-party cryptographic audit (UC-082)
- This is the PRIMARY gap that must be addressed before production deployment
High Priority Improvements:
- HSM integration for issuer signing keys (UC-074)
- Emergency rotation runbook and monitoring (UC-075)
- Formal security specification and threat model documentation
Overall Assessment: The cryptographic implementation is STRONG with one CRITICAL gap (audit) and several HIGH priority improvements needed before production deployment. The custom RedJubjub signature scheme is the primary concern requiring external validation.
Recommended Next Steps:
- Engage cryptographic audit firm (3-6 months)
- Implement HSM integration (2-3 months)
- Document formal security specification (1 month)
- Document emergency rotation runbook (1 month)
- Address audit findings (timeline TBD based on findings)
Review Status: Draft v1.0 Next Review: After cryptographic audit completion