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:
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:
# 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:
- TLS — all traffic over HTTPS via rustls
- RSA-4096 OAEP-SHA256 — bracket payload encrypted with the server's public key
- Ed25519 device keypair — payload signed to prove device identity
- AES-256-GCM + PBKDF2 — local keystore encrypted with account passphrase
Build:
cd sync-client
cargo build --release
# Binary at target/release/age-verification-sync
First-time device registration (do this once after account creation):
# 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:
age-verification-sync \
--user alice \
--server https://accounts.example.com \
--token "$YOUR_ACCOUNT_JWT"
Deploy as a systemd timer (syncs on login or periodically):
# /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:
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:
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.dbandkeys/server-private.pem(separately, securely)
Privacy properties
| Property | How it's achieved |
|---|---|
| Minimum data on disk | Daemon stores bracket (1–4), not raw age |
| Minimum data on server | Server stores bracket (1–4), 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
<!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.NoSuchUserorg.freedesktop.AgeVerification1.Error.PermissionDeniedorg.freedesktop.AgeVerification1.Error.InvalidDateorg.freedesktop.AgeVerification1.Error.AgeUndefinedorg.freedesktop.AgeVerification1.Error.Internal