Skip to content

proof-of-life passes a single static selfie: burst-free requests report is_real_person true with only face detection #431

Description

@kilodesodiq-arch

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_evidence

With 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

  1. It is a policy/security decision, not a boolean flip. Simply requiring burst_required would 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.
  2. 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.
  3. It crosses the request contract. ProofOfLifeRequest is a public Pydantic schema consumed by the backend (app/backend/src/verification and the AI client); changing optional→required or adding a liveness_mode is a breaking contract change.
  4. Confidence weighting must stay honest. Any fix must ensure confidence cannot 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: false with 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
pytest

Good first files to read: proof_of_life.py analyze/_score_liveness, then main.py ProofOfLifeRequest.

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaignarea:ai-serviceAI service (FastAPI) areabugSomething isn't workinghighHigh severity issues

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions