IITG.eth Hackathon 2026  ·  Ethereum Sepolia Testnet
SecureHire

A zero-knowledge credential verification system. Prove your academic qualifications to employers without revealing your identity, wallet, or transcript.

Semaphore v4 @kohaku-eth / provider Groth16 ZK Proofs Solidity 0.8.23

Kushal N  ·  BTech Mathematics and Computing, 2nd Year  ·  IIT Guwahati

01  ·  Problem Statement
The Credential Verification Crisis
3 layered problems that the current system cannot solve.

The Broken Verification Process

Currently, employers face a lengthy and inaccurate verification process. They have to manually verify certificates with universities, dealing with slow responses and inaccurate records. It is expensive and incredibly tedious for the companies.

The Over-Disclosure Trap

Applying to different companies in different domains forces you to hand over your entire transcript. If you are applying to a new company while already employed elsewhere, you don't want your transcript stuck in a career network where your current employer might see it.

On-Chain Privacy & Quantum Threat

Ethereum is public. Paying gas to submit a credential proof permanently links your wallet to that identity. Furthermore, traditional wallets rely on ECDSA, making them vulnerable to future quantum computers.

The Core Contradiction

Employers need verified truth. Students need verifiable privacy. The current system forces a choice between the two. It should not. We try to solve this...

02  ·  Solution
Selective Disclosure via Zero-Knowledge Proofs
The employer gets cryptographic certainty. The student reveals nothing else.
01
Student logs in and generates a commitment. The student generates an identity commitment and sends it to the university off-chain. The university verifies the person, approves their courses, and issues the credential by adding them to an anonymous Semaphore group.
02
Student selects what to prove. They choose one or more courses from their credential vault. No one can see what they did not select.
03
A Groth16 ZK Proof is generated in the browser itself. The proof mathematically guarantees group membership without revealing the member's identity.
04
Employer verifies on-chain. The smart contract validates the proof and records the nullifier. The proof is only usable for one time. The student's wallet is never mentioned.

What the Employer Sees

  • Verified: Passed DSAI Minor — true
  • Issued by: IIT Guwahati — true
  • Proof verified on Sepolia block #22,xxx — true

What Remains Hidden

  • Student name and identity
  • Student's wallet address
  • Other courses and their grades
  • Which wallet paid the gas

The Mathematical Guarantee

The proof is validated by semaphore.validateProof() on-chain. It is not a policy or a promise — it is an arithmetic constraint that cannot be forged without breaking SHA-256.

03  ·  Architecture
System Architecture
Three actors. Three smart contracts. One privacy primitive.
Admin (MoE)
SecureHire
Approves universities
(e.g. Ministry of Education)
Issuer
University
UNIVERSITY_ROLE
Creates groups, issues credentials
Prover
Student
Semaphore Identity
Generates ZK Proof in the browser itself
Verifier
Employer
Submits proof via Kohaku
to safeguard their identity while paying gas

CourseRegistry.sol

  • Single source of truth for courses & degrees
  • Maps groupId → Course struct
  • isDegree flag differentiates course vs. degree groups
  • Owned by deployer, written to by CredentialIssuer only

CredentialIssuer.sol

  • OpenZeppelin AccessControl — UNIVERSITY_ROLE
  • Calls semaphore.createGroup() and semaphore.addMember()
  • Degree + course linking via linkCourseToDegree()
  • Supports revocation via Semaphore's Merkle removal

CredentialVerifier.sol

  • Single proof: verifyCredential()
  • Batch proof: verifyBatch() returns bool[]
  • Nullifier tracking prevents reusing the same proof twice (replay attacks)
  • Uses try/catch — partial batch failures do not revert
04  ·  Application Portals
4 Purpose-Built Interfaces
Each portal is scoped to one actor. No portal leaks data from another.

Admin Portal

/admin
  • View all pending university registration requests
  • Approve or reject using approveUniversity()
  • Revoke a university's role at any time (Fully implemented)
  • Wallet must hold DEFAULT_ADMIN_ROLE to access this page

University Portal

/university
  • Register with name + metadata; wait for admin approval
  • Dashboard: toggle between Course and Degree creation
  • Link courses to a degree group (full qualification)
  • Issue credentials by submitting a student's identity commitment
  • Degree badges visible on issued credentials

Student Portal

/student
  • Deterministic login: MetaMask signature → SHA-256 seed → Semaphore Identity
  • View all credentials issued to your identity commitment
  • Select mode: Custom Selection (checkboxes) or Full Degree
  • Generates a multi-proof JSON bundle for submission

Employer Verification Portal

/verify
  • Auto-detects single proof vs. multi-proof bundle JSON
  • Calls verifyBatch() via Kohaku's EthersSignerAdapter
  • Displays a per-proof result table (pass / fail / replay)
  • Employer never sees the student's wallet or identity
05  ·  Library Usage — Semaphore v4
How We Use Semaphore
Every privacy guarantee in SecureHire is backed by Semaphore's ZK group membership protocol.
Function / Interface Location Purpose
semaphore.createGroup(address) CredentialIssuer.sol Called in createCourse() and createDegree(). Deploys a new anonymous Merkle group on Semaphore v4 and returns its groupId.
semaphore.addMember(groupId, commitment) CredentialIssuer.sol Called in issueCredential(). Inserts a student's identity commitment into the course's Merkle tree, making them a group member without recording who they are.
semaphore.removeMember(groupId, commitment, proofSiblings) CredentialIssuer.sol Called in revokeCredential(). Removes a commitment using a Merkle sibling path, effectively revoking the credential.
semaphore.validateProof(groupId, proof) CredentialVerifier.sol Called inside verifyCredential() and wrapped in a try/catch inside verifyBatch(). Validates the Groth16 proof on-chain by checking the Merkle root and nullifier constraints.
new Identity(seed) identity-vault / Student Portal Creates a student's Semaphore identity client-side. The seed is derived deterministically from the student's MetaMask signature via SHA-256, so the same identity is always recovered on login.
generateProof(identity, group, message, scope) Student Portal (browser) Called when the student clicks "Generate Proof." Runs the Groth16 witness computation client-side using the Semaphore WASM circuit and returns the proof object.
ISemaphore.SemaphoreProof CredentialVerifier.sol The on-chain type definition for a submitted proof. Contains merkleTreeDepth, merkleTreeRoot, nullifier, message, scope, and points.

Nullifier Replay Prevention

Each proof generates a unique Nullifier Hash — a deterministic but opaque function of the student's secret and the job application scope. The contract records every used nullifier in usedNullifiers[nullifier]. Submitting the same proof a second time is rejected with "Nullifier already used" without ever revealing who submitted the first one.

06  ·  Library Usage — Kohaku
How We Use Kohaku
We integrated @kohaku-eth/provider into the employer verification flow. The pq-account package is not yet public, so that part runs on a deterministic mock for now.

@kohaku-eth/provider

Active Implementation

Integrated into the Employer Verification portal. Because this is an alpha package, we manually encode the transaction payload and submit via Kohaku's EthersSignerAdapter instead of using standard ethers contract wrappers.

import { EthersSignerAdapter, createTx } from '@kohaku-eth/provider/ethers'; // 1. Manually encode the calldata const data = contract.interface.encodeFunctionData( "verifyBatch", [proofs, groupIds] ); // 2. Wrap standard signer in Kohaku adapter const kohakuSigner = new EthersSignerAdapter(standardSigner); // 3. Build and send transaction through Kohaku const tx = createTx(contractAddress, data, 0n); await kohakuSigner.sendTransaction(tx);

@kohaku-eth/pq-account

Architecture Stub

Each student's Semaphore identity is designed to be rooted in a CRYSTALS-Dilithium post-quantum key pair. The identity vault wrapper is production-ready; only the key generator falls back to a deterministic mock due to the package not being publicly available as an npm package yet.

export async function createPQAccount(seed?: string) { try { // Attempt real Kohaku import via dynamic import const kohaku = await import('@kohaku-eth/pq-account'); return kohaku.createPQAccount(seed); } catch { // Fallback: deterministic mock via SubtleCrypto SHA-256 return createMockPQAccount(seed); } }

Gas Privacy (No ERC-4337 Needed)

Architectural Win

Instead of relying on complex ERC-4337 Paymasters or Railgun shielded transactions to hide the student's wallet, we completely decoupled proof generation from proof submission. The student generates the ZK proof off-chain in the browser (costs 0 gas). The employer submits the payload on-chain and pays the gas. The student never touches the blockchain, achieving perfect anonymity.

07  ·  Engineering Challenges
Problems We Encountered and Fixed
Major blockers hit during the hackathon and how we resolved them.
Scope Truncation — Identical Nullifiers in Batch Proofs
Identity Vault

Problem: Proof scope was computed as Buffer.from(jobId+"-"+groupId).toString('hex').slice(0,16). Since the job ID was exactly 8 chars (16 hex), the slice removed the groupId entirely — every proof in a bundle had the same scope and the same nullifier, causing on-chain replay reverts.

Fix: Replaced slice with BigInt('0x' + ethers.id(`${groupId}-${jobId}`).slice(2,18)) — a Keccak256 hash guarantees unique scopes per course per job.

Parallel Transactions — MetaMask Nonce Conflict
University Dashboard

Problem: Issuing credentials for multiple courses fired all issueCredential() calls in parallel. MetaMask signed all of them with the same nonce — all but the first failed on-chain.

Fix: Switched to sequential execution with await tx.wait() between each call, with a live progress indicator: "Issuing 2 / 3 — Please confirm in MetaMask."

Multi-Proof Bundle — False Failures on Verify Page
Employer Portal

Problem: The verify UI only checked whether the transaction succeeded overall. For a 3-course bundle, any unselected courses were marked "not verified" even if valid proofs existed for them — creating false negatives for the employer.

Fix: Redesigned to auto-detect single vs. bundle proof JSONs and iterate the returned bool[] array per course, showing individual pass/fail status for each credential.

MetaMask Account Switch Not Reflected in UI
Frontend

Problem: After switching wallets in MetaMask, the University and Admin portals kept showing the old account's status. The accountsChanged event fired, but BrowserProvider.listAccounts() returned the stale cached address.

Fix: Updated the handler to read the new address directly from the event payload (accounts[0]) and pass it straight into the status check, bypassing the provider cache.

08  ·  Security & Testing
Security & Testing
The attack vectors we designed against, and the access-control edge cases we tested.

Attack Vectors Covered

SEC-01
Replay Attack. Submitting the same proof twice. The second submission hits the usedNullifiers check and reverts with "Nullifier already used."
SEC-02
Invalid Group. Submitting a valid ZK proof against an unregistered groupId. Reverts with "Invalid course."
SEC-03
Tampered Proof. Altering any points[] value in the proof before submission. semaphore.validateProof() reverts on the arithmetic check.
SEC-04
Cross-Course Reuse. Using a proof generated for Course A on Course B's groupId. Reverts due to Merkle root mismatch — the student's commitment is simply not in Course B's tree.

Access Control Tests

  • Unregistered wallet calling createCourse() → reverts AccessControl
  • Wallet calling requestRegistration() twice → reverts "Already registered"
  • Admin calling approveUniversity() for unapproved wallet → grants UNIVERSITY_ROLE correctly
  • Duplicate course code creation → reverts "Course code already exists"
  • Linking a course to a degree not owned by the same university → reverts "Not course owner"

Test Command

# Smart contract tests cd packages/contracts npx hardhat test # Identity vault tests cd packages/identity-vault pnpm test
09  ·  Deployment
Live on Ethereum Sepolia
All contracts are deployed and verified on Sepolia Testnet.
Contract Address Notes
Semaphore v4 0x8A1fd199516489B0Fb7153EB5f075cDAC83c693D Pre-deployed by PSE team
CourseRegistry 0x8895d0401384Dffd60E53df362D3f422e2A0bF23 Deployed this hackathon
CredentialIssuer 0x9499153dDf0bD0c8A6F173d0bD4cF0780183e85D Deployed this hackathon
CredentialVerifier 0xAAe96283690450E6a869e2a44aAb4a04Cf453605 Deployed this hackathon

Tech Stack

  • Next.js 15 (App Router)
  • Vanilla CSS (no UI framework)
  • Hardhat + OpenZeppelin
  • Semaphore v4 SDK
  • @kohaku-eth/provider
  • pnpm monorepo workspace

What We Would Do Next

  • Full Railgun integration for shielded issuance
  • Live @kohaku-eth/pq-account (Dilithium keys)
  • Mainnet deployment on Kohaku v1.0
  • Anonymous relayer for gas privacy

Prize Tracks

  • Best Use of Semaphore — entire credential flow runs on Semaphore v4
  • Best Use of Kohaku — active provider integration + PQ architecture
  • Privacy Innovation — selective disclosure of academic credentials
10  ·  Thank You
Selective Disclosure.
Cryptographic Proof.

A student should never have to choose between proving their qualifications and protecting their identity. SecureHire makes both possible simultaneously — on-chain, without trust.

Built by
Kushal N
BTech Mathematics and Computing, 2nd Year  ·  IIT Guwahati
Event
IITG.eth Hackathon 2026
Ethereum Foundation — Road to Devcon Academic Program