Files
featherChat/warzone/docs/IDP_SMART_CONTRACT.md
Siavash Sameni de1ce77fea IDP_SMART_CONTRACT.md: featherChat as IdP + on-chain ACL (1111 lines)
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>
2026-03-28 08:07:34 +04:00

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 is SHA-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 is Keccak-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:

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

  2. 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 27
  • CHALLENGE_TTL_SECS = 60 — line 29
  • Challenges stored in-memory via LazyLock<Mutex<HashMap>> — line 54
  • Token validation via validate_token() reads from the sled tokens tree — 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:

  1. Server Ed25519 key — generate a server-level Seed at first startup, derive an Ed25519 keypair, use it to sign JWTs. The JWK published at /auth/oidc/jwks exposes the Ed25519 public key in JWK format (kty: OKP, crv: Ed25519). This aligns with the existing cryptographic infrastructure.

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

  • jsonwebtoken crate for JWT creation/validation
  • serde_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.rs with 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:

  1. In Authentik: Create a new "OAuth2/OIDC Source" pointing to featherChat's discovery URL
  2. featherChat server: Register Authentik as an OIDC client (client_id, client_secret, redirect_uri stored in a new oidc_clients sled tree)
  3. Group Sync: featherChat's group memberships (from groups.rs) are exposed in the OIDC token's groups claim. 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.rs pattern)

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.rsPOST /v1/keys/register has no auth check
Join group Anyone can join any group by name groups.rs:101-129join_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-268group.creator != fp check
Admin alias removal Password-protected aliases.rs:383-399WARZONE_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 = true makes 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(&eth_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:

  1. 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() from ethereum.rs:115-121, derives the Ethereum address from the secp256k1 public key, and checks the contract.

  2. 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-48Seed::derive_identity()
X25519 encryption keypair Implemented identity.rs:37-42 — HKDF with "warzone-x25519"
secp256k1 keypair Implemented ethereum.rs:80-106derive_eth_identity()
Ethereum address derivation Implemented ethereum.rs:89-105 — Keccak-256 of uncompressed pubkey
EIP-55 checksum addresses Implemented ethereum.rs:58-75EthAddress::to_checksum()
secp256k1 signing/verification Implemented ethereum.rs:109-121eth_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-186validate_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-13Seed derives Zeroize + ZeroizeOnDrop
Bincode wire format Implemented message.rsWireMessage 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:

  1. Generate server signing keypair on first startup (store in sled or file)
  2. Implement routes/oidc.rs:
    • GET /.well-known/openid-configuration — static discovery document
    • GET /auth/oidc/jwks — server's public key in JWK format
    • GET /auth/oidc/authorize — redirect to login page, initiate challenge-response
    • POST /auth/oidc/token — exchange authorization code for JWT
    • GET /auth/oidc/userinfo — return user claims from JWT
  3. Build a minimal login web page (served from routes/web.rs pattern):
    • "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
  4. Implement OIDC client registry (sled tree oidc_clients):
    • client_id, client_secret, redirect_uris, name
    • Admin CLI command to register clients
  5. Populate JWT claims:
    • sub = fingerprint hex
    • alias = resolve from aliases sled tree (fp:<fingerprint> → alias)
    • eth_address = derive from seed or require at registration time
    • groups = scan groups sled tree for membership
  6. 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:

  1. Write and test FeatherChatACL.sol (Foundry/Hardhat)
  2. Deploy to Arbitrum Sepolia (testnet), then Arbitrum One (mainnet)
  3. Implement warzone-server/src/chain.rs:
    • ChainVerifier struct with alloy provider
    • can_join_server(), can_join_group() methods
    • sled-backed permission cache with TTL
    • WebSocket RPC event subscription for real-time updates
  4. Add ChainVerifier to AppState (optional, behind feature flag)
  5. Add permission checks to:
    • routes/keys.rs — registration
    • routes/groups.rs — group join
  6. Implement fingerprint-to-address proof:
    • New registration field: eth_signature (secp256k1 signature of fingerprint)
    • Server verifies using eth_verify() from ethereum.rs:115-121
    • Store mapping in sled: eth:<address> → fingerprint
  7. 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
  8. Configuration:
    • WARZONE_ACL_CONTRACT=0x... — contract address
    • WARZONE_ETH_RPC=https://arb1.arbitrum.io/rpc — RPC endpoint
    • WARZONE_ETH_RPC_WS=wss://arb1.arbitrum.io/ws — WebSocket RPC
    • WARZONE_CHAIN_CACHE_TTL_SECS=300 — cache TTL

Deliverables:

  • contracts/FeatherChatACL.sol + deployment scripts
  • warzone-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:

  1. Deploy FeatherChatMembership.sol (ERC-721) and FeatherChatRoles.sol (ERC-1155)
  2. Extend ChainVerifier:
    • has_membership_nft(address) — checks ERC-721 balanceOf()
    • has_role(address, role_id) — checks ERC-1155 balanceOf()
    • token_balance(address) — checks ERC-20 balanceOf()
  3. Add token-gated feature checks:
    • Premium features (voice, large files) gated by token balance
    • Specific groups gated by NFT ownership
  4. 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:

  1. 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
  2. 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
  3. 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
  4. 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:

  1. L2 with private transactions (future): Aztec, zkSync, or other ZK-rollups with private state
  2. 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
  1. 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.

  2. 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_SECS in auth.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:

  1. E2E encrypted messaging with forward secrecy (X3DH + Double Ratchet)
  2. Identity Provider (OIDC/OAuth2) for external service authentication
  3. 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)