WISK: Trustless background verification with DigiLocker + SNARK proofs secure, private & employer-ready
Rethinking document verification from the ground up with zero-knowledge cryptography
- The Problem We Solved
- Architecture Catalog
- Technology Reference Guide
- Core Components Deep Dive
- Frontend Implementation Guide
- Developer Documentation
- Setup & Deployment
- API Reference
- Security & Privacy
- Performance Benchmarks
- Contributing
- Resources & Links
Background verification has always felt outdated. An employer asks for your documents, a third-party agency collects them, and suddenly your most personal information β your identity, your certificates, your government IDs β is sitting in someone else's database. It's inefficient, it's risky, and worst of all, it forces you to give up control of your own data.
We wanted to rethink this from the ground up. What if verification didn't require handing over documents at all? What if you could prove the truth of your records β without ever exposing them?
That's exactly what we built.
Here's how it works: when an employer wants to verify a candidate, they don't call a middleman. Instead, they send a simple request by email. The candidate receives that request and uploads their Digilocker-issued certificate β a government-signed PDF. Everything happens locally, right in the browser. The raw document never leaves the user's device.
Our system uses WebAssembly with zk-pdf to parse the actual PDF bytes. It checks whether the requested details β like a name or PAN number β are present inside the document. At the same time, it validates the government's digital signature embedded in the PDF, making sure the document hasn't been edited or forged. If even a single byte is tampered with, the signature breaks and the verification fails.
Once everything checks out, we generate a zero-knowledge SNARK proof. This proof says, "Yes, this document is real. Yes, it's signed by the Government of India. Yes, it contains the requested details." And it does all of this without revealing the entire document. Only the facts the employer asked for are disclosed β nothing more.
The proof is then emailed back to the employer. They can verify it instantly, with mathematical certainty. No agencies, no central databases, no trust required β just pure cryptography. And since the proofs are standard SNARKs, anyone in the network can independently verify them too.
What makes this powerful is simplicity. For the employee, it's just clicking a link in their inbox. For the employer, it's receiving an email with a verified result. Underneath, though, it's state-of-the-art cryptography ensuring privacy, authenticity, and tamper-proof verification.
No private data is stored anywhere. The entire codebase is open source, so anyone can audit how it works. And because it's tied to Digilocker, only official government-signed certificates are accepted. Fake or edited PDFs are instantly rejected.
In short: we turned background verification into a trustless, privacy-preserving experience. No middlemen. No leaks. Just cryptographic truth, delivered seamlessly through email.
WISK is architected as a multi-layered system that seamlessly integrates cryptographic primitives with modern web technologies. Each layer serves a specific purpose in the verification pipeline.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β WISK Architecture β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ β
β β Frontend β β Prover β β Blockchain β β
β β (Next.js + βββββΊβ Service βββββΊβ Verifier β β
β β WebAssembly) β β (Rust + SP1) β β (Solidity) β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ β
β β β β β
β βΌ βΌ βΌ β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ β
β β WASM PDF β β ZK Circuits β β Smart β β
β β Processing β β (SP1 VM) β β Contract β β
β βββββββββββββββββββ βββββββββββββββββββ βββββββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Core PDF Libraries (Rust) ββ
β β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββββββββ
β β β Signature β β Text β β Core Library βββ
β β β Validator β β Extractor β β (Combined Ops) βββ
β β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββββββββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Component | Technology | Purpose | Reference |
|---|---|---|---|
| Frontend Layer | Next.js 15, TypeScript, Tailwind | User interface and workflow management | Frontend Guide |
| WebAssembly Layer | Rust + wasm-bindgen | Browser-side PDF processing | WASM Documentation |
| ZK Circuit Layer | SP1, Rust | Zero-knowledge proof generation | Circuit Documentation |
| Prover Service | Axum, SP1 SDK | High-performance proof API | Prover Implementation |
| PDF Processing | Pure Rust | Document parsing and validation | PDF Utils Documentation |
| Smart Contracts | Solidity, Foundry | On-chain proof verification | Contract Documentation |
- SP1 (Succinct Protocol v1) - Zero-knowledge virtual machine for SNARK generation
- PKCS#7/CMS - Digital signature verification standard
- RSA-PSS - Cryptographic signature algorithms (SHA-256/SHA-1)
- WebAssembly - Browser-compatible cryptographic operations
- Rust - Systems programming language for core logic
- Next.js 15 - Full-stack React framework with App Router
- TypeScript - Type-safe development
- Tailwind CSS - Utility-first CSS framework
- MongoDB - Document database for verification requests
- Node.js - JavaScript runtime for backend services
- wasm-bindgen - Rust-WebAssembly bindings
- Axum - Async web framework for Rust
- PKI.js - JavaScript cryptographic library
- Radix UI - Headless UI components
- Lucide React - Icon library
The PDF processing system is built from scratch in pure Rust, avoiding heavy dependencies like OpenSSL or lopdf. This design ensures compatibility with zero-knowledge environments and provides a minimal attack surface.
Purpose: Validates digital signatures in PDF documents using PKCS#7/CMS standards.
pub fn verify_pdf_signature(pdf_bytes: &[u8]) -> Result<PdfSignatureResult, String> {
// Extract signature dictionary and ByteRange
let signature_info = extract_signature_info(pdf_bytes)?;
// Verify content integrity (Hash verification)
let content_hash = calculate_content_hash(&signature_info)?;
// Verify signature authenticity (RSA verification)
let signature_valid = verify_rsa_signature(&signature_info)?;
Ok(PdfSignatureResult {
is_valid: content_hash && signature_valid,
public_key: extract_public_key(&signature_info)?,
message_digest: format_digest(&signature_info.digest)?,
})
}Two-Stage Verification Process:
-
Content Integrity Check
- Extracts
signed_bytesusing PDF'sByteRange - Calculates cryptographic hash (SHA-256/SHA-1/SHA-384/SHA-512)
- Mathematical verification:
Hash(signed_bytes) == MessageDigest
- Extracts
-
Signature Authenticity Check
- Extracts and processes
signed_attributes(ASN.1 structure) - Verifies signature using signer's public key
- Mathematical verification:
Verify(PublicKey, Hash(signed_attributes), Signature) == true
- Extracts and processes
Reference: Signature Validator Documentation
Purpose: Extracts plain text from PDF files with complex font encoding support.
pub fn extract_text(pdf_bytes: Vec<u8>) -> Result<Vec<String>, String> {
let pdf = parse_pdf_structure(&pdf_bytes)?;
let mut pages_text = Vec::new();
for page in pdf.pages {
let content_streams = extract_content_streams(&page)?;
let text = process_text_operations(&content_streams, &pdf.fonts)?;
pages_text.push(text);
}
Ok(pages_text)
}Advanced Features:
- Font Encoding Support: ToUnicode maps, Differences arrays, built-in encodings
- CID Font Handling: Complex glyph-to-Unicode mapping for Indian government documents
- Multiple Encodings: StandardEncoding, WinAnsiEncoding, MacRomanEncoding, PDFDocEncoding
- Stream Processing: FlateDecode and other PDF compression methods
Reference: Text Extractor Documentation
Purpose: Unified interface combining signature validation and text extraction.
pub fn verify_text(
pdf_bytes: Vec<u8>,
page_number: u8,
substring: &str,
offset: usize
) -> Result<PdfVerificationResult, String> {
// Step 1: Verify digital signature
let signature_result = signature_validator::verify_pdf_signature(&pdf_bytes)?;
// Step 2: Extract text from all pages
let pages = extractor::extract_text(pdf_bytes)?;
// Step 3: Verify substring at specific offset
let text_matches = verify_substring_at_offset(&pages[page_number as usize], substring, offset);
Ok(PdfVerificationResult {
signature: signature_result,
substring_matches: text_matches,
pages,
})
}Reference: Core Library Documentation
Purpose: Core zero-knowledge program running inside the SP1 virtual machine.
#![no_main]
sp1_zkvm::entrypoint!(main);
pub fn main() {
// Read input from the verifier
let input = sp1_zkvm::io::read::<PDFCircuitInput>();
// Perform PDF verification inside ZK environment
let output = verify_pdf_claim(input).unwrap_or_else(|_| PDFCircuitOutput::failure());
// Convert to public values and commit
let public_values: PublicValuesStruct = output.into();
let bytes = PublicValuesStruct::abi_encode(&public_values);
// Commit to public values - this becomes part of the proof
sp1_zkvm::io::commit_slice(&bytes);
}Key Features:
- Deterministic execution in constrained environment
- Public value commitment for proof verification
- Error handling with graceful degradation
Purpose: High-level verification functions for circuit implementations.
pub fn verify_pdf_claim(input: PDFCircuitInput) -> Result<PDFCircuitOutput, String> {
let PDFCircuitInput {
pdf_bytes,
page_number,
offset,
substring,
} = input;
// Perform verification using core PDF libraries
let result = verify_text(pdf_bytes, page_number, substring.as_str(), offset as usize)?;
// Construct circuit output
Ok(PDFCircuitOutput::from_verification(
&substring,
page_number,
offset,
result,
))
}Specialized Verification Functions:
// GST Certificate specific verification
pub fn verify_gst_certificate(pdf_bytes: Vec<u8>) -> GSTCertificate {
let verified_content = pdf_core::verify_and_extract(pdf_bytes).unwrap();
let full_text = verified_content.pages.join(" ");
// Extract GST number using regex pattern
let gst_pattern = regex::Regex::new(
r"([0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[Z]{1}[0-9A-Z]{1})"
).unwrap();
let gst_number = gst_pattern
.captures(&full_text)
.and_then(|cap| cap.get(1))
.map(|m| m.as_str().to_string())
.unwrap();
// Extract legal name using regex pattern
let legal_name_pattern = regex::Regex::new(
r"Legal Name\s*([A-Za-z\s&.,]+?)(?:\n|Trade Name|Additional|$)"
).unwrap();
let legal_name = legal_name_pattern
.captures(&full_text)
.and_then(|cap| cap.get(1))
.map(|m| m.as_str().trim().to_string())
.unwrap();
GSTCertificate {
gst_number,
legal_name,
signature: verified_content.signature,
}
}Reference: Circuit Library Documentation
Purpose: On-chain proof verification using Solidity.
contract PdfVerifier {
address public verifier; // SP1 verifier contract address
bytes32 public programVKey; // Verification key for zkPDF program
function verifyPdfProof(
bytes calldata _publicValues,
bytes calldata _proofBytes
) public view returns (bool) {
// Verify the SP1 proof
ISP1Verifier(verifier).verifyProof(
programVKey,
_publicValues,
_proofBytes
);
// Decode and return the verification result
PublicValuesStruct memory publicValues = abi.decode(
_publicValues,
(PublicValuesStruct)
);
return publicValues.result;
}
}Reference: Smart Contract Documentation
async fn prove(Json(body): Json<ProofRequest>) -> Json<SP1ProofWithPublicValues> {
let client = ProverClient::from_env();
let (pk, _vk) = client.setup(ZKPDF_ELF);
let proof_input = PDFCircuitInput {
pdf_bytes: body.pdf_bytes,
page_number: body.page_number,
offset: body.offset.expect("Offset must be provided"),
substring: body.sub_string,
};
let mut stdin = SP1Stdin::new();
stdin.write(&proof_input);
// Generate proof using SP1 (local or network prover)
let proof = client
.prove(&pk, &stdin)
.expect("Failed to generate proof");
Json(proof)
}async fn verify(Json(proof): Json<SP1ProofWithPublicValues>) -> Json<VerifyResponse> {
let client = ProverClient::from_env();
let (_pk, vk) = client.setup(ZKPDF_ELF);
match client.verify(&proof, &vk) {
Ok(()) => Json(VerifyResponse {
valid: true,
error: None,
}),
Err(e) => Json(VerifyResponse {
valid: false,
error: Some(e.to_string()),
}),
}
}# High-performance network prover
SP1_PROVER=network \
NETWORK_PRIVATE_KEY=<SUCCINCT_API_KEY> \
RUST_LOG=info \
cargo run --release --bin prover
# Local prover (development)
RUST_LOG=info \
cargo run --release --bin proverThe frontend is built using the App Router architecture with server-side components and client-side interactivity.
frontend/
βββ src/
β βββ app/ # App Router pages
β β βββ name/[id]/ # Name verification workflow
β β βββ academic/[id]/ # Academic verification workflow
β β βββ pan/[id]/ # PAN verification workflow
β β βββ layout.tsx # Root layout
β βββ components/ # Reusable UI components
β β βββ Header.tsx # Navigation header
β β βββ ui/ # Shadcn/ui components
β βββ actions/ # Server actions
β β βββ nameActions.ts # Name verification logic
β β βββ academicActions.ts # Academic verification logic
β β βββ panActions.ts # PAN verification logic
β βββ models/ # MongoDB schemas
β βββ utils/ # Utility functions
β β βββ mail/ # Email service
β β βββ connectToDb.ts # Database connection
β βββ pkg/ # WebAssembly modules
Request Creation Flow:
export async function createNameVerify(
name: string,
email: string,
recieverEmail: string
) {
await connectToDB();
// Create verification record
const nameVerify = new NameVerify({
proverName: name,
email,
recieverEmail,
isVerified: false,
});
await nameVerify.save();
// Send email with verification link
await sendNameVerificationEmail(recieverEmail, email, name, nameVerify._id);
return { success: true, message: "Verification request sent" };
}Database Schema (models/nameModel.ts):
const NameSchema = new mongoose.Schema(
{
email: { type: String, required: true },
recieverEmail: { type: String, required: true },
proverName: { type: String, required: true },
isVerified: { type: Boolean, default: false },
snark: { type: mongoose.Schema.Types.Mixed }, // SNARK proof data
signature: { type: String }, // Public key from PDF
},
{ timestamps: true }
);Professional Email Templates (utils/mail/nameMail.ts):
export const sendNameVerificationEmail = async (
to: string,
fromEmail: string,
proverName: string,
id: string
) => {
const verifyUrl = `${process.env.API_URL}/name/${id}`;
const mailOptions = {
from: `"Wisk" <[email protected]>`,
to,
subject: `Verify Name Request - Wisk`,
html: `
<div style="
font-family: Arial, sans-serif;
background-color: #ffffff;
color: #000000;
padding: 32px;
max-width: 600px;
margin: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
">
<h2 style="margin: 0 0 16px 0; font-size: 20px; font-weight: 600;">
Name Verification Request
</h2>
<p style="font-size: 15px; line-height: 1.6; margin: 0 0 24px 0;">
You received a verification request from <strong>${fromEmail}</strong>
to verify the name <strong>${proverName}</strong>.
</p>
<a href="${verifyUrl}"
style="
display: inline-block;
text-decoration: none;
background-color: #000;
color: #fff;
font-size: 14px;
padding: 12px 20px;
border-radius: 6px;
font-weight: 500;
">
Verify Name
</a>
<p style="font-size: 12px; color: #444; margin-top: 24px;">
If you did not expect this request, you can safely ignore this email.
</p>
</div>
`,
};
await transporter.sendMail(mailOptions);
};// Dynamic WASM loading with error handling
const loadWasm = async () => {
try {
const wasm = await import("@/pkg/pdf_utils_wasm");
await wasm.default(); // Initialize WASM module
return wasm;
} catch (error) {
console.error("Failed to load WASM module:", error);
throw new Error("WebAssembly module failed to load");
}
};const processFile = useCallback(
async (file: File) => {
setStatus("Processing PDF file...");
setProcessing(true);
try {
// Convert file to Uint8Array
const buffer = await file.arrayBuffer();
const uint8 = new Uint8Array(buffer);
setPdfBytes(uint8);
// Load and use WebAssembly module
const wasm = await loadWasm();
const result = wasm.wasm_verify_and_extract(uint8);
if (result?.success) {
// Update verification states
setPublicKeyPEM(result.signature.public_key);
setSignatureValid(result.signature.is_valid);
setPages(result.pages);
// Verify required text is present
const textPresent = verifyTextInPages(result.pages, res.proverName);
setTextVerified(textPresent);
setStatus(
textPresent
? "β
PDF verified and text found"
: "β οΈ Required text not found in PDF"
);
} else {
throw new Error(result?.error || "PDF processing failed");
}
} catch (error) {
console.error("Error processing file:", error);
setStatus("β PDF processing failed");
toast.error("Error processing PDF file");
} finally {
setProcessing(false);
}
},
[res?.proverName, verifyTextInPages]
);The academic verification system handles multiple fields concurrently:
const onGenerateProof = async () => {
// Validation checks
const checks = {
"PDF signature must be valid": signatureValid,
"Name must be present in PDF": nameVerified,
"Academic ID must be present in PDF": academicIdVerified,
"CGPA must be present in PDF": cgpaVerified,
"Institute must be present in PDF": instituteVerified,
};
for (const [msg, condition] of Object.entries(checks)) {
if (!condition) return toast.error(msg);
}
setProcessing(true);
setStatus("Generating SNARK proofs...");
try {
const proveEndpoint = "http://localhost:3001/prove";
// Generate proofs for all fields in parallel
const makeProofRequest = (sub_string: string) =>
fetch(proveEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
pdf_bytes: Array.from(pdfBytes!),
page_number: 1,
offset: 0,
sub_string,
}),
});
const [
nameProofResponse,
academicIdProofResponse,
instituteProofResponse,
cgpaProofResponse,
] = await Promise.all([
makeProofRequest(res.proverName),
makeProofRequest(res.proverAcademicId),
makeProofRequest(res.proverInstitute),
makeProofRequest(res.proverCGPA),
]);
// Combine all proofs
const combinedProof = {
nameProof: await nameProofResponse.json(),
academicIdProof: await academicIdProofResponse.json(),
instituteProof: await instituteProofResponse.json(),
cgpaProof: await cgpaProofResponse.json(),
};
setProofData(JSON.stringify(combinedProof, null, 2));
setProofGenerated(true);
toast.success("All SNARK proofs generated successfully!");
} catch (error) {
toast.error("Error generating proofs: " + error.message);
} finally {
setProcessing(false);
}
};const verifyTextInPages = useCallback(
(extractedPages: string[], searchText: string): boolean => {
const normalizeText = (text: string) =>
text.toLowerCase().replace(/\s+/g, " ").trim();
const normalizedSearchText = normalizeText(searchText);
return extractedPages.some((page) => {
const normalizedPage = normalizeText(page);
return normalizedPage.includes(normalizedSearchText);
});
},
[]
);
// Specialized verification for different document types
const verifyCgpaInPages = useCallback(
(extractedPages: string[], proverCgpa: string): boolean => {
const cgpaVariations = [
proverCgpa,
proverCgpa.replace(".", ""),
`CGPA: ${proverCgpa}`,
`CGPA ${proverCgpa}`,
];
return extractedPages.some((page) =>
cgpaVariations.some((variation) =>
normalizeText(page).includes(variation.toLowerCase())
)
);
},
[]
);const VerificationResultItem = ({
label,
isValid,
validText = "Valid",
invalidText = "Invalid",
}) => (
<div className="flex items-center justify-between p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50">
<span className="text-sm font-medium">{label}</span>
<Badge
variant={isValid ? "default" : "destructive"}
className="shadow-sm px-3 py-1"
>
{isValid ? validText : invalidText}
</Badge>
</div>
);
// Usage in verification workflow
{
signatureValid !== null && (
<VerificationResultItem
label="Digital Signature"
isValid={signatureValid}
validText="Valid"
invalidText="Invalid"
/>
);
}
{
nameVerified !== null && (
<VerificationResultItem label="Name Verification" isValid={nameVerified} />
);
}const ProcessingIndicator = ({ processing, status }) => (
<div className="flex items-center space-x-3 p-3 rounded-lg bg-slate-50 dark:bg-slate-800/50">
{processing ? (
<div className="animate-spin rounded-full h-5 w-5 border-2 border-primary/20 border-t-primary"></div>
) : (
<div className="h-5 w-5 rounded-full bg-slate-200 dark:bg-slate-700"></div>
)}
<span className="text-muted-foreground text-sm font-medium">{status}</span>
</div>
);const FileUploadZone = ({ onFileSelect, processing, accept = ".pdf" }) => {
const [dragActive, setDragActive] = useState(false);
const handleDrag = (e) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive(true);
} else if (e.type === "dragleave") {
setDragActive(false);
}
};
const handleDrop = (e) => {
e.preventDefault();
e.stopPropagation();
setDragActive(false);
if (e.dataTransfer.files && e.dataTransfer.files[0]) {
onFileSelect(e.dataTransfer.files[0]);
}
};
return (
<div
className={`
relative border-2 border-dashed rounded-xl p-8 text-center transition-all duration-200
${
dragActive
? "border-primary bg-primary/5"
: "border-gray-300 hover:border-gray-400"
}
${processing ? "pointer-events-none opacity-50" : "cursor-pointer"}
`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
>
<input
type="file"
accept={accept}
onChange={(e) => onFileSelect(e.target.files?.[0])}
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
disabled={processing}
/>
<div className="space-y-4">
<div className="h-16 w-16 mx-auto bg-blue-100 dark:bg-blue-900/30 rounded-full flex items-center justify-center">
<FileText className="h-8 w-8 text-blue-600 dark:text-blue-400" />
</div>
<div>
<p className="text-lg font-semibold">Drop your PDF here</p>
<p className="text-muted-foreground">or click to browse</p>
</div>
</div>
</div>
);
};const onVerifySnarkProof = async () => {
if (!res?.snark) return toast.error("No SNARK proof found to verify");
setVerifyingSnark(true);
toast.info("Verifying received SNARK proofs...");
try {
const proofToVerify =
typeof res.snark === "string" ? JSON.parse(res.snark) : res.snark;
const verifyEndpoint = "http://localhost:3001/verify";
const verifyProof = async (proof: any) => {
if (!proof) return true;
const response = await fetch(verifyEndpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(proof),
});
const result = await response.json();
return result.success || result.valid;
};
// Verify all proofs in parallel
const [nameValid, academicIdValid, instituteValid, cgpaValid] =
await Promise.all([
verifyProof(proofToVerify.nameProof),
verifyProof(proofToVerify.academicIdProof),
verifyProof(proofToVerify.instituteProof),
verifyProof(proofToVerify.cgpaProof),
]);
const allValid =
nameValid && academicIdValid && instituteValid && cgpaValid;
setSnarkVerificationResult(allValid);
if (allValid) {
toast.success("π All SNARK proofs verified successfully!");
} else {
toast.error("β Some SNARK proofs failed verification");
}
} catch (error) {
console.error("Error verifying SNARK proof:", error);
toast.error("Error verifying SNARK proof: " + error.message);
} finally {
setVerifyingSnark(false);
}
};Before setting up WISK, ensure you have:
- Rust (latest stable)
- Node.js (v18 or later)
- SP1 Toolkit
- MongoDB (local or cloud)
-
Clone the Repository
git clone https://github.com/YadlaMani/wisk.git cd wisk -
Build PDF Utilities
cd pdf-utils/wasm wasm-pack build --target web cd ../..
-
Build ZK Circuits
cd circuits cargo build --release sp1 build cd ..
-
Install Frontend Dependencies
cd frontend npm install cd ..
-
Environment Setup
cp .env.example .env.local # Edit .env.local with your configuration
Terminal 1 - Prover Service:
cd circuits/script
SP1_PROVER=network \
NETWORK_PRIVATE_KEY=your_succinct_key \
RUST_LOG=info \
cargo run --release --bin proverTerminal 2 - Frontend Development:
cd frontend
npm run devTerminal 3 - MongoDB (if local):
mongod --dbpath /usr/local/var/mongodb# Run signature validator tests
cd pdf-utils/signature-validator
cargo test
# Run text extractor tests
cd ../extractor
cargo test
# Run core library tests
cd ../core
cargo test# Test circuit compilation
cd circuits
cargo test
# Test proof generation
cd script
cargo test test_proof_generation# Run component tests
cd frontend
npm test
# Run E2E tests
npm run test:e2e| Document Type | Local Prover | Network Prover | Memory Usage |
|---|---|---|---|
| Single Name | 45-90s | 15-25s | ~2.1GB |
| PAN + Name | 60-120s | 20-35s | ~2.8GB |
| Academic (4 fields) | 90-180s | 30-60s | ~3.5GB |
| GST Certificate | 50-100s | 18-30s | ~2.3GB |
| Operation | Chrome | Firefox | Safari | Memory |
|---|---|---|---|---|
| PDF Parse | ~200ms | ~250ms | ~300ms | ~15MB |
| WASM Load | ~100ms | ~150ms | ~200ms | ~8MB |
| Signature Verify | ~50ms | ~75ms | ~100ms | ~5MB |
| Text Extract | ~150ms | ~200ms | ~250ms | ~12MB |
Create .env.local in the frontend directory:
# Database Configuration
MONGODB_URI=mongodb://localhost:27017/wisk
DATABASE_URL=mongodb://localhost:27017/wisk
# Email Configuration (for verification emails)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
[email protected]
SMTP_PASS=your-app-password
# API Configuration
API_URL=http://localhost:3000
NEXT_PUBLIC_API_URL=http://localhost:3000
# Clerk Authentication (optional)
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=your_clerk_key
CLERK_SECRET_KEY=your_clerk_secret
# SP1 Configuration (for prover service)
SP1_PROVER=network # or 'local'
NETWORK_PRIVATE_KEY=your_succinct_api_key
RUST_LOG=info# Build everything
make build-all
# Or step by step:
make build-pdf-utils
make build-circuits
make build-frontend# Dockerfile for prover service
FROM rust:1.75 as builder
WORKDIR /app
COPY circuits/ ./circuits/
COPY pdf-utils/ ./pdf-utils/
RUN cd circuits && cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/circuits/target/release/prover /usr/local/bin/
EXPOSE 3001
CMD ["prover"]# docker-compose.yml
version: "3.8"
services:
prover:
build: .
ports:
- "3001:3001"
environment:
- SP1_PROVER=network
- NETWORK_PRIVATE_KEY=${SUCCINCT_API_KEY}
- RUST_LOG=info
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- "3000:3000"
environment:
- MONGODB_URI=${MONGODB_URI}
- API_URL=http://localhost:3000
depends_on:
- mongodb
mongodb:
image: mongo:7
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
volumes:
mongodb_data:# Install Vercel CLI
npm i -g vercel
# Deploy frontend
cd frontend
vercel --prod# railway.json
{
"build": {
"builder": "DOCKERFILE",
"dockerfilePath": "Dockerfile"
},
"deploy": {
"startCommand": "prover",
"healthcheckPath": "/health"
}
}Base URL: http://localhost:3001
POST /prove
Content-Type: application/json
{
"pdf_bytes": [/* array of bytes */],
"page_number": 1,
"offset": 0,
"sub_string": "John Doe"
}Response:
{
"proof": "0x...",
"public_values": "0x...",
"vkey": "0x..."
}POST /verify
Content-Type: application/json
{
"proof": "0x...",
"public_values": "0x...",
"vkey": "0x..."
}Response:
{
"valid": true,
"error": null
}// Name verification
POST /api/name/create
{
"name": "John Doe",
"email": "[email protected]",
"receiverEmail": "[email protected]"
}
// Academic verification
POST /api/academic/create
{
"proverName": "John Doe",
"proverAcademicId": "12345",
"proverInstitute": "MIT",
"proverCGPA": "9.5",
"email": "[email protected]",
"receiverEmail": "[email protected]"
}
// PAN verification
POST /api/pan/create
{
"proverName": "John Doe",
"proverPanId": "ABCDE1234F",
"email": "[email protected]",
"receiverEmail": "[email protected]"
}// Load WASM module
const wasm = await import("@/pkg/pdf_utils_wasm");
await wasm.default();
// Verify and extract PDF
const result = wasm.wasm_verify_and_extract(pdfBytes);
// Verify signature only
const sigResult = wasm.wasm_verify_signature(pdfBytes);
// Extract text only
const textResult = wasm.wasm_extract_text(pdfBytes);
// Verify specific text
const verifyResult = wasm.wasm_verify_text(
pdfBytes,
pageNumber,
substring,
offset
);-
Zero-Knowledge Properties
- Completeness: Valid documents always generate valid proofs
- Soundness: Invalid documents cannot generate valid proofs
- Zero-Knowledge: Proofs reveal no information beyond the verified claim
-
Digital Signature Validation
- PKCS#7/CMS standard compliance
- RSA signature verification with SHA-256/SHA-1/SHA-384/SHA-512
- Certificate chain validation against government root CAs
- Tamper detection through ByteRange integrity checks
-
Content Integrity Assurance
- Cryptographic hashing of signed content
- Byte-level comparison with embedded message digests
- Detection of any document modifications post-signing
-
Local Processing Model
- PDF parsing and text extraction in WebAssembly
- Private key operations never leave user's device
- Zero server-side storage of document content
-
Minimal Data Exposure
- Proof contains only the verified claim (e.g., "name matches")
- Original document content remains private
- Metadata and other document details are not disclosed
-
Trustless Architecture
- Cryptographic proofs are mathematically verifiable
- Open-source codebase allows independent auditing
- Decentralized verification through standard SNARK protocols
-
Input Validation
- PDF structure validation before processing
- Content-type verification for uploaded files
- Size limits to prevent resource exhaustion
- Sanitization of all user inputs
-
Error Handling
- No sensitive information in error messages
- Graceful degradation on verification failures
- Rate limiting on proof generation endpoints
- Comprehensive logging for security monitoring
// Always validate inputs
pub fn verify_pdf_signature(pdf_bytes: &[u8]) -> Result<PdfSignatureResult, String> {
if pdf_bytes.is_empty() {
return Err("PDF bytes cannot be empty".to_string());
}
if pdf_bytes.len() > MAX_PDF_SIZE {
return Err("PDF file too large".to_string());
}
// Continue with validation...
}# Use strong environment variables
export SP1_PROVER=network
export NETWORK_PRIVATE_KEY=$(cat /run/secrets/succinct_key)
export MONGODB_URI=$(cat /run/secrets/mongodb_uri)
# Enable HTTPS only
export FORCE_HTTPS=true
export HSTS_MAX_AGE=31536000
# Configure rate limiting
export RATE_LIMIT_REQUESTS=100
export RATE_LIMIT_WINDOW=3600- SP1 Documentation - Zero-knowledge virtual machine
- WebAssembly Guide - Rust to WASM compilation
- Next.js App Router - Modern React framework
- PDF Reference - Official PDF specification
- PKCS#7 Standard - Cryptographic message syntax
- Rust Analyzer - IDE support for Rust
- wasm-pack - Rust to WebAssembly toolchain
- Foundry - Smart contract development toolkit
- Thunder Client - API testing for VS Code
- Zero-Knowledge Proofs - Introduction to zk-SNARKs
- Digital Signatures - Cryptographic signatures
- PKI Fundamentals - Public key infrastructure
- ASN.1 Notation - Abstract syntax notation
- DigiLocker - Government document repository
- e-Sign Framework - Digital signature standards
- Aadhaar API - Identity verification
- zk-SNARKs - Succinct Non-Interactive Arguments
- Bulletproofs - Short Proofs for Confidential Transactions
- PLONK - Permutations over Lagrange-bases
- GitHub Issues - Bug reports and feature requests
- Discussions - Community discussions
- Discord - Real-time chat and support
- Twitter - Updates and announcements
We welcome contributions to WISK! Please see our Contributing Guide for details on:
- Code of Conduct
- Development workflow
- Pull request process
- Testing requirements
- Documentation standards
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This project is licensed under the MIT License - see the LICENSE file for details.
Built with β€οΈ by the WISK team. Transforming document verification through zero-knowledge cryptography.