Fiveo1 Secure Voting Poll @2026-09-28 14:59:07 - Calle 15 de Febrero
Calle 15 de Febrero |
Abstract
The Fiveo1 Secure Voting Poll is a single-file, self-hosted, mobile-first polling system that binds each yes/no vote to a cryptographic footprint signature — a SHA-256 hash of the voter's network address, device fingerprint, screen geometry, and GPS position. The signature is computed exclusively on the server, stored in a local SQLite database, and enforced by a SQL UNIQUE(question_id, signature) constraint. Once a signature has voted on a person, that signature can never vote on that person again — regardless of cache clearing, cookie deletion, browser reinstall, or session reset. GPS movement produces a new signature, allowing a voter to cast a fresh vote from a genuinely different physical location. Every vote is stored as a complete, immutable line entry containing IP, device, screen, GPS coordinates, reverse-geocoded location, timestamp, and signature hash. A cumulative OpenStreetMap renders every recorded footprint as a green (yes) or red (no) marker.
---
1. Design Goals
Goal Implementation
One vote per person, per device, per location, forever Server-computed signature + SQLite UNIQUE constraint
Zero client-side trust Signature computed only on the server using server-observable headers
Cache/cookie/history-proof Enforcement lives in the database, not the browser
Mobile-only, no keyboard, no mouse All controls are ≥52px tap targets; no text inputs on the public page
Complete footprint capture per vote Every vote row stores IP, UA, screen, GPS, accuracy, reverse-geocoded location, timestamp, and signature
Cumulative geographic visualization Leaflet + OpenStreetMap, one marker per vote, colour-coded by answer
Zero external dependencies beyond Leaflet tiles Single SQLite file, single PHP file, CDN only for the map library
Auto-provisioning DB and schema are created on first page load
---
2. System Architecture
```
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ Voter's Phone │ │ Apache / Nginx + PHP-FPM │
│ ──────────────────────── │ │ ──────────────────────── │
│ • Mobile browser │ HTTPS │ • index.php (public) │
│ • Geolocation API │ ─────► │ • admin.php (password) │
│ • Leaflet.js (CDN) │ │ │
│ • Nominatim reverse geocode │ │ ┌──────────────┐ │
│ │ │ │ fiveo1.sqlite│ │
└──────────────────────────────┘ │ │ (WAL mode) │ │
│ └──────────────┘ │
└──────────────────────────────┘
```
Components
Layer Responsibility
index.php Renders live footprint card, totals, person feed, map button; serves JSON APIs for signatures, votes, and map points; receives vote POSTs
admin.php Password-gated question manager; view/delete questions, wipe votes, view latest signatures
fiveo1.sqlite Single-file embedded database; WAL mode for concurrent read safety
Leaflet 1.9.4 Client-side map rendering; loads tiles from OpenStreetMap
Nominatim Free reverse-geocoding service; converts lat/lon to a human-readable street name
---
3. The Footprint Signature
3.1 Definition
The signature is a hex-encoded SHA-256 digest of five fields, pipe-separated:
```
signature = SHA256( ip | user_agent | screen | lat₃ | lon₃ )
```
Component Source Notes
ip $_SERVER['REMOTE_ADDR'] Never client-supplied; cannot be spoofed over TCP
user_agent $_SERVER['HTTP_USER_AGENT'] The header Apache/PHP actually received
screen Client screen.width × screen.height normalised to max×min Rotation-proof
lat₃ Client GPS latitude, rounded to 3 decimals ~110 m resolution
lon₃ Client GPS longitude, rounded to 3 decimals ~110 m resolution
3.2 Why GPS Is Rounded to 3 Decimals
At 4 decimals (~11 m), a stationary phone drifts 5–20 m between page loads due to GPS jitter, producing a different signature on each visit and defeating the lock. At 3 decimals (~110 m), real drift is absorbed, but a voter who physically relocates to a different block produces a genuinely new signature.
3.3 Why the Server Computes the Signature
An earlier prototype had the client compute the signature and send it to the server. This failed for two reasons:
1. Truncation mismatch — if the server stored substr($UA, 0, 400) but the client hashed the full UA, the hashes differed by thousands of bits, and duplicate detection never fired.
2. Client tamperability — a malicious client could hash whatever it wanted and invent unlimited signatures.
The current design eliminates both: the server is the sole authority on the signature. The client sends only screen, lat, and lon, and receives back the resulting signature for display. It cannot forge a signature because it does not know what the server hashed for ip and user_agent.
3.4 Signature Stability Table
Scenario IP UA Screen GPS₃ Signature Allowed to Vote Again?
Same phone, same place, cleared cache same same same same same ❌ Blocked
Same phone, same place, new browser same same same same same ❌ Blocked
Same phone, moved 500 m down the street same same same different different ✅ New line entry
Same phone, switched Wi-Fi to cellular different same same same different ✅ New line entry
Same phone, OS update changed UA string same different same same different ✅ New line entry
Different phone, same Wi-Fi, same place same different different same different ✅ New line entry
---
4. Database Schema
```sql
CREATE TABLE questions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE votes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
question_id INTEGER NOT NULL,
signature TEXT NOT NULL,
answer TEXT NOT NULL, -- 'yes' | 'no'
ip TEXT,
device TEXT, -- UA (truncated to 400 chars for storage)
screen TEXT,
lat REAL,
lon REAL,
accuracy REAL, -- GPS accuracy in metres
location TEXT, -- reverse-geocoded street/city
user_id TEXT, -- persistent localStorage UUID
created_at TEXT NOT NULL,
UNIQUE(question_id, signature) -- ★ the master key constraint
);
CREATE INDEX idx_votes_q ON votes(question_id);
CREATE INDEX idx_votes_sig ON votes(question_id, signature);
```
The Master Key
The UNIQUE(question_id, signature) constraint is the security core of the system. It is enforced at the SQLite engine level, not in application code. Even if the application layer were bypassed, the database itself would reject a duplicate insert with a SQLITE_CONSTRAINT error, which the PHP handler translates into an HTTP 409 duplicate_signature response.
Once a row exists for a given (question_id, signature) pair, it is a master key. No second row with the same pair can ever exist. The vote is a permanent, single-use token scoped to that specific person.
---
5. Vote Lifecycle
5.1 Pre-Vote (Silent Verification)
```
1. Page loads → client requests ?action=client_ip → server echoes REMOTE_ADDR
2. Client requests GPS via navigator.geolocation.getCurrentPosition()
3. On GPS lock, client requests ?action=my_signature&screen=…&lat=…&lon=…
4. Server recomputes the signature and returns:
{ ok:true, signature:"abc…", voted:{ "12":"yes", "34":"no", … } }
5. Client iterates .person[data-qid] and for each qid in voted:
· disables both YES and NO buttons
· marks the chosen button with .chosen (green/red)
· shows "✔ Your footprint already voted YES on this person."
For qids not in voted, both buttons are unlocked.
```
This check happens before the voter touches anything. If they have already voted, they see it immediately. There is no "vote then discover you're blocked" — the lock is visible on load.
5.2 The Vote
```
1. Voter taps YES or NO on a person card
2. Both buttons on that card are disabled immediately (optimistic UI)
3. POST / with JSON body:
{ question_id, answer, screen, lat, lon, accuracy, location, user_id }
NOTE: no signature is sent. The server will compute it.
4. Server:
· reads REMOTE_ADDR and HTTP_USER_AGENT from the request
· recomputes signature = SHA256(ip | ua | screen | lat₃ | lon₃)
· INSERT INTO votes (…) VALUES (…)
5. If INSERT succeeds: → 200 { ok:true, yes, no, signature }
If UNIQUE violation: → 409 { ok:false, error:"duplicate_signature", yes, no }
6. Client updates:
· .p-counts with the new yes/no totals
· markVoted(person, answer) → locks both buttons, shows ✔ message
· localStorage f1votes[qid] = answer (fast-path for next load)
· global totals bar (sum of all per-person counts)
```
5.3 Guarantees
· Atomicity — the INSERT either succeeds or fails; there is no partial write.
· Idempotence of display — whether the vote is a fresh success or a rejected duplicate, the client ends up showing the correct locked state and totals.
· Server-authoritative counting — the yes/no counts returned in the response are recomputed by the server from the votes table, not derived from client input.
---
6. Threat Model
Threat Mitigation
Clearing cache / cookies / history to re-vote Enforcement lives in SQLite, not the browser. Same footprint → same signature → 409.
Switching to private/incognito mode Incognito does not change IP, UA, or GPS. Signature unchanged. Blocked.
Reinstalling the browser Same IP/UA/GPS → same signature. Blocked.
Using a different phone on the same Wi-Fi Different UA and screen → different signature. Allowed (this is by design; the poll is per-device, not per-household).
Spoofing GPS via developer tools Any spoofed GPS produces a signature that must match a stored one to be blocked; a new spoof produces a new signature and a new vote. Detected only if the admin observes the map. Mitigation: admin can audit the signature, ip, and location columns in admin.php.
Man-in-the-middle Terminate TLS at the reverse proxy. Set HSTS. The API accepts JSON POSTs only; no cookies carry vote state.
Brute-force admin password 5-attempt lockout with 60-second cooldown; hash_equals constant-time comparison; 400 ms artificial delay on failure.
SQL injection All queries use PDO prepared statements with bound parameters.
XSS in question text or footprint display All output passes through htmlspecialchars($s, ENT_QUOTES).
Vote replay with a stolen signature The signature is not a secret; it is a public identifier of the footprint. An attacker who knows the signature still cannot insert a duplicate because the DB constraint rejects it. The only way to vote is to actually be the footprint.
Carrier IP rotation mid-session If the carrier rotates the IP between the pre-check and the vote POST, the signature will differ and the vote will succeed even if the pre-check said "already voted". Mitigation: pre-check is advisory; the DB constraint is the final arbiter. The window is small (typically one page load).
6.1 Known Non-Mitigations
· VPN switching. A voter can rotate IPs and cast multiple votes, each from a distinct exit node. Fixing this would require a trusted identity provider, which is out of scope for a zero-login, zero-account poll.
· Shared devices. A household with one phone cannot give each member a vote. This is a deliberate trade-off.
· GPS jitter on the edge of a rounding boundary. A phone sitting exactly between two 3-decimal squares may occasionally produce a different signature. In practice this is rare; the rounding error is bounded by ~55 m, well below the drift of a stationary phone indoors.
---
7. Reverse Geocoding
On GPS lock the client calls:
```
GET https://nominatim.openstreetmap.org/reverse?format=jsonv2
&lat=…&lon=…&zoom=16&addressdetails=1
```
The server never calls out — the client does. The resulting street name (road, pedestrian, neighbourhood, suburb, village, town, or city, in that priority order) is stored in votes.location and displayed on the card header, in the feed, and in the popup of each map marker.
Nominatim's public usage policy requires a valid User-Agent and a request rate under 1 per second. The page hits it exactly once per GPS lock, so this is satisfied.
---
8. Map Rendering
The map is served by Leaflet 1.9.4 (CDN) and OpenStreetMap tiles.
Behaviour Implementation
Cumulative view ?action=map_points returns all votes with non-null lat/lon
Colour code Green circle for yes, red circle for no
Radius 7 px for historical points; 10 px for a single GPS tap from the live card
Popup Person name, answer, timestamp, reverse-geocoded location, signature prefix (16 chars)
Zoom fitBounds on all points with maxZoom:16; single point centres at zoom 16; GPS tap centres at zoom 17
Legend Fixed bottom-left, shows the green/red key
The map overlay is fixed-position and occupies 84 vh (max 640 px), with a clear "Close" button. Tapping the dark surround also closes it. No keyboard is ever required.
---
9. The Person Feed
Every question is a person. The public page renders them in id order, each as a person card:
```
┌────────────────────────────────────────────┐
│ Evan Winter │
│ Central America Negotiations & Logistics… │
│ @2026-09-28 15:28:18 · Calle 15 de Febrero │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ YES │ │ NO │ │
│ └───────────────┘ └───────────────┘ │
│ ✔ Your footprint already voted YES. │
│ │
│ Y 2 N 0 2 total │
│ ──────────────────────────────────────── │
│ show footprints ▾ │
└────────────────────────────────────────────┘
```
Tapping the name or the toggle expands the card to show each vote as a full footprint block: badge, timestamp, location, IP, device, screen, GPS, and signature prefix. Tapping a GPS value opens the map focused on that single coordinate.
The 53 seeded people cover the Fiveo1 contact list plus the November 3, 2026 California general election officials named in the supplied document (San Diego, Riverside, San Bernardino, Corona, Apple Valley, Victorville).
---
10. Administration
admin.php is protected by a single password (@Evan.Winter in the shipped build) with the following controls:
· Add a question / person. Format: Name — Role · Office · City.
· View all questions with yes/no counts and unique-signature counts per person.
· Delete a single question and its votes (transactional).
· Wipe all votes across every question (keeps the questions).
· Latest 40 signatures with IP, location, timestamp, and signature prefix.
· Sign out.
The admin page is not linked from the public page. It must be reached by typing its URL. The public footer shows only:
```
Fiveo1
PO Box 312786
Avalon, California 90704
Evan Winter
```
---
11. Deployment Requirements
Requirement Value
PHP 7.4 or newer
Extensions pdo_sqlite, json, session
Filesystem Write permission on the directory containing index.php (for fiveo1.sqlite and its WAL/SHM siblings)
Web server Apache or Nginx with TLS termination
Files index.php, admin.php
Database Auto-created on first request
Outbound Only the voter's browser contacts Nominatim and OSM tiles; the server makes no outbound calls
11.1 Hardening Checklist
☐ Force HTTPS; enable HSTS.
☐ Set session.cookie_secure=1, session.cookie_httponly=1, session.cookie_samesite=Lax.
☐ Change ADMIN_PASSWORD in admin.php.
☐ Disable directory listing.
☐ Add a robots.txt disallow for admin.php.
☐ Back up fiveo1.sqlite (and its -wal / -shm siblings) on a schedule.
☐ Set upload_max_filesize low and disable allow_url_fopen if not needed.
☐ Restrict write access to the SQLite directory to the PHP-FPM user only.
---
12. Operational Notes
· WAL mode is enabled so reads never block writes. Readers see a consistent snapshot even while a vote is being inserted.
· Signature is stored in plaintext in votes.signature. It is a SHA-256 digest, not a secret. Storing it plainly allows efficient UNIQUE enforcement and fast my_signature lookups.
· UA is truncated to 400 chars for storage only. The full UA is used for hashing.
· The user_id column holds a 16-hex-char UUID generated once per browser via crypto.getRandomValues and kept in localStorage. It is used only for human-readable logs and future enhancements; it is not part of the signature and clearing it does not unlock the vote.
· Timezone in all timestamps is the server's local time. If a single canonical timezone is required, set it at the top of index.php with date_default_timezone_set('America/Los_Angeles').
---
13. Limitations & Future Work
Limitation Possible future direction
VPN rotation defeats IP binding Optional email or SMS verification gate
Household shares one phone Optional per-voter passcode issued by admin
GPS spoofing undetectable Server-side accuracy sanity check; flag implausible coordinates
No audit trail for admin actions Append-only admin_log table
No rate-limiting on the vote endpoint Per-IP token bucket in front of the INSERT
Nominatim rate limits Self-hosted Nominatim or a paid geocoder
SQLite single-writer limit Migrate to PostgreSQL if concurrency exceeds ~100 writes/sec
No export Admin CSV export of the full votes table
---
14. Conclusion
The Fiveo1 Secure Voting Poll achieves a strict property — one yes/no vote per footprint, per person, forever — using only a single PHP file, a single SQLite file, and a server-side SHA-256 hash of IP, User-Agent, screen geometry, and GPS rounded to ~110 m. The enforcement is not in JavaScript, not in cookies, and not in the client's goodwill: it is a UNIQUE constraint enforced by the SQLite engine itself. Every vote produces a full forensic line entry (IP, device, screen, GPS, location, timestamp, signature hash), and every entry is plotted cumulatively on an OpenStreetMap so the administrator can see exactly where and when each answer was cast. The system is mobile-only, touch-only, keyboard-free, and zero-login, and it is designed to be deployed as a pair of files into any public_html directory with PHP and pdo_sqlite.
---
End of whitepaper — Fiveo1 Secure Voting Poll v1.0 ← Back to Fiveo1