Files
2026-03-10 19:14:51 -07:00

225 lines
8.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# age-verification
Implementation of `org.freedesktop.AgeVerification1` — a D-Bus interface for
California SB-976-style age bracket compliance on Linux, with an optional
privacy-preserving account sync backend.
---
## Architecture
```
┌──────────────────────────────────────────────────────────────┐
│ Linux OS (Kicksecure / Whonix / Debian / etc.) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ age-verification-daemon (C) │ system D-Bus │
│ │ /var/lib/age-verification/ │ org.freedesktop. │
│ │ ages.conf (root:root 0600) │ AgeVerification1 │
│ └────────────────┬─────────────────┘ │
│ │ D-Bus GetAgeBracket │
│ ┌────────────────▼─────────────────┐ │
│ │ age-verification-sync (Rust) │──────────────────────► │
│ │ Layered encryption: │ HTTPS (TLS) │
│ │ 1. TLS transport │ POST /api/v1/ │
│ │ 2. RSA-OAEP server pubkey │ age-bracket │
│ │ 3. Ed25519 device sig │ │
│ │ 4. AES-256-GCM local keystore │ │
│ └──────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
┌─────────────────────────────────┐
│ age-verification-server (Node) │
│ SQLite: stores bracket (1-4) │
│ ONLY — no raw age/dob stored │
│ │
│ GET /api/v1/query/bracket │◄── OS services /
│ (returns only bracket int) │ app stores
└─────────────────────────────────┘
```
### Age brackets
| Bracket | Meaning |
|---------|----------------------|
| 1 | Under 13 |
| 2 | 13 15 (inclusive) |
| 3 | 16 17 (inclusive) |
| 4 | 18 or older |
---
## Components
### 1. `daemon/` — C D-Bus daemon
Implements `org.freedesktop.AgeVerification1` on the **system bus**.
Runs as root. Stores age brackets (not raw ages) in `/var/lib/age-verification/ages.conf`.
**Build:**
```bash
cd daemon
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
sudo cmake --install build
sudo systemctl daemon-reload
sudo systemctl enable --now age-verification-daemon
```
**Test via dbus-send:**
```bash
# Set age
sudo dbus-send --system --print-reply \
--dest=org.freedesktop.AgeVerification1 \
/org/freedesktop/AgeVerification1 \
org.freedesktop.AgeVerification1.SetAge \
string:youruser uint32:25
# Set date of birth
sudo dbus-send --system --print-reply \
--dest=org.freedesktop.AgeVerification1 \
/org/freedesktop/AgeVerification1 \
org.freedesktop.AgeVerification1.SetDateOfBirth \
string:youruser string:2000-06-15
# Get bracket
dbus-send --system --print-reply \
--dest=org.freedesktop.AgeVerification1 \
/org/freedesktop/AgeVerification1 \
org.freedesktop.AgeVerification1.GetAgeBracket \
string:youruser
```
---
### 2. `sync-client/` — Rust sync client
Reads the bracket from the local daemon and uploads it to your account server
using layered encryption:
1. **TLS** — all traffic over HTTPS via rustls
2. **RSA-4096 OAEP-SHA256** — bracket payload encrypted with the server's public key
3. **Ed25519 device keypair** — payload signed to prove device identity
4. **AES-256-GCM + PBKDF2** — local keystore encrypted with account passphrase
**Build:**
```bash
cd sync-client
cargo build --release
# Binary at target/release/age-verification-sync
```
**First-time device registration** (do this once after account creation):
```bash
# Register device with server — you'll need to extract the Ed25519 pubkey
# from the keystore or add a `--print-pubkey` subcommand for this.
# See TODO in src/main.rs for a planned 'register-device' subcommand.
```
**Sync:**
```bash
age-verification-sync \
--user alice \
--server https://accounts.example.com \
--token "$YOUR_ACCOUNT_JWT"
```
**Deploy as a systemd timer** (syncs on login or periodically):
```ini
# /etc/systemd/user/age-verification-sync.timer
[Unit]
Description=Sync age bracket
[Timer]
OnBootSec=30s
OnUnitActiveSec=24h
[Install]
WantedBy=timers.target
```
---
### 3. `server/` — Node.js account server
Express + SQLite backend. Stores **only the bracket integer** per account.
**Setup:**
```bash
cd server
npm install
npm run keygen # generates keys/server-private.pem + server-public.pem
cp .env.example .env # fill in JWT_SECRET
mkdir -p data
npm start
```
**Distribute the public key to OS installs:**
```bash
scp keys/server-public.pem \
root@client:/etc/age-verification/server-pubkey.pem
```
**API endpoints:**
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/api/v1/auth/register` | — | Create account |
| POST | `/api/v1/auth/login` | — | Get JWT |
| POST | `/api/v1/auth/register-device` | JWT | Register device Ed25519 pubkey |
| POST | `/api/v1/age-bracket` | JWT | Upload encrypted bracket from sync client |
| GET | `/api/v1/query/bracket` | JWT | Get bracket for this account (returns `{bracket: N}` only) |
**Production deployment:**
- Put nginx in front (TLS termination, `proxy_pass http://127.0.0.1:3000`)
- Use a process manager: `pm2 start src/index.js --name age-verif`
- Back up `data/ageverif.db` and `keys/server-private.pem` (separately, securely)
---
## Privacy properties
| Property | How it's achieved |
|----------|------------------|
| Minimum data on disk | Daemon stores bracket (14), not raw age |
| Minimum data on server | Server stores bracket (14), not raw age |
| Local file not world-readable | `/var/lib/age-verification/ages.conf` is root:root 0600 |
| Local keystore encrypted | AES-256-GCM keyed from PBKDF2(account passphrase, salt) |
| Transport encrypted | TLS (reqwest/rustls, HTTPS only) |
| Server can't be MITM'd | Payload also RSA-OAEP encrypted with server pubkey |
| Device identity verified | Ed25519 signature on every sync payload |
| Server returns minimum | `/query/bracket` returns `{bracket: N}` only |
---
## D-Bus interface reference
```xml
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node name="/org/freedesktop/AgeVerification1">
<interface name="org.freedesktop.AgeVerification1">
<method name="SetAge">
<arg type="s" name="User" direction="in"/>
<arg type="u" name="YearsOfAge" direction="in"/>
</method>
<method name="SetDateOfBirth">
<arg type="s" name="User" direction="in"/>
<arg type="s" name="Date" direction="in"/>
</method>
<method name="GetAgeBracket">
<arg type="s" name="User" direction="in"/>
<arg type="u" name="AgeBracket" direction="out"/>
</method>
</interface>
</node>
```
**Errors:**
- `org.freedesktop.AgeVerification1.Error.NoSuchUser`
- `org.freedesktop.AgeVerification1.Error.PermissionDenied`
- `org.freedesktop.AgeVerification1.Error.InvalidDate`
- `org.freedesktop.AgeVerification1.Error.AgeUndefined`
- `org.freedesktop.AgeVerification1.Error.Internal`