Repository navigation
proof-of-life passes a single static selfie: burst-free requests report is_real_person true with only face detection #431
Description
Problem
Proof-of-life passes a single static selfie as a "real person" whenever no burst frames are supplied, because the liveness gate defaults to "satisfied" when burst images are absent:
# app/ai-service/proof_of_life.py
burst_required = bool(burst_images_base64)
has_liveness_evidence = (
checks["blink_detected"] or checks["head_movement_detected"] or not burst_required
)
is_real_person = confidence >= threshold and has_liveness_evidenceWith burst_images_base64 empty, burst_required is False, so not burst_required is True, making has_liveness_evidence True regardless of any blink/head-movement signal. liveness_score stays at its 0.40 default, and confidence is computed from face_confidence * 0.50 + quality * 0.20 + liveness * 0.30; a clear, centered face photograph easily clears the default confidence_threshold = 0.65.
Consequence: a request that submits only a selfie — no burst frames — is accepted as a real, live person by the same code path that is meant to detect spoofing. The ProofOfLifeRequest model in main.py makes burst_images_base64 optional, so nothing upstream forces burst frames. The anti-fraud signal that gates claims collapses to "a face is present in one image", which a static photograph or screen recording trivially satisfies.
Root cause
The liveness requirement is conditional on the caller choosing to send burst frames, rather than being a mandatory precondition for a is_real_person = true result.
Why this is architecturally hard
- It is a policy/security decision, not a boolean flip. Simply requiring
burst_requiredwould break legitimate selfie-only callers; the fix must decide the minimum liveness bar (burst frames? a video? a challenge-response) and reflect that in the request schema, not just the score. - The current liveness detector is heuristic and spoofable. Blink is inferred from eye-count differences between Haar-cascade frames and head movement from bounding-box translation; a printed photo waved or a two-frame replay can satisfy them. Raising the bar means designing a defensible liveness signal, not just adding a field.
- It crosses the request contract.
ProofOfLifeRequestis a public Pydantic schema consumed by the backend (app/backend/src/verificationand the AI client); changing optional→required or adding aliveness_modeis a breaking contract change. - Confidence weighting must stay honest. Any fix must ensure
confidencecannot be gamed by a high quality/face score when the liveness evidence is absent; otherwise the threshold remains the weak point.
Proposed design
Make burst/liveness evidence a first-class, required input for a positive result (or add an explicit liveness_mode), and fail closed: if no liveness evidence is supplied, return is_real_person: false with a reason. Optionally add a per-request require_liveness flag so selfie-only callers get an explicit liveness_required refusal rather than a silent pass. Document the minimum acceptable liveness signal.
Acceptance criteria
Service
- A selfie-only request (no burst frames) returns
is_real_person: falsewith a liveness-related reason. - A request with burst frames is scored on actual blink/head-movement evidence, and a static-photo replay does not pass.
Tests
- A test pins the selfie-only refusal and a test pins a burst-based positive/negative result.
Documentation
- The AI service README states the minimum liveness requirement so callers (backend/mobile) know to supply burst frames.
Out of scope
Face-matching/1:1 verification against a reference image and fraud-clustering scoring are separate issues.
Getting started
Files: app/ai-service/proof_of_life.py, app/ai-service/main.py (ProofOfLifeRequest/ProofOfLifeResponse).
cd app/ai-service
pytestGood first files to read: proof_of_life.py analyze/_score_liveness, then main.py ProofOfLifeRequest.
Activity
Opulencechuks commentedon Aug 19, 2026
I am confident that I can implement this issue having worked on similar issues in the past.
northersubair commentedon Aug 20, 2026
can i work on this?
grantfox-oss commentedon Aug 20, 2026
🦊 GrantFox — @northersubair has been assigned to this issue as part of the Third Campaign campaign!
Next steps:
Closes #431)Good luck! Track your progress on GrantFox.
grantfox-oss commentedon Aug 22, 2026
🎉 This issue has been marked as completed on GrantFox as part of the Third Campaign campaign!
@northersubair's PR #441 was approved and merged by @kilodesodiq-arch.
🏆 @northersubair: You earned 35 FoxPoints for this contribution! Your current tier: Explorer (885 total points). Track your full progress on GrantFox.
👏 Great work, @northersubair! Keep contributing to ChainForgee.