featherChat as Identity Provider: - OIDC provider endpoints (/auth/oidc/authorize, /token, /userinfo) - JWT tokens with fingerprint, alias, eth_address, groups claims - Authentik integration (featherChat as upstream IdP, group sync) - SAML support for enterprise Smart Contract Access Control: - FeatherChatACL Solidity contract (server/group/feature access) - secp256k1 address from same BIP39 seed = on-chain identity - NFT-gated access (ERC-721/ERC-1155 membership) - Token-gated access (ERC-20 staking) - DAO governance for group membership decisions - UUPS upgradeable proxy pattern Hybrid architecture: - OIDC token carries on-chain permissions as claims - Event-driven sync (WebSocket RPC + periodic poll + sled cache) - L2 deployment (Arbitrum/Base/Polygon) for low gas costs Feasibility: 7-11 weeks across 4 phases. Comparison with SpruceID, Ceramic, Lens, XMTP. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
54 KiB
featherChat as Identity Provider & Smart Contract Permission System
Version: 0.0.20 Status: Feasibility & Design Document (Pre-Implementation) Date: 2026-03-28
1. Executive Summary
featherChat already possesses a robust cryptographic identity layer: every user holds a single BIP39 seed (32 bytes, 24 mnemonic words) from which three distinct keypairs are deterministically derived via HKDF-SHA256 with domain-separated info strings (identity.rs:29-48):
- Ed25519 (
info="warzone-ed25519") — signing and identity verification; the fingerprint isSHA-256(Ed25519_pubkey)[:16] - X25519 (
info="warzone-x25519") — Diffie-Hellman key exchange for X3DH and Double Ratchet - secp256k1 (
info="warzone-secp256k1") — Ethereum-compatible signing; the Ethereum address isKeccak-256(uncompressed_pubkey[1:])[-20:](ethereum.rs:80-106)
This dual-curve identity model, combined with the existing challenge-response authentication (auth.rs), alias system (aliases.rs), and group management (groups.rs), makes featherChat a natural candidate to serve two additional roles:
-
Identity Provider (IdP) — featherChat becomes the authoritative source of identity for external services. Organizations configure Authentik, Keycloak, Grafana, Gitea, and other applications to accept "Sign in with featherChat." The server issues OIDC tokens containing the user's fingerprint, alias, Ethereum address, and group memberships.
-
Smart Contract-Based Permission System — an on-chain access control layer on Ethereum (or an L2 such as Arbitrum, Base, or Polygon) governs who can join a server, who can access specific groups, and who holds administrative privileges. Because the user's secp256k1 address is already derived from the same seed, the Ethereum address IS the featherChat identity — no bridging or linking step is needed.
This document provides a technical feasibility analysis, concrete design proposals, and a phased implementation roadmap.
2. featherChat as an Identity Provider (IdP)
2.1 Current Authentication Architecture
The server already implements a challenge-response flow in warzone-server/src/routes/auth.rs:
1. Client → POST /v1/auth/challenge { fingerprint }
2. Server → { challenge: random_hex(32), expires_at: now + 60s }
3. Client → POST /v1/auth/verify { fingerprint, challenge, signature }
(signature = Ed25519_sign(challenge_bytes, identity_signing_key))
4. Server → verifies Ed25519 signature against stored PreKeyBundle
→ issues bearer token (random 64 hex chars), valid 7 days
→ stores token in sled `tokens` tree as JSON { fingerprint, expires_at }
5. Client → uses Authorization: Bearer <token> on subsequent requests
Key implementation details from auth.rs:
TOKEN_TTL_SECS = 7 * 24 * 3600(7 days) — line 27CHALLENGE_TTL_SECS = 60— line 29- Challenges stored in-memory via
LazyLock<Mutex<HashMap>>— line 54 - Token validation via
validate_token()reads from the sledtokenstree — lines 177-186 - The server extracts the Ed25519 verifying key from the stored
PreKeyBundle(bincode-deserialized) and verifies the challenge signature — lines 117-154
This is already 80% of what an OIDC provider needs. The missing pieces are standardized token formats (JWT), discovery metadata, and the userinfo endpoint.
2.2 OIDC/OAuth2 Provider Design
Concept: The featherChat server acts as a fully compliant OpenID Connect Provider. External services configure it as an upstream IdP. Users authenticate by proving knowledge of their seed (via the existing challenge-response), and the server issues standard OIDC tokens.
Login Flow:
External App (e.g., Grafana) featherChat Server User
│ │ │
│ "Sign in with featherChat" │ │
│ ← User clicks button ──────→│ │
│ │ │
│ 302 Redirect ──────────────→│ │
│ /auth/oidc/authorize? │ │
│ client_id=grafana& │ │
│ redirect_uri=...& │ │
│ scope=openid profile │ │
│ │ Challenge-response flow │
│ │←─────────────────────────→│
│ │ (existing auth.rs logic) │
│ │ │
│ 302 Redirect back │ │
│ ?code=AUTH_CODE ←───────────│ │
│ │ │
│ POST /auth/oidc/token │ │
│ { code, client_secret } │ │
│─────────────────────────────→│ │
│ ← { id_token, access_token }│ │
│ │ │
│ GET /auth/oidc/userinfo │ │
│ Authorization: Bearer ... │ │
│─────────────────────────────→│ │
│ ← { sub, fingerprint, │ │
│ alias, eth_address, │ │
│ groups } │ │
New Server Routes:
| Route | Method | Purpose |
|---|---|---|
/.well-known/openid-configuration |
GET | OIDC Discovery document |
/auth/oidc/authorize |
GET | Authorization endpoint (redirect flow) |
/auth/oidc/token |
POST | Token endpoint (exchange code for JWT) |
/auth/oidc/userinfo |
GET | UserInfo endpoint (claims about the user) |
/auth/oidc/jwks |
GET | JSON Web Key Set (server's signing public key) |
/auth/oidc/register |
POST | Dynamic client registration (optional) |
JWT Token Claims:
The id_token is a JWT signed by the server's Ed25519 key (the same key infrastructure already exists in warzone-protocol). Claims include:
{
"iss": "https://wz.example.com",
"sub": "a3f8c912e7b04d6f1234567890abcdef",
"aud": "grafana",
"exp": 1711530000,
"iat": 1711443600,
"nonce": "random_from_client",
"fingerprint": "a3f8:c912:e7b0:4d6f:1234:5678:90ab:cdef",
"alias": "alice",
"eth_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"groups": ["ops", "dev", "general"],
"group_roles": {
"ops": "creator",
"dev": "member",
"general": "member"
}
}
The sub claim is the user's fingerprint hex (the 32-char lowercase hex string used internally, as produced by normalize_fp() in auth.rs:41-43). The fingerprint claim is the display format (xxxx:xxxx:...). The eth_address is the EIP-55 checksummed address from EthAddress::to_checksum() (ethereum.rs:58-75). The groups claim is populated by scanning the server's groups sled tree for memberships (same logic as groups.rs:146-165).
JWT Signing:
The server needs its own signing key for OIDC tokens. Two options:
-
Server Ed25519 key — generate a server-level
Seedat first startup, derive an Ed25519 keypair, use it to sign JWTs. The JWK published at/auth/oidc/jwksexposes the Ed25519 public key in JWK format (kty: OKP,crv: Ed25519). This aligns with the existing cryptographic infrastructure. -
RSA-2048 or ES256 — more broadly compatible with existing OIDC libraries. Some OIDC clients do not support Ed25519 JWTs (
alg: EdDSA). ES256 (P-256 ECDSA) is a practical middle ground.
Recommendation: support both EdDSA (Ed25519) and ES256 as signing algorithms, with EdDSA as default. This maximizes compatibility while staying consistent with featherChat's crypto philosophy.
Discovery Document (.well-known/openid-configuration):
{
"issuer": "https://wz.example.com",
"authorization_endpoint": "https://wz.example.com/auth/oidc/authorize",
"token_endpoint": "https://wz.example.com/auth/oidc/token",
"userinfo_endpoint": "https://wz.example.com/auth/oidc/userinfo",
"jwks_uri": "https://wz.example.com/auth/oidc/jwks",
"scopes_supported": ["openid", "profile", "groups", "eth"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["EdDSA", "ES256"],
"claims_supported": [
"sub", "fingerprint", "alias", "eth_address",
"groups", "group_roles", "iss", "aud", "exp", "iat"
]
}
Implementation in axum:
Following the existing pattern in routes/auth.rs where pub fn routes() -> Router<AppState>, a new module routes/oidc.rs would be created:
// warzone-server/src/routes/oidc.rs
pub fn routes() -> Router<AppState> {
Router::new()
.route("/auth/oidc/authorize", get(authorize))
.route("/auth/oidc/token", post(token))
.route("/auth/oidc/userinfo", get(userinfo))
.route("/auth/oidc/jwks", get(jwks))
.route("/.well-known/openid-configuration", get(discovery))
}
The authorize handler would initiate the existing challenge-response flow from auth.rs, but wrapped in the OIDC authorization code flow. Upon successful Ed25519 verification (reusing the exact logic at auth.rs:139-154), an authorization code is generated and stored in a new sled tree (oidc_codes). The token handler exchanges this code for a signed JWT.
Dependencies Required:
jsonwebtokencrate for JWT creation/validationserde_json(already present) for claims serialization- No external OIDC library needed — the protocol is simple enough for direct implementation on axum
2.3 SAML Support
For enterprise environments that require SAML 2.0:
- featherChat issues SAML assertions signed by the server's key
- Identity attributes in SAML: fingerprint (NameID), alias, groups, eth_address
- The SAML Response is signed using the same server key used for OIDC JWTs
- Implementation:
routes/saml.rswith endpoints for SAML metadata, SSO service, and assertion consumer
SAML is significantly more complex than OIDC (XML signing, canonicalization, deflate encoding). Recommendation: implement OIDC first, add SAML only if specific enterprise demand exists. Most modern systems support OIDC, and Authentik can bridge OIDC-to-SAML for legacy apps.
2.4 Authentik Integration (Specific)
Authentik is an open-source identity provider that supports federated upstream IdPs. The integration architecture:
┌──────────────┐ OIDC ┌──────────────┐ OIDC/SAML ┌──────────────┐
│ featherChat │──────────────→│ Authentik │────────────────→│ Downstream │
│ Server │ (upstream) │ (broker) │ (downstream) │ Apps │
│ │ │ │ │ - Grafana │
│ IdP │ │ Maps users │ │ - Gitea │
│ source of │ │ Syncs groups │ │ - Portainer │
│ truth │ │ Applies │ │ - Wiki.js │
│ │ │ policies │ │ - etc. │
└──────────────┘ └──────────────┘ └──────────────┘
Configuration Steps:
- In Authentik: Create a new "OAuth2/OIDC Source" pointing to featherChat's discovery URL
- featherChat server: Register Authentik as an OIDC client (client_id, client_secret, redirect_uri stored in a new
oidc_clientssled tree) - Group Sync: featherChat's group memberships (from
groups.rs) are exposed in the OIDC token'sgroupsclaim. Authentik maps these to its own groups. Authentik groups then map to application permissions.
Sync Flow:
featherChat group "ops"
→ OIDC token: groups: ["ops"]
→ Authentik group: "featherchat-ops"
→ Grafana role: "Editor" on Ops dashboard
→ Gitea team: "ops-team" in "infrastructure" org
Benefits of the Authentik Broker Model:
- featherChat remains simple (just OIDC provider, no complex policy engine)
- Authentik handles policy enforcement, MFA wrapping, rate limiting, session management
- Downstream apps need zero featherChat-specific configuration
- Authentik provides an admin UI for managing the mapping
- Users get a unified "Sign in with featherChat" across all services
Limitations:
- Adds a dependency on Authentik (additional service to maintain)
- Latency: user → featherChat → Authentik → app (two redirects)
- Group membership is eventually consistent (updated on next login/token refresh)
2.5 Direct IdP Mode (Without Authentik)
For simpler deployments, featherChat can serve as the IdP directly without Authentik:
User → featherChat (OIDC Provider) → Grafana (OIDC Client)
→ Gitea (OIDC Client)
→ Portainer (OIDC Client)
Each downstream app registers directly with featherChat as an OIDC client. The server manages the client registry in sled. This avoids the Authentik dependency but means featherChat must handle:
- Client registration/management
- Token revocation
- Session management for the OIDC flow (distinct from messaging sessions)
- A web-based consent/login page (HTML served from
routes/web.rspattern)
3. Smart Contract-Based Access Control
3.1 The Problem
featherChat currently has no access control beyond existence:
| Action | Current Behavior | Reference |
|---|---|---|
| Join server | Anyone with the URL can register a pre-key bundle | routes/keys.rs — POST /v1/keys/register has no auth check |
| Join group | Anyone can join any group by name | groups.rs:101-129 — join_group() auto-creates groups, no permission check |
| Create group | Anyone can create any group | groups.rs:77-99 — only checks if name is non-empty and not taken |
| Kick member | Only group creator | groups.rs:237-268 — group.creator != fp check |
| Admin alias removal | Password-protected | aliases.rs:383-399 — WARZONE_ADMIN_PASSWORD env var |
| Send messages | Anyone registered | No sender verification beyond having a valid fingerprint |
This is fine for trusted deployments but insufficient for organizations, paid services, or public-facing servers.
3.2 The Solution: On-Chain Permissions
A Solidity smart contract on an EVM-compatible chain stores permission grants. The critical insight: the user's Ethereum address IS their featherChat identity — both derive from the same BIP39 seed. derive_eth_identity() in ethereum.rs:80-106 produces the secp256k1 keypair and address using HKDF(seed, info="warzone-secp256k1"). No external linking, no bridging, no attestation needed.
3.3 How It Works
Server Registration (Access-Controlled):
User wants to register on featherChat server:
1. User generates identity from seed:
- Ed25519 fingerprint: a3f8:c912:e7b0:4d6f:...
- Ethereum address: 0xABC... (from same seed, ethereum.rs:80-106)
2. User sends: POST /v1/keys/register { fingerprint, bundle }
3. Server (NEW middleware):
a. Derive expected Ethereum address from the bundle's public key
(server can verify the secp256k1 pubkey corresponds to the Ed25519 pubkey
by requiring the user to sign the fingerprint with their secp256k1 key)
b. Call smart contract: FeatherChatACL.canJoinServer(0xABC...)
c. If true → proceed with registration (existing logic)
d. If false → HTTP 403 "not authorized on-chain"
4. Admin grants access:
a. Admin calls: FeatherChatACL.grantServerAccess(0xABC...)
b. Transaction mined on L2 (1-3 seconds on Arbitrum/Base)
c. User retries registration → succeeds
Group Join (Access-Controlled):
User wants to join group "ops":
1. Server (NEW check in join_group()):
a. Compute groupId = keccak256("ops")
b. Call contract: FeatherChatACL.canJoinGroup(0xABC..., groupId)
c. If true → existing join logic (groups.rs:101-129)
d. If false → { "error": "not authorized for this group on-chain" }
3.4 Smart Contract Design
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";
contract FeatherChatACL is AccessControl, UUPSUpgradeable {
bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE");
bytes32 public constant MODERATOR_ROLE = keccak256("MODERATOR_ROLE");
// Server-level access
mapping(address => bool) public serverAccess;
// Group-level access: user → groupId → authorized
mapping(address => mapping(bytes32 => bool)) public groupAccess;
// Feature-level access (extensible)
mapping(address => mapping(bytes32 => bool)) public featureAccess;
// Open-access mode: if true, anyone can join (contract only enforces bans)
bool public openServer;
mapping(address => bool) public banned;
// Events (server listens to these via WebSocket RPC)
event ServerAccessGranted(address indexed user, address indexed grantedBy);
event ServerAccessRevoked(address indexed user, address indexed revokedBy);
event GroupAccessGranted(address indexed user, bytes32 indexed groupId, address indexed grantedBy);
event GroupAccessRevoked(address indexed user, bytes32 indexed groupId, address indexed revokedBy);
event UserBanned(address indexed user, address indexed bannedBy, string reason);
event UserUnbanned(address indexed user, address indexed unbannedBy);
constructor(address admin) {
_grantRole(DEFAULT_ADMIN_ROLE, admin);
_grantRole(ADMIN_ROLE, admin);
openServer = false;
}
// --- Server Access ---
function grantServerAccess(address user) external onlyRole(ADMIN_ROLE) {
serverAccess[user] = true;
emit ServerAccessGranted(user, msg.sender);
}
function revokeServerAccess(address user) external onlyRole(ADMIN_ROLE) {
serverAccess[user] = false;
emit ServerAccessRevoked(user, msg.sender);
}
function grantServerAccessBatch(address[] calldata users) external onlyRole(ADMIN_ROLE) {
for (uint i = 0; i < users.length; i++) {
serverAccess[users[i]] = true;
emit ServerAccessGranted(users[i], msg.sender);
}
}
function canJoinServer(address user) external view returns (bool) {
if (banned[user]) return false;
if (openServer) return true;
return serverAccess[user];
}
// --- Group Access ---
function grantGroupAccess(address user, bytes32 groupId) external onlyRole(ADMIN_ROLE) {
groupAccess[user][groupId] = true;
emit GroupAccessGranted(user, groupId, msg.sender);
}
function revokeGroupAccess(address user, bytes32 groupId) external onlyRole(ADMIN_ROLE) {
groupAccess[user][groupId] = false;
emit GroupAccessRevoked(user, groupId, msg.sender);
}
function canJoinGroup(address user, bytes32 groupId) external view returns (bool) {
if (banned[user]) return false;
return groupAccess[user][groupId];
}
// --- Feature Access ---
function grantFeature(address user, bytes32 featureId) external onlyRole(ADMIN_ROLE) {
featureAccess[user][featureId] = true;
}
function canUseFeature(address user, bytes32 featureId) external view returns (bool) {
return featureAccess[user][featureId];
}
// --- Banning ---
function ban(address user, string calldata reason) external onlyRole(MODERATOR_ROLE) {
banned[user] = true;
emit UserBanned(user, msg.sender, reason);
}
function unban(address user) external onlyRole(ADMIN_ROLE) {
banned[user] = false;
emit UserUnbanned(user, msg.sender);
}
// --- Admin ---
function setOpenServer(bool _open) external onlyRole(ADMIN_ROLE) {
openServer = _open;
}
function _authorizeUpgrade(address) internal override onlyRole(DEFAULT_ADMIN_ROLE) {}
}
Design Decisions:
- UUPSUpgradeable: The contract can be upgraded without redeploying (critical for fixing bugs without losing state). The upgrade is gated by
DEFAULT_ADMIN_ROLE. - Open server mode:
openServer = truemakes the contract only enforce bans, not allowlists. This supports both "private server" and "public server with ban list" models. - Batch operations:
grantServerAccessBatch()reduces gas costs when onboarding multiple users. - Event-driven: All state changes emit events. The featherChat server subscribes to these via WebSocket RPC (see section 4).
- bytes32 groupId: Group names are hashed to
keccak256(group_name)for fixed-size on-chain storage. The server performs the mapping.
3.5 Server-Side Contract Integration (Rust)
The featherChat server needs an Ethereum RPC client to call the smart contract. The recommended crate is alloy (the successor to ethers-rs, maintained by the same team).
New module: warzone-server/src/chain.rs:
use alloy::providers::{Provider, ProviderBuilder};
use alloy::contract::ContractInstance;
pub struct ChainVerifier {
contract: ContractInstance,
cache: DashMap<(EthAddress, Permission), (bool, Instant)>,
cache_ttl: Duration,
}
#[derive(Hash, Eq, PartialEq)]
pub enum Permission {
JoinServer,
JoinGroup(String),
UseFeature(String),
}
impl ChainVerifier {
pub async fn can_join_server(&self, addr: &EthAddress) -> bool {
// Check cache first
if let Some(entry) = self.cache.get(&(*addr, Permission::JoinServer)) {
if entry.1.elapsed() < self.cache_ttl {
return entry.0;
}
}
// Call contract
let result = self.contract
.function("canJoinServer", &[addr.into()])
.call()
.await
.unwrap_or(false);
// Update cache
self.cache.insert((*addr, Permission::JoinServer), (result, Instant::now()));
result
}
}
Integration with existing routes:
The ChainVerifier would be added to AppState (defined in warzone-server/src/state.rs). Existing route handlers gain a permission check before their current logic:
// In routes/keys.rs — register endpoint
async fn register_keys(
State(state): State<AppState>,
Json(req): Json<RegisterRequest>,
) -> AppResult<Json<Value>> {
// NEW: On-chain access check
if let Some(chain) = &state.chain_verifier {
let eth_addr = derive_eth_address_from_bundle(&req.bundle)?;
if !chain.can_join_server(ð_addr).await {
return Err(AppError::Forbidden("not authorized on-chain"));
}
}
// ... existing registration logic ...
}
The Option<ChainVerifier> pattern ensures backward compatibility — servers without blockchain configuration operate exactly as before. A new environment variable WARZONE_ACL_CONTRACT and WARZONE_ETH_RPC enables the feature.
Linking Ed25519 Fingerprint to Ethereum Address:
The server needs to verify that the registering user's Ethereum address corresponds to their Ed25519 fingerprint (both derived from the same seed). Two approaches:
-
Registration-time proof: The user signs their Ed25519 fingerprint with their secp256k1 key and includes the signature + secp256k1 public key in the registration request. The server verifies with
eth_verify()fromethereum.rs:115-121, derives the Ethereum address from the secp256k1 public key, and checks the contract. -
Trust the derivation: Since both keys derive from the same seed via deterministic HKDF, the server could accept the user-provided Ethereum address and verify it against the contract. However, this trusts the client to provide the correct address. Approach 1 is more secure.
3.6 NFT-Gated Access
Instead of (or in addition to) the admin-managed allowlist, membership can be represented as NFTs:
ERC-721 (Unique Membership):
contract FeatherChatMembership is ERC721 {
function mint(address to, uint256 tokenId) external onlyRole(MINTER_ROLE);
// The ACL contract checks ownership
// FeatherChatACL.canJoinServer() → checks if user holds any token from this contract
}
- Each token represents one server membership
- Transferable: a user can sell or gift their membership
- Revocable: the contract owner can burn tokens (with appropriate governance)
- Metadata: token URI can encode the server URL, join date, role
ERC-1155 (Role-Based):
// Token ID 1 = "member" role
// Token ID 2 = "moderator" role
// Token ID 3 = "admin" role
// Token ID 100 = access to "ops" group
// Token ID 101 = access to "dev" group
contract FeatherChatRoles is ERC1155 {
function mint(address to, uint256 roleId, uint256 amount) external onlyRole(MINTER_ROLE);
}
- More flexible than ERC-721: multiple role types in a single contract
- Semi-fungible: role tokens are interchangeable within the same type
- Server checks:
balanceOf(user, roleId) > 0
Benefits of NFT-Gated Access:
- Membership is a tradeable asset (useful for paid communities)
- On-chain provenance: complete history of who held access
- Composable with DeFi: stake membership NFT as collateral, fractionalize ownership
- Visual: NFTs can have artwork, shown in wallets and on OpenSea
Risks:
- NFT theft → unauthorized access to the server
- Gas costs for minting (mitigated by L2 — ~$0.01 on Arbitrum)
- Complexity: users must understand NFTs and have a wallet
3.7 Token-Gated Access
ERC-20 token holdings can gate access to features or premium tiers:
Hold >= 100 WZT → access to premium features (voice calls, large file transfer)
Hold >= 1000 WZT → access to "premium" group
Stake 500 WZT for 30 days → moderator privileges
The server checks ERC20.balanceOf(user) via the contract. This integrates naturally with DeFi mechanics:
- Token buyback from server revenue → deflationary pressure
- Liquidity mining → earn WZT by being active on the server
- Governance → WZT holders vote on server policy via snapshot/on-chain DAO
Implementation:
contract FeatherChatACL {
IERC20 public token;
uint256 public premiumThreshold;
function canUsePremium(address user) external view returns (bool) {
return token.balanceOf(user) >= premiumThreshold;
}
}
4. Hybrid Architecture
4.1 Combined IdP + Smart Contract Verifier
The full architecture combines both capabilities:
┌─────────────────────────────┐
│ Ethereum L2 (Arbitrum) │
│ │
│ FeatherChatACL Contract │
│ - serverAccess mapping │
│ - groupAccess mapping │
│ - featureAccess mapping │
│ - banned mapping │
└──────────┬──────────────────┘
│ JSON-RPC / WSS
│
┌──────────────┐ OIDC ┌───────────────────┴───────────────────┐
│ External │←─────────→│ featherChat Server │
│ Apps │ │ │
│ (Grafana, │ │ ┌──────────┐ ┌──────────────────┐ │
│ Gitea, │ │ │ OIDC │ │ ChainVerifier │ │
│ Authentik) │ │ │ Provider │ │ (alloy + cache) │ │
│ │ │ └────┬─────┘ └────────┬─────────┘ │
│ │ │ │ │ │
│ │ │ ┌────┴──────────────────┴──────────┐ │
│ │ │ │ Auth + Permission Layer │ │
│ │ │ │ challenge-response (auth.rs) │ │
│ │ │ │ + on-chain permission check │ │
│ │ │ └────────────────┬─────────────────┘ │
│ │ │ │ │
│ │ │ ┌────────────────┴─────────────────┐ │
│ │ │ │ Existing Infrastructure │ │
│ │ │ │ sled DB · WebSocket · Groups │ │
│ │ │ │ Aliases · Messages · Keys │ │
│ │ │ └──────────────────────────────────┘ │
└──────────────┘ └───────────────────────────────────────┘
▲
│ HTTP / WebSocket
│
┌──────────┴──────────────────┐
│ featherChat Clients │
│ CLI · TUI · Web (WASM) │
└─────────────────────────────┘
4.2 OIDC Tokens with On-Chain Permissions
The key insight: the OIDC token carries on-chain permissions as claims. When the server issues a JWT, it reads the user's permissions from the smart contract (or its local cache) and embeds them:
{
"sub": "a3f8c912e7b04d6f1234567890abcdef",
"fingerprint": "a3f8:c912:e7b0:4d6f:1234:5678:90ab:cdef",
"alias": "alice",
"eth_address": "0xABC...",
"groups": ["ops", "dev"],
"on_chain": {
"server_access": true,
"group_access": ["ops", "dev"],
"features": ["voice", "large_files"],
"nft_memberships": ["FeatherChatMembership#42"],
"token_balance": "1500",
"roles": ["member", "moderator"]
},
"permissions_block": 18234567,
"permissions_chain": "arbitrum-one"
}
External apps reading this token can make authorization decisions based on on-chain state without needing their own blockchain connection. The permissions_block field indicates the block height at which the permissions were last verified, enabling staleness detection.
4.3 Event-Driven Sync
The server maintains a local cache of on-chain permissions in the sled database (a new permissions tree). This cache is kept fresh via two mechanisms:
1. WebSocket RPC Subscription (Real-Time):
// Subscribe to contract events via WebSocket RPC
let filter = contract.event::<ServerAccessGranted>().watch().await?;
filter.for_each(|event| {
// Update local sled cache immediately
db.permissions.insert(
format!("server:{}", event.user),
serde_json::to_vec(&PermissionCache {
granted: true,
updated_at: Utc::now(),
block: event.block_number,
}).unwrap(),
);
});
2. Periodic Poll (Fallback):
If the WebSocket connection drops, a background task polls every 60 seconds:
loop {
let latest_block = provider.get_block_number().await?;
let events = contract
.event::<ServerAccessGranted>()
.from_block(last_synced_block)
.to_block(latest_block)
.query()
.await?;
for event in events {
update_cache(&db, event);
}
last_synced_block = latest_block;
tokio::time::sleep(Duration::from_secs(60)).await;
}
Cache Semantics:
| Scenario | Behavior |
|---|---|
| Cache hit, fresh (< TTL) | Use cached value, no RPC call |
| Cache hit, stale (> TTL) | Use cached value, schedule background refresh |
| Cache miss | Synchronous RPC call, block until response |
| RPC unavailable | Fall back to cached value (even if stale), log warning |
| First startup | Full sync from contract deployment block |
Default cache TTL: 5 minutes. Configurable via WARZONE_CHAIN_CACHE_TTL_SECS.
5. Feasibility Analysis
5.1 What featherChat Already Has
| Capability | Status | Location |
|---|---|---|
| Ed25519 identity keypair | Implemented | identity.rs:29-48 — Seed::derive_identity() |
| X25519 encryption keypair | Implemented | identity.rs:37-42 — HKDF with "warzone-x25519" |
| secp256k1 keypair | Implemented | ethereum.rs:80-106 — derive_eth_identity() |
| Ethereum address derivation | Implemented | ethereum.rs:89-105 — Keccak-256 of uncompressed pubkey |
| EIP-55 checksum addresses | Implemented | ethereum.rs:58-75 — EthAddress::to_checksum() |
| secp256k1 signing/verification | Implemented | ethereum.rs:109-121 — eth_sign(), eth_verify() |
| Challenge-response auth | Implemented | auth.rs:62-172 — Ed25519 signature verification |
| Bearer token system | Implemented | auth.rs:157-165 — sled tokens tree, 7-day TTL |
| Token validation | Implemented | auth.rs:177-186 — validate_token() |
| Group management | Implemented | groups.rs — create, join, leave, kick, members, list |
| Alias system | Implemented | aliases.rs — register, resolve, recover, renew, admin remove |
| WebSocket infrastructure | Implemented | routes/ws.rs — real-time message push |
| axum HTTP framework | Implemented | All route modules use axum 0.7 |
| sled embedded database | Implemented | 5 trees: keys, messages, groups, aliases, tokens |
| BIP39 mnemonic | Implemented | mnemonic.rs — encode/decode 24-word mnemonic |
| Fingerprint system | Implemented | types.rs — SHA-256(Ed25519_pubkey)[:16], xxxx:xxxx format |
Zeroize memory safety |
Implemented | identity.rs:12-13 — Seed derives Zeroize + ZeroizeOnDrop |
| Bincode wire format | Implemented | message.rs — WireMessage enum, serde derive |
5.2 What Needs to Be Built
Phase A: OIDC Provider
| Component | Complexity | Notes |
|---|---|---|
routes/oidc.rs — 5 endpoints |
Medium | Authorize, token, userinfo, jwks, discovery |
| JWT signing (Ed25519 or ES256) | Low | jsonwebtoken crate, server key management |
| Authorization code flow | Medium | Code generation, storage in sled, exchange |
| OIDC client registry | Low | New sled tree oidc_clients, JSON records |
| Consent/login web page | Medium | HTML/JS served from routes/web.rs pattern |
| Group membership claim population | Low | Scan groups tree, same as list_groups() |
| Ethereum address in claims | Low | Call derive_eth_identity() or require at registration |
Phase B: Smart Contract ACL
| Component | Complexity | Notes |
|---|---|---|
FeatherChatACL.sol |
Medium | Solidity contract with OpenZeppelin base |
| Deployment scripts (Hardhat/Foundry) | Low | Standard L2 deployment |
chain.rs — ChainVerifier |
Medium | alloy crate, RPC calls, cache layer |
| Permission middleware | Low | Check before existing route handlers |
| Event subscription | Medium | WebSocket RPC subscription to contract events |
| sled permission cache | Low | New permissions tree |
| Fingerprint-to-address linking | Low | secp256k1 signature proof at registration |
| Admin CLI for contract management | Medium | cast commands or custom CLI subcommand |
Phase C: NFT/Token Gating
| Component | Complexity | Notes |
|---|---|---|
FeatherChatMembership.sol (ERC-721) |
Low | Standard OpenZeppelin ERC-721 |
FeatherChatRoles.sol (ERC-1155) |
Low | Standard OpenZeppelin ERC-1155 |
| ERC-20 balance check | Low | Single balanceOf() call |
| NFT ownership check in ChainVerifier | Low | ownerOf() or balanceOf() call |
| Token staking contract (optional) | High | Time-locked staking with slash conditions |
5.3 Effort Estimates
| Phase | Duration | Dependencies |
|---|---|---|
| Phase A: OIDC Provider | 1-2 weeks | jsonwebtoken crate |
| Phase B: Smart Contract ACL | 2-3 weeks | alloy crate, L2 RPC endpoint, contract deployment |
| Phase C: NFT/Token Gating | 1-2 weeks | Phase B complete |
| Phase D: DID/Verifiable Credentials | 3-4 weeks | Phase A + B complete |
| Total (A-D): | 7-11 weeks |
5.4 Risks and Mitigations
| Risk | Impact | Likelihood | Mitigation |
|---|---|---|---|
| Gas costs for on-chain operations | Medium | High (on L1) | Deploy on L2 (Arbitrum: ~$0.01/tx, Base: ~$0.005/tx) |
| RPC endpoint reliability | Medium | Medium | Local cache in sled with graceful degradation; multiple RPC fallbacks |
| Smart contract bugs | Critical | Low | Formal verification, OpenZeppelin base contracts, upgradeable proxy (UUPS), phased rollout |
| User complexity (wallets, gas) | High | High | Admin handles all on-chain operations; users only interact with featherChat as usual |
| OIDC implementation compliance | Medium | Medium | Test against Authentik, Keycloak, and Grafana; use OIDC conformance test suite |
| Ed25519 JWT algorithm support | Low | Medium | Also support ES256 for broader compatibility |
| Key management for server signing | High | Low | Generate server key on first startup, back up to encrypted file; document key rotation |
| Contract upgrade governance | Medium | Medium | Multisig (Gnosis Safe) for admin role; timelock on upgrades |
| On-chain privacy (addresses visible) | Medium | High | Addressed in Section 7 (zero-knowledge proofs) |
6. Implementation Roadmap
Phase A: featherChat as OIDC Provider (No Blockchain)
Goal: Any OIDC-capable service can use featherChat for authentication.
Tasks:
- Generate server signing keypair on first startup (store in sled or file)
- Implement
routes/oidc.rs:GET /.well-known/openid-configuration— static discovery documentGET /auth/oidc/jwks— server's public key in JWK formatGET /auth/oidc/authorize— redirect to login page, initiate challenge-responsePOST /auth/oidc/token— exchange authorization code for JWTGET /auth/oidc/userinfo— return user claims from JWT
- Build a minimal login web page (served from
routes/web.rspattern):- "Enter your fingerprint" or "Sign in with passphrase"
- Challenge-response flow in the browser (reuse WASM
WasmIdentity) - Redirect back to the requesting application with authorization code
- Implement OIDC client registry (sled tree
oidc_clients):client_id,client_secret,redirect_uris,name- Admin CLI command to register clients
- Populate JWT claims:
sub= fingerprint hexalias= resolve from aliases sled tree (fp:<fingerprint>→ alias)eth_address= derive from seed or require at registration timegroups= scan groups sled tree for membership
- Test integration with:
- Authentik (as upstream OIDC source)
- Grafana (direct OIDC client)
- Gitea (direct OIDC client)
Deliverables:
warzone-server/src/routes/oidc.rs- Server signing key management
- Login web page
- Documentation for configuring external apps
Phase B: Smart Contract ACL
Goal: On-chain permission checks for server registration and group membership.
Tasks:
- Write and test
FeatherChatACL.sol(Foundry/Hardhat) - Deploy to Arbitrum Sepolia (testnet), then Arbitrum One (mainnet)
- Implement
warzone-server/src/chain.rs:ChainVerifierstruct with alloy providercan_join_server(),can_join_group()methods- sled-backed permission cache with TTL
- WebSocket RPC event subscription for real-time updates
- Add
ChainVerifiertoAppState(optional, behind feature flag) - Add permission checks to:
routes/keys.rs— registrationroutes/groups.rs— group join
- Implement fingerprint-to-address proof:
- New registration field:
eth_signature(secp256k1 signature of fingerprint) - Server verifies using
eth_verify()fromethereum.rs:115-121 - Store mapping in sled:
eth:<address>→ fingerprint
- New registration field:
- Admin tools:
- CLI commands:
warzone-server acl grant <eth_address>,warzone-server acl revoke <eth_address> - These shell out to
cast send(Foundry) or use alloy directly
- CLI commands:
- Configuration:
WARZONE_ACL_CONTRACT=0x...— contract addressWARZONE_ETH_RPC=https://arb1.arbitrum.io/rpc— RPC endpointWARZONE_ETH_RPC_WS=wss://arb1.arbitrum.io/ws— WebSocket RPCWARZONE_CHAIN_CACHE_TTL_SECS=300— cache TTL
Deliverables:
contracts/FeatherChatACL.sol+ deployment scriptswarzone-server/src/chain.rs- Modified routes with optional on-chain checks
- Admin CLI for contract interaction
Phase C: NFT/Token Gating
Goal: NFT ownership and token balances as access criteria.
Tasks:
- Deploy
FeatherChatMembership.sol(ERC-721) andFeatherChatRoles.sol(ERC-1155) - Extend
ChainVerifier:has_membership_nft(address)— checks ERC-721balanceOf()has_role(address, role_id)— checks ERC-1155balanceOf()token_balance(address)— checks ERC-20balanceOf()
- Add token-gated feature checks:
- Premium features (voice, large files) gated by token balance
- Specific groups gated by NFT ownership
- Implement DAO governance (optional):
- Snapshot-compatible: read from contract for off-chain voting
- On-chain voting: Governor contract for policy changes
Deliverables:
- NFT/token contracts
- Extended
ChainVerifier - Token-gated features in server routes
Phase D: Full Decentralized Identity
Goal: featherChat identities are W3C DIDs, interoperable with the broader decentralized identity ecosystem.
Tasks:
- DID Method: Define
did:featherchat:<fingerprint>method- DID Document contains Ed25519 verification key, X25519 key agreement key, secp256k1 key, and service endpoints
- Resolution: query featherChat server or read from smart contract
- Verifiable Credentials:
- Server issues VCs: "this fingerprint is alias X", "this fingerprint is a member of group Y"
- VCs signed by server's Ed25519 key
- Verifiable Presentations for third-party consumption
- DID Registry Contract:
- Store DID Documents on-chain (replaces DNS key transparency)
- Users register/update their DID Document via transaction
- Anyone can resolve a DID without trusting a server
- Cross-Chain Identity:
- Same seed → same identity on Ethereum, Polygon, Solana (different derivation paths)
- Cross-chain DID resolution
Deliverables:
- DID method specification
- VC issuance and verification
- On-chain DID registry contract
- Cross-chain identity bridging
7. Security Considerations
7.1 Smart Contract as Single Point of Truth
If the smart contract is compromised (admin key stolen, bug exploited), the attacker controls all permissions. Mitigations:
- Multisig admin: Use Gnosis Safe with 3-of-5 threshold for the admin role. No single key can modify permissions.
- Timelock: All permission changes go through a 24-hour timelock. Users can detect and react to malicious changes.
- Upgradeable proxy with governance: Contract upgrades require multisig + timelock + community review.
- Server fallback: If the contract is compromised, the server can switch to local-only mode (
WARZONE_ACL_CONTRACT=unset) and operate without blockchain.
7.2 L2 vs L1 Security Trade-offs
| Property | L1 (Ethereum) | L2 (Arbitrum/Base) |
|---|---|---|
| Finality | ~12 minutes (after The Merge) | Seconds (optimistic, 7-day challenge) |
| Cost | $5-50 per transaction | $0.005-0.05 per transaction |
| Security | Full Ethereum security | Inherits L1 security with additional trust assumptions |
| Sequencer risk | N/A | Sequencer downtime = delayed transactions (not lost) |
| Data availability | On-chain | On-chain (via L1 calldata/blobs) |
Recommendation: deploy on Arbitrum One for production. The security trade-off (optimistic rollup, 7-day challenge period) is acceptable for an ACL contract — permission changes are not time-critical to the same degree as financial transactions. If the sequencer goes down, the server falls back to its local cache.
7.3 Admin Key Management
The contract admin key is the most sensitive asset in this system. If compromised, the attacker can:
- Grant/revoke server access for anyone
- Ban any user
- Upgrade the contract to a malicious implementation
Recommended key management:
- Gnosis Safe multisig with 3-of-5 signers
- Each signer uses a hardware wallet (Ledger/Trezor)
- Signers are geographically distributed
- One signer key stored offline (cold storage) for emergency recovery
7.4 On-Chain Privacy
All Ethereum addresses and their permission states are publicly visible on-chain. This means:
- Anyone can see who has server access
- Anyone can see group memberships
- Transaction history reveals when permissions were granted/revoked
Mitigations:
- L2 with private transactions (future): Aztec, zkSync, or other ZK-rollups with private state
- Zero-knowledge proofs: Instead of storing
serverAccess[address] = true, store a Merkle root of authorized addresses. Users prove membership via ZK proof (Semaphore/Bandada protocol):
Server publishes: Merkle root of all authorized addresses
User proves: "I know a private key whose address is in this Merkle tree"
without revealing WHICH address
-
Off-chain permission storage with on-chain anchoring: Store permissions in an encrypted off-chain database. On-chain: only a hash of the permission set. The server decrypts locally. This sacrifices transparency for privacy.
-
Separate identity addresses: Users derive a fresh address specifically for ACL purposes (different HKDF info string), unlinkable to their public Ethereum address. The mapping is known only to the featherChat server.
7.5 Token/NFT Theft
If a user's membership NFT or access tokens are stolen (wallet compromise, phishing), the thief gains unauthorized server access. Mitigations:
- Time-locked revocation: Admin can revoke access within hours; the attacker has a limited window
- Soulbound tokens (ERC-5192): Non-transferable NFTs that cannot be sold or stolen (only the issuer can burn/reissue)
- Multi-factor: Require both NFT ownership AND featherChat challenge-response. Stealing only the NFT is insufficient without the BIP39 seed.
- Monitoring: The server monitors on-chain transfers. If a membership NFT is transferred, flag the account for review.
7.6 OIDC Token Security
- Token lifetime: Short-lived access tokens (15 minutes) with longer refresh tokens (7 days, matching existing
TOKEN_TTL_SECSinauth.rs:27) - Token storage: External apps store tokens according to their own security model. featherChat cannot control this.
- Token revocation: Server maintains a revocation list in sled. On user request or admin action, tokens are invalidated.
- Signing key rotation: Server signing key should be rotated periodically (every 90 days). Old keys remain in JWKS for validation of existing tokens until they expire.
8. Comparison with Existing Solutions
8.1 SpruceID / Sign-In with Ethereum (SIWE)
| Aspect | featherChat IdP | SpruceID/SIWE |
|---|---|---|
| Identity basis | BIP39 seed → Ed25519 + secp256k1 | Ethereum wallet (secp256k1 only) |
| Authentication | Ed25519 challenge-response | EIP-4361 message signing |
| Messaging | Built-in E2E encrypted messaging | No messaging |
| OIDC support | Planned (this document) | Via oidc-proxy |
| Group management | Built-in (groups.rs) | External (on-chain or off-chain) |
| Privacy | Semi-trusted server, E2E encryption | Public Ethereum address |
featherChat advantage: SIWE only proves wallet ownership. featherChat proves identity AND provides encrypted messaging AND group management in one system.
8.2 Ceramic Network
| Aspect | featherChat | Ceramic |
|---|---|---|
| Identity | Seed-derived (deterministic) | DID-based (did:pkh, did:key) |
| Data storage | sled (server) + sled (client) | IPFS/Ceramic streams |
| Encryption | E2E (X3DH + Double Ratchet) | Application-level (Lit Protocol) |
| Decentralization | Server-based (federated future) | Fully decentralized |
| Complexity | Single binary | Complex infrastructure (IPFS + Ceramic + ComposeDB) |
| Performance | Milliseconds (local sled) | Seconds (IPFS propagation) |
featherChat advantage: Simplicity and performance. Ceramic provides a more decentralized data layer but at significant complexity cost. featherChat's single-binary deployment is a major operational advantage.
8.3 Lens Protocol
| Aspect | featherChat | Lens Protocol |
|---|---|---|
| Identity | Fingerprint (Ed25519) | Lens Profile NFT |
| Social graph | Group membership (sled) | On-chain follow graph |
| Messaging | E2E encrypted, forward-secret | Via XMTP (separate protocol) |
| Content | Ephemeral (no persistence by default) | Permanent (on-chain/Arweave) |
| Cost | Free (self-hosted) | Gas costs for social actions |
| Privacy | E2E encrypted content | Public content (social media model) |
featherChat advantage: Privacy-first design. Lens is a social media protocol where content is public by default. featherChat is a messaging protocol where content is encrypted by default.
8.4 XMTP
| Aspect | featherChat | XMTP |
|---|---|---|
| Identity | BIP39 seed → Ed25519 + secp256k1 | Ethereum wallet → XMTP keys |
| Encryption | X3DH + Double Ratchet | MLS (Message Layer Security) |
| Key exchange | X3DH (3-4 DH operations) | MLS TreeKEM |
| Groups | Sender Keys (O(1) encrypt) | MLS (O(log n) encrypt) |
| Transport | HTTP + WebSocket (self-hosted) | XMTP network (decentralized) |
| On-chain ACL | Planned (this document) | Not built-in |
| IdP capability | Planned (this document) | Not designed for this |
featherChat advantage: featherChat combines messaging, identity provider, and access control in one system. XMTP focuses solely on messaging and delegates identity to Ethereum wallets and ACL to external systems.
8.5 What featherChat Does Differently
No existing solution combines all three:
- E2E encrypted messaging with forward secrecy (X3DH + Double Ratchet)
- Identity Provider (OIDC/OAuth2) for external service authentication
- On-chain access control (smart contract ACL, NFT gating, token gating)
featherChat is the only system where a single BIP39 mnemonic gives you:
- A messaging identity (Ed25519 fingerprint)
- An encryption identity (X25519)
- An Ethereum identity (secp256k1 address)
- SSO access to your organization's infrastructure (OIDC)
- Verifiable, auditable, censorship-resistant permissions (smart contract)
The trade-off is that featherChat is not fully decentralized (server-based, though federation is planned). This is an intentional design choice: the server model enables instant message delivery, simple deployment, and the IdP role requires a stable endpoint. The smart contract layer adds decentralized trust for the permission model without requiring the messaging infrastructure itself to be decentralized.
Appendix A: Relevant Source File Reference
| File | Key Types/Functions |
|---|---|
crates/warzone-protocol/src/identity.rs |
Seed, IdentityKeyPair, PublicIdentity, Fingerprint, derive_identity(), compute_fingerprint() |
crates/warzone-protocol/src/ethereum.rs |
EthIdentity, EthAddress, derive_eth_identity(), eth_sign(), eth_verify(), to_checksum() |
crates/warzone-protocol/src/crypto.rs |
hkdf_derive() — used by both identity.rs and ethereum.rs |
crates/warzone-protocol/src/message.rs |
WireMessage enum, MessageContent, ReceiptType |
crates/warzone-protocol/src/prekey.rs |
PreKeyBundle — deserialized in auth.rs for Ed25519 key extraction |
crates/warzone-server/src/routes/auth.rs |
create_challenge(), verify_challenge(), validate_token(), CHALLENGES static |
crates/warzone-server/src/routes/aliases.rs |
AliasRecord, register_alias(), resolve_alias(), reverse_lookup() |
crates/warzone-server/src/routes/groups.rs |
GroupInfo, create_group(), join_group(), kick_member(), get_members() |
crates/warzone-server/src/state.rs |
AppState — would be extended with Option<ChainVerifier> |
crates/warzone-server/src/db.rs |
Database struct with 5 sled trees — would add permissions and oidc_clients trees |
Appendix B: Environment Variables (New)
| Variable | Default | Purpose |
|---|---|---|
WARZONE_ACL_CONTRACT |
(none) | Ethereum address of FeatherChatACL contract. If unset, no on-chain checks. |
WARZONE_ETH_RPC |
(none) | HTTP JSON-RPC endpoint for contract reads |
WARZONE_ETH_RPC_WS |
(none) | WebSocket JSON-RPC endpoint for event subscriptions |
WARZONE_CHAIN_CACHE_TTL_SECS |
300 |
Permission cache TTL in seconds |
WARZONE_OIDC_ISSUER |
(auto-detected) | OIDC issuer URL (defaults to server's public URL) |
WARZONE_OIDC_SIGNING_ALG |
EdDSA |
JWT signing algorithm (EdDSA or ES256) |
WARZONE_ACL_MODE |
allowlist |
allowlist (default deny) or banlist (default allow) |