Signing & Verification
Ed25519 signing, keys, and public verification.
AttendiBot's core differentiator is cryptographically verifiable voice attendance. Every completed session and archived period snapshot is signed with Ed25519, so anyone can prove records were not tampered with.
What gets signed
Voice sessions
When a member leaves a tracked voice channel, AttendiBot:
- Finalizes the session (duration, user, channel, timestamps)
- Serializes the payload to canonical JSON
- Computes a SHA-256 hash of the canonical payload
- Signs the canonical bytes with the server's Ed25519 private key
- Stores the session UUID, payload hash, and signature in PostgreSQL
Period snapshots
When a leaderboard period resets, AttendiBot:
- Builds a snapshot payload (period metadata + all leaderboard entries)
- Signs the snapshot with the same Ed25519 key
- Archives the signed snapshot for later verification
Key lifecycle
1. Generate keys (once per server)
An admin generates an Ed25519 keypair with a passphrase:
- Dashboard → Signing → Generate keys
- Or
/admin generate-keys passphrase:...
The private key is encrypted with AES-256-GCM using a key derived from the passphrase via Argon2id (memory-hard KDF). Only the encrypted blob and public key are stored in the database.
2. Unlock signing (after every restart)
The private key exists in memory only while unlocked. After every backend restart:
- Dashboard → Signing → Unlock signing
- Or
/admin unlock-signing passphrase:...
If signing is locked, new voice sessions are not signed and completed sessions are dropped on leave.
3. Share the public key
Anyone verifying sessions needs the server's public key:
- Dashboard → Signing → Public key
- Or
/admin public-key
The public key is safe to share publicly — it cannot sign new records.
Verification workflow
Public session verifier
Anyone can verify a session without logging in:
- Go to attendibot.com/#verifier
- Enter the session UUID
- AttendiBot fetches the session, recomputes the payload hash, and verifies the Ed25519 signature against the server's public key
API endpoint: GET /verify/session/{session_id}
Response:
{ "valid": true }Verify in Discord
Admins can verify sessions and period archives directly:
/admin verify-session session_id:550e8400-e29b-41d4-a716-446655440000
/admin verify-period period_index:1Verify period archives (public)
API endpoint: GET /verify/period/{guild_id}/{period_index}
Returns whether the archived period snapshot signature is valid.
Cryptographic details
| Component | Algorithm |
|---|---|
| Signing | Ed25519 (ed25519-dalek) |
| Payload hashing | SHA-256 of canonical JSON |
| Private key encryption | AES-256-GCM |
| Key derivation | Argon2id (m=19456, t=2, p=1) |
| Public key encoding | Base64 |
The signing implementation lives in backend/crates/shared/src/signing.rs:
generate_keypair()— create and encrypt keypairdecrypt_signing_key()— unlock with passphrasesign_payload()— canonical JSON → hash → Ed25519 signatureverify_signature()— recompute hash, verify signature
Security best practices
- Use a strong passphrase (≥ 8 characters; longer is better)
- Unlock after every restart — automate alerts if signing stays locked
- Limit Manage Server access — only trusted admins can generate/unlock keys
- Share public keys openly — verification requires the public key, not the private key
- Back up your database — encrypted private keys and all session records live in PostgreSQL
- Key rotation — generating new keys invalidates verification of sessions signed with the old key; plan rotation carefully
What happens when signing fails
| State | Behavior |
|---|---|
| No keys configured | Sessions not recorded; admin prompted to generate keys |
| Keys locked (post-restart) | Active sessions tracked in memory but dropped on leave |
| Wrong passphrase on unlock | Unlock fails; signing remains locked |
| Invalid signature on verify | Verifier returns valid: false |
Related docs
- Getting Started — initial key setup
- Commands — signing admin commands
- FAQ — common signing questions