Biometric + Aadhaar identity binding
The current scaffold proves the device was at the right place, signing with a device-bound key. It does not yet prove the person holding the device is the registered employee. NIC's AEBAS (attendance.gov.in) closes that gap with Aadhaar fingerprint or face authentication against the UIDAI database. This page lays out how Attendance Sanchalan reaches that bar.
What we have today vs. what we need
| Property | Today (Sanchalan v0.1) | AEBAS (NIC) | Sanchalan target |
|---|---|---|---|
| Device-at-location proof | ✅ P-256 ECDSA punch signature + geofence + Wi-Fi SSID | RD-device tied to terminal location | Keep. |
| Anti-spoof signals | ✅ mock_location / rooted / emulator (soft) | RD device with hardware liveness | Add Play Integrity / DeviceCheck. |
| Person identity binding | ❌ none — assumes device == employee | ✅ Aadhaar 1:1 face/fingerprint match (UIDAI yes/no) | Layered, see below. |
| Liveness detection | ❌ | ✅ Face RD passive PAD + UIDAI sign-off | UIDAI Face RD (primary) + ML Kit fallback. |
| Cryptographic non-repudiation | ✅ per-punch ECDSA signature | RD-device-signed PID block | Keep both. |
| Audit chain | ✅ sds_audit hash-chained | NIC AEBAS audit (private) | Keep. |
The four-layer model
No single signal is sufficient. We layer them so the system fails closed if any one is compromised, and so we can reach Aadhaar-grade assurance without paying that latency cost on every punch.
Phased rollout — what's feasible when
| Phase | Capability | UIDAI dependency | Build effort |
|---|---|---|---|
| 0 (today) | Device + location + crypto | none | shipped |
| 1 | + on-device biometric prompt before punch (fingerprint / face the user previously enrolled on the phone) | none | ~2 days mobile + 1 day backend (record bio_used flag) |
| 2 | + selfie capture + ML Kit passive liveness, encrypted evidence storage | none | ~1 week mobile + storage layer |
| 3 | + UIDAI Face Auth via Face RD APK + AUA license | AUA / Sub-AUA license from UIDAI (typically via NIC sub-licensing) | ~3 weeks engineering + license timeline (months) |
| 4 | + hardware attestation (Play Integrity / DeviceCheck) replacing soft anti-spoof | Google Play Console + Apple Developer Program | ~1 week + procurement |
Phase 1 — on-device biometric (implementable now)
Android BiometricPrompt and iOS LocalAuthentication let us require the user to present an enrolled fingerprint or face before each punch. The biometric never leaves the secure enclave; the OS returns yes/no.
This is not Aadhaar identity proof, but it does prove "the person who enrolled biometric on this device tapped the punch button" — a meaningful upgrade over "someone holding the device tapped". Combined with one-time device registration done in person at HR (operator verifies the employee's Aadhaar at registration), it gives a defensible chain.
title="Confirm punch", reason="...") SE->>U: show fingerprint / face prompt U->>SE: present biometric SE->>SE: match against enrolled template (in TEE/SE) alt match SE-->>App: success — release sign() capability App->>SE: sign(nonce + uuid + punched_at) App->>Be: POST /v1/punch + bio_used=true + bio_method=fingerprint Be->>Be: existing flow + record bio_used in attendance_punches else no match SE-->>App: error (cancelled / lockout / no enrollment) App->>U: "Biometric required" end
Backend changes for Phase 1 (small):
// migration: add nullable bio_used + bio_method to attendance_punches
$table->boolean('bio_used')->default(false);
$table->string('bio_method', 16)->nullable(); // fingerprint | face | iris | none
// PunchController validate:
'bio_used' => ['nullable', 'boolean'],
'bio_method' => ['nullable', Rule::in(['fingerprint','face','iris'])],
Mobile changes for Phase 1:
- Add
local_auth: ^2.3topubspec.yaml. - Wrap
PunchKeypair.signRaw()in aLocalAuthentication.authenticate()call. - Bind the keypair to require user authentication in Android KeyStore — so even a rooted attacker can't sign without the biometric.
Phase 2 — selfie + on-device liveness
For staff cohorts where Aadhaar isn't available (visiting members, foreign delegations) or as evidence for dispute resolution, we capture a punch-time selfie with an active liveness challenge.
- Liveness:
google_mlkit_face_detection(free) detects head pose, eye-blink probability, and smile probability. Active challenge: "blink twice", "turn head left", random per punch — defeats simple photo replay. - Storage: encrypted blob (AES-GCM with Vault-issued DEK), stored under an
attendance_punch_selfiestable referencing the punch row. Never displayed to other employees; admin RBAC-gated, audit-logged on access. - Retention: 90 days unless attached to a dispute. Punch row keeps a hash of the encrypted blob for tamper detection.
This is not identity authentication — there's nothing to match against — but it's photographic proof that someone roughly resembling the registered employee was at the location. Useful for HR investigation, not for automated approve/reject.
Phase 3 — UIDAI Face Auth (the AEBAS-equivalent)
What UIDAI Face Auth actually is
UIDAI provides a "Face RD" service distributed as a separate Android APK (and iOS framework) that the user installs alongside our app. Our app launches it via Intent; it captures the face, runs hardware-assisted liveness (passive — no challenge needed), packages an encrypted PID block, and returns it. We forward the PID to the AUA endpoint over HTTPS; UIDAI returns a yes/no with an auth_code.
(extras: aadhaar_last_4, consent) FRD->>U: open camera, capture + liveness FRD->>FRD: passive PAD (motion + texture) FRD-->>App: encrypted PID block (XML, signed by RD private key) App->>AUA: POST /v1/punch/aadhaar
{aadhaar_hash, pid_xml, device_punch_payload} AUA->>AUA: validate device punch (existing flow) AUA->>AUA: wrap PID with our AUA license + sign Auth XML AUA->>ASA: HTTPS POST signed Auth XML ASA->>UIDAI: forward UIDAI-->>ASA: yes/no + auth_code + ts ASA-->>AUA: response AUA->>AUA: persist punch with verdict
+ aadhaar_auth_code (for forensic, not the number) AUA-->>App: { verdict: accepted_aadhaar_face, auth_code }
What needs to be in place before Phase 3 ships
- AUA license: Rajya Sabha applies to UIDAI as Sub-AUA (typically under NIC's parent AUA license, which already serves AEBAS). Application includes:
- Use case: parliamentary attendance, employees + members.
- Privacy Impact Assessment (PIA).
- Data flow diagram (the one above).
- Information Security Audit Report (ISO 27001 / equivalent).
- Annual external audit commitment.
- ASA (Authentication Service Agency): contract with one of the UIDAI-empanelled ASAs (NSDL, BSNL, etc.) for the network leg into UIDAI's authentication server. They host the licensed endpoint and transport our signed Auth XML.
- HSM / Vault for AUA signing key: the AUA license private key must be stored in a FIPS 140-2 L2+ HSM or equivalent. Vault transit engine with auto-unseal qualifies; SDS Vault setup already in place per
/infra/. - Annual audit: budgeted; typically ₹5–10L per year for the audit + license renewal.
Legal / compliance constraints
- Aadhaar Act §7 + Puttaswamy 2018: Aadhaar auth requires the activity to be a "service" or "subsidy". Attendance is borderline — defensible if framed as access control to government workplace. Legal opinion required before deployment.
- Voluntariness: an alternative non-Aadhaar fallback path is mandatory. Our existing device + geofence + Phase-1 biometric flow is that fallback.
- Data minimisation (Aadhaar Act §29): we never store the raw Aadhaar number. Only:
- SHA-256 hash of Aadhaar (with per-deployment salt) — for de-duplication and lookup.
- UIDAI
auth_code(the response identifier) — for forensic correlation if UIDAI requests it. - Encrypted consent record (with consent text version + timestamp).
- Consent: explicit, informed, separate from app login. Recorded in
attendance_aadhaar_consenttable per employee per consent-text-version. - DPDP Act 2023: data fiduciary obligations — privacy policy URL, grievance officer, breach notification within 72 h.
What we encrypt and store (Aadhaar layer)
| Data | Where it goes | Encryption |
|---|---|---|
| Raw Aadhaar number | Never persisted. In-memory only during one auth call. | — |
| Aadhaar SHA-256 hash | attendance_aadhaar_consent.aadhaar_hash + linked to users.id | Salt in Vault. |
| PID block | Transmitted to UIDAI, never persisted. | UIDAI public key envelope. |
| UIDAI auth_code | attendance_punches.aadhaar_auth_code (nullable) | — |
| Selfie (Phase 2 only) | attendance_punch_selfies.encrypted_blob | AES-256-GCM, DEK from Vault transit, KEK in Vault HSM. |
| Consent record | attendance_aadhaar_consent | — |
Phase 4 — hardware attestation
- Android Play Integrity API replaces our soft
rooted_or_jailbroken+emulatorsignals with a Google-signed integrity verdict. We forward the Play Integrity token with each punch; backend verifies with Google's public key. Defeats most root cloak / Magisk modules. - iOS DeviceCheck + App Attest: equivalent for iOS — App Attest cryptographically asserts the binary is genuine and unmodified, signed by Apple.
- Cost: Play Integrity is free for moderate volumes. App Attest free with Apple Developer Program ($99/year).
Concrete data-model additions
The following migrations would land alongside Phase 1 / Phase 3:
// Phase 1 — biometric flag on punches
ALTER TABLE attendance_punches
ADD COLUMN bio_used boolean NOT NULL DEFAULT false,
ADD COLUMN bio_method varchar(16); -- fingerprint | face | iris
// Phase 3 — Aadhaar auth + consent
CREATE TABLE attendance_aadhaar_consent (
id bigserial PRIMARY KEY,
user_id bigint REFERENCES users(id),
employee_id bigint REFERENCES attendance_employee_register(id),
aadhaar_hash varchar(64) NOT NULL, -- sha256(aadhaar + vault_salt)
consent_version varchar(16) NOT NULL,
consent_text_url varchar(255) NOT NULL,
consented_at timestamptz NOT NULL,
withdrawn_at timestamptz,
ip_address inet,
user_agent text,
UNIQUE(user_id, consent_version, withdrawn_at)
);
ALTER TABLE attendance_punches
ADD COLUMN aadhaar_auth_code varchar(64), -- UIDAI response identifier
ADD COLUMN aadhaar_verified boolean,
ADD COLUMN identity_assurance varchar(8); -- l1 (device) | l2 (device+bio) | l3 (Aadhaar)
// Phase 2 — selfie evidence
CREATE TABLE attendance_punch_selfies (
id bigserial PRIMARY KEY,
punch_id bigint REFERENCES attendance_punches(id) ON DELETE CASCADE,
encrypted_blob bytea NOT NULL, -- AES-256-GCM, DEK in Vault
blob_sha256 varchar(64) NOT NULL,
liveness_score decimal(3,2), -- 0..1
liveness_method varchar(32), -- mlkit | facetec | uidai_rd
retention_until date NOT NULL DEFAULT (now() + interval '90 days'),
created_at timestamptz DEFAULT now()
);
Verdict expansion
The verdict column will gain values to capture identity assurance:
| Verdict | Meaning |
|---|---|
accepted | Phase 0 — device + location only. |
accepted_bio | Phase 1 — device biometric matched. |
accepted_aadhaar_face | Phase 3 — UIDAI Face Auth yes. |
accepted_aadhaar_finger | Phase 3 — UIDAI Fingerprint Auth yes. |
rejected_biometric | BiometricPrompt cancelled / lockout. |
rejected_aadhaar | UIDAI returned no. |
rejected_liveness | On-device liveness failed. |
The pragmatic path
- Now — implement Phase 1 (on-device BiometricPrompt). Two days of work. Closes 80% of the "wrong person on the device" attack surface.
- Operator action — start the AUA license application via NIC. The 4–8 week clock starts now.
- While waiting on UIDAI — build the Aadhaar integration end-to-end against a UIDAI sandbox (they offer a staging endpoint to license-applicants). Test the full PID flow before production license arrives.
- License received — flip
AUTH_AADHAAR_ENABLED=true, route Phase-3 punches through. Existing Phase 0/1 flows remain available for fallback. - Phase 4 — Play Integrity / App Attest in parallel, no UIDAI dependency.
Open questions for the deployment team
- Does Rajya Sabha already hold an AUA license, or are we applying fresh? (Determines timeline.)
- Is the use-case framing "access control" or "service delivery"? (Affects §7 applicability and legal sign-off.)
- Mandatory vs. opt-in Aadhaar? (Mandatory needs explicit notification per Aadhaar Act §7; opt-in is straightforward but means many staff stay on Phase 1.)
- For members of parliament — Aadhaar use for parliamentary attendance has not been litigated; expect legal review before enabling.
- Retention policy for selfies (Phase 2) — 90 days default; finance/HR teams may want longer for payroll dispute resolution.
