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

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:

  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:

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.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

<!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
Description
No description provided
Readme 65 KiB
Languages
C 49.9%
Rust 45.8%
CMake 4.3%