Freeq implements a custom SASL mechanism for authenticating IRC users with
AT Protocol (Bluesky) identities. The mechanism name is ATPROTO-CHALLENGE.
Client Server
| |
| CAP REQ :sasl |
|------------------------------>|
| CAP ACK :sasl |
|<------------------------------|
| AUTHENTICATE ATPROTO-CHALLENGE
|------------------------------>|
| AUTHENTICATE <base64 challenge>
|<------------------------------|
| AUTHENTICATE <base64 response>
|------------------------------>|
| 900 RPL_LOGGEDIN |
| 903 RPL_SASLSUCCESS |
|<------------------------------|
The server sends a JSON challenge encoded as base64url:
{
"session_id": "<unique per TCP connection>",
"nonce": "<32 bytes, cryptographically random, base64url>",
"timestamp": <unix epoch seconds>
}The client responds with base64url-encoded JSON:
{
"did": "did:plc:abc123...",
"method": "crypto" | "pds-session" | "pds-oauth",
"signature": "<base64url signature over raw challenge bytes>",
"pds_url": "https://bsky.social"
}-
crypto— Client signs the raw challenge bytes with a key listed in the DID document'sauthenticationorassertionMethodsections. Supported curves: secp256k1 (required), ed25519 (recommended). -
pds-session— Client provides a Bearer JWT (from an app password session). Server callscom.atproto.server.getSessionon the claimed PDS to verify the token belongs to the claimed DID. -
pds-oauth— Client provides a DPoP-bound OAuth access token. Server constructs a DPoP proof and callsgetSessionon the PDS.
- Nonce uniqueness: Each challenge contains a 32-byte cryptographically random nonce. Challenges are single-use (invalidated after verification).
- Timestamp window: Challenges expire after 60 seconds (configurable
via
--challenge-timeout-secs). - No private key transmission: The server never sees private keys. All verification uses public keys from DID documents or PDS token validation.
- DID document resolution: The server resolves
did:plcvia plc.directory anddid:webvia HTTPS, then extracts verification keys.
- JSON encoding: Both challenge and response are JSON (not binary). This aids debuggability at the cost of a few extra bytes. A production IRCv3 specification would likely use a binary format.
- Multi-method auth: The mechanism supports three verification methods (crypto, pds-session, pds-oauth). A formal spec might split these into separate SASL mechanism names.
- Signature over raw bytes: The signature is over the raw challenge bytes (the decoded JSON), not a hash. This is simpler but means the signed payload is larger than strictly necessary.
When a user authenticates, their nick is bound to their DID. This binding:
- Persists across server restarts (stored in SQLite)
- Prevents other users from using the nick
- Unauthenticated users claiming a registered nick are renamed to
GuestXXXX - Propagated across federated servers via CRDT
- Founder: The first authenticated user to create a channel becomes its founder. Founder status is permanent and survives server restarts.
- DID ops: Channel operators can be granted by DID. DID-based ops persist across reconnects and work across federated servers.
- DID bans:
MODE +b did:plc:xyzbans by identity rather than hostmask. DID bans survive nick changes.
Freeq adds custom WHOIS numerics:
- 330 (RPL_WHOISACCOUNT): Shows the authenticated DID
- 671: Shows the resolved AT Protocol handle (e.g.
chadfowler.com) - 672: Shows the iroh P2P endpoint ID (if connected via iroh)
All transports feed into the same IRC protocol handler. The server is transport-agnostic — clients can mix transports freely.
| Transport | Port | Notes |
|---|---|---|
| TCP | 6667 | Standard IRC |
| TLS | 6697 | Standard IRC over TLS |
| WebSocket | configurable | IRC-over-WebSocket at /irc |
| iroh QUIC | auto | NAT-traversing, end-to-end encrypted |
The server advertises its iroh endpoint ID in CAP LS:
CAP * LS :sasl message-tags ... iroh=<endpoint-id>
Clients that support iroh can discover the endpoint and upgrade their connection to QUIC, gaining NAT traversal and relay fallback.
Servers connect to each other over iroh QUIC links using a JSON-based protocol. State convergence uses Automerge CRDTs for:
- Channel membership
- Topics
- Nick ownership
- DID-based ops
- Bans
See docs/s2s-audit.md for details on the S2S protocol.
Freeq supports these IRCv3 capabilities:
| Capability | Notes |
|---|---|
sasl |
ATPROTO-CHALLENGE mechanism |
message-tags |
Full tag routing per client |
server-time |
Timestamps on history replay |
batch |
History wrapped in chathistory batch |
multi-prefix |
All prefix chars in NAMES |
echo-message |
Echo own messages back |
account-notify |
ACCOUNT broadcast on auth |
extended-join |
JOIN includes account + realname |
draft/chathistory |
On-demand CHATHISTORY command |
Freeq supports server plugins that hook into events:
| Hook | Description |
|---|---|
on_connect |
New client connection (before registration) |
on_auth |
SASL authentication complete (can override displayed identity) |
on_join |
User joins a channel |
on_message |
PRIVMSG/NOTICE (can suppress or rewrite) |
on_nick_change |
Nick change |
Plugins are compiled into the binary and activated by name via CLI or
TOML config files. See examples/plugins/ for examples.