08-fediverse-integration.md 32800 octets

Intégration de la fédération ActivityPub

Public visé : développeurs qui consomment le framework Rustwarden et souhaitent comprendre l'architecture interne de rustwarden-fediverse, ses points d'extension, ses flux d'authentification et son modèle de données.

Hors périmètre : déploiement opérationnel, supervision, variables d'environnement de production. Voir docs/fediverse-consumer-guide.md et docs/fediverse-operations.md.

Type Diátaxis : explication (comprendre pourquoi et comment le système est structuré).


1. Pourquoi la fédération ?

Rustwarden est, au premier chef, un framework d'authentification local. La crate rustwarden-fediverse y greffe le protocole ActivityPub pour trois cas d'usage distincts :

SSO décentralisé entre instances. Une instance Rustwarden B peut authentifier un utilisateur dont le compte existe sur une instance A, sans synchronisation de base de données ni provider OIDC central. Le protocole AP sert de canal de signal : l'IdP (instance A) répond à un challenge émis par le SP (instance B). Le résultat est un JWT local signé par B, avec identity_kind = remote.

Partage de ressources cross-instance. Un utilisateur distant (identifié par @alice@a.tld) peut être membre d'une équipe hébergée sur B. La table fediverse_resource_share modélise ce lien. Les clés RSA d'acteur chiffrent les payloads d'invite avant leur envoi sur le réseau.

Gestion d'équipes multi-domaines. Un admin sur B crée une invitation vers @bob@c.tld. L'activité AP Invite{TeamMembership} est envoyée à l'inbox de C. Quand Bob accepte, une ligne team_members est créée sur B avec remote_identity_id renseigné — sans que Bob ait jamais de mot de passe sur B.

Ces trois cas reposent sur un contrat unique : rustwarden-core est totalement ignorant de tout ce qui précède. Il ne dépend d'aucune crate fediverse. Le branchement se fait exclusivement via trois traits définis dans rustwarden-fediverse.


2. Architecture en quatre couches

flowchart TB
    subgraph UI ["rustwarden-fediverse-ui"]
        UI_NODE["Handlers HTML\nlogin · consent · admin · invite"]
    end

    subgraph FEDI ["rustwarden-fediverse"]
        FEDI_NODE["Protocole ActivityPub headless\nacteur · inbox · auth · signatures · outbound"]
    end

    subgraph CORE ["rustwarden-core"]
        CORE_NODE["Framework agnostique fédération\nauth JWT · RBAC · services · middleware"]
    end

    subgraph GUARD ["rustwarden-fediverse-guard"]
        GUARD_NODE["Détection d'abus\nsignature_failure · replay · ban automatique"]
    end

    UI_NODE -->|dépend de| FEDI_NODE
    FEDI_NODE -->|dépend de| CORE_NODE
    GUARD_NODE -->|se branche via FediverseEventObserver| FEDI_NODE

La règle fondamentale est que les dépendances ne remontent jamais : rustwarden-core n'importe rien de rustwarden-fediverse. Les crates compagnons s'y branchent via des traits exportés par rustwarden-fediverse.


3. Diagramme des composants

flowchart LR
    APP([Application consommatrice])

    APP -->|router_with_auth| ROUTER["routes::router_with_auth()"]
    ROUTER --> WEBFINGER["/.well-known/webfinger"]
    ROUTER --> ACTOR["/users/{username}"]
    ROUTER --> INBOX["/inbox  (POST)"]
    ROUTER --> AUTH_ROUTES["auth_routes\n/initiate · /status · /consume\n/auth/inbox · /auth/consent"]
    ROUTER --> INVITE_ROUTES["invite_routes\n/invite-fediverse · /invites · /accept · /reject"]

    INBOX --> SIGNAL["FediverseSignalService\nUpdate · Delete · Revoke"]
    AUTH_ROUTES --> AUTH_SVC["FediverseAuthService\nchallenge · status · consume"]
    INVITE_ROUTES --> INVITE_SVC["FediverseInviteService\nstate machine invite"]

    SIGNAL --> OBSERVER["FediverseEventObserver"]
    AUTH_SVC --> OBSERVER
    INVITE_SVC --> OBSERVER

    OBSERVER -->|implémenté par| GUARD["rustwarden-fediverse-guard"]
    OBSERVER -->|no-op par défaut| NOOP["NoopFediverseObserver"]

    INBOX --> VERIFIER["SignatureVerifier\nHttpSigVerifier"]
    AUTH_ROUTES --> VERIFIER

    AUTH_SVC -->|JWT HS256| CORE_JWT["rustwarden_core::auth::JwtService"]
    SIGNAL --> REMOTE_ID["remote_identities (DB)"]

4. Les trois traits d'extension

rustwarden-core et rustwarden-fediverse communiquent via trois traits. Ils sont tous définis dans rustwarden-fediverse pour que la direction de dépendance reste unidirectionnelle.

4.1 LocalUserResolver

Fichier : crates/rustwarden-fediverse/src/resolver.rs

Ce trait répond à la question : « qui est @alice sur cette instance ? ». Il est utilisé par les handlers WebFinger, Actor et NodeInfo pour exposer les profils locaux au réseau AP.

#[async_trait]
pub trait LocalUserResolver: Send + Sync {
    /// Cherche un utilisateur local par username.
    /// Retourne Ok(None) si inexistant ou soft-deleted.
    async fn find_local_user(&self, username: &str)
        -> Result<Option<UserSummary>, AppError>;

    /// Nombre total d'utilisateurs (NodeInfo usage.users.total).
    async fn count_users_total(&self) -> Result<u64, AppError>;

    /// Utilisateurs actifs depuis `days` jours (last_login_at).
    async fn count_users_active_since(&self, days: u32) -> Result<u64, AppError>;

    /// Posts locaux (toujours 0 jusqu'à un RFC futur introduisant les posts).
    async fn count_local_posts(&self) -> Result<u64, AppError>;
}

Implémentation fournie : SeaOrmLocalUserResolver — lit directement les tables SeaORM de rustwarden-core et génère les clés RSA d'acteur à la demande (RFC 0005) si CryptoService est disponible.

Implémentation custom : brancher un store de comptes existant sans SeaORM.

use async_trait::async_trait;
use rustwarden_fediverse::resolver::{LocalUserResolver, UserSummary};
use rustwarden_core::error::AppError;

struct MonStoreResolver { /* ... */ }

#[async_trait]
impl LocalUserResolver for MonStoreResolver {
    async fn find_local_user(&self, username: &str)
        -> Result<Option<UserSummary>, AppError>
    {
        // Requête vers votre propre base de données.
        todo!()
    }
    async fn count_users_total(&self) -> Result<u64, AppError> { Ok(0) }
    async fn count_users_active_since(&self, _: u32) -> Result<u64, AppError> { Ok(0) }
    async fn count_local_posts(&self) -> Result<u64, AppError> { Ok(0) }
}

4.2 SignatureVerifier

Fichier : crates/rustwarden-fediverse/src/services/fediverse_signal_service.rs

Ce trait délègue la vérification cryptographique des signatures HTTP entrantes. Il est appelé par inbox::handle et auth_routes::auth_inbox avant tout traitement des activités.

#[async_trait]
pub trait SignatureVerifier: Send + Sync {
    async fn verify(&self, ctx: &VerifyCtx<'_>) -> Result<(), AppError>;
}

pub struct VerifyCtx<'a> {
    pub actor_ap_id: &'a str,
    pub method: &'a str,        // "post"
    pub path: &'a str,          // "/inbox"
    pub headers: &'a HeaderMap,
    pub body: &'a [u8],
    pub public_key_pem: &'a str, // vide = auto-résolution via remote_identities
}

Implémentation fournie :

  • HttpSigVerifier — verifier de production (RFC 0016 + RFC 0017). Auto-résout la clé publique depuis remote_identities quand public_key_pem est vide. Valide la fenêtre temporelle (Date ±300 s) et le Digest SHA-256 du corps (constant-time).
  • AlwaysAcceptVerifier — accepte toute signature, dev/test uniquement.

Implémentation custom : brancher un cache de clés externe ou une PKI d'entreprise.

use async_trait::async_trait;
use rustwarden_fediverse::services::fediverse_signal_service::{SignatureVerifier, VerifyCtx};
use rustwarden_core::error::AppError;

struct MonVerifier;

#[async_trait]
impl SignatureVerifier for MonVerifier {
    async fn verify(&self, ctx: &VerifyCtx<'_>) -> Result<(), AppError> {
        // Appel à votre service de PKI interne.
        todo!()
    }
}

4.3 FediverseEventObserver

Fichier : crates/rustwarden-fediverse/src/observer.rs

Ce trait reçoit des notifications asynchrones aux points chauds du protocole. Toutes les méthodes ont une implémentation par défaut vide — une implémentation partielle est valide.

#[async_trait]
pub trait FediverseEventObserver: Send + Sync {
    /// Vérification cryptographique échouée.
    async fn on_signature_failure(
        &self, _domain: &str, _actor_uri: Option<&str>, _reason: &str,
    ) {}

    /// Replay détecté (Date périmée, nonce dupliqué, signature rejouée).
    async fn on_replay_attempt(
        &self, _domain: &str, _actor_uri: Option<&str>, _details: Value,
    ) {}

    /// Garde SSRF déclenchée sur une URL privée/loopback.
    async fn on_fetch_ssrf(&self, _target_host: &str, _attempted_ip: Option<&str>) {}

    /// Payload inbox structurellement invalide (corps trop grand, JSON corrompu, type inconnu).
    async fn on_inbox_abuse(
        &self, _domain: &str, _actor_uri: Option<&str>, _reason: &str,
    ) {}

    /// Un challenge d'authentification AP a été initié.
    async fn on_auth_challenge(&self, _domain: &str, _handle: &str) {}

    /// Une invite a été acceptée, rejetée ou révoquée.
    async fn on_invite_action(&self, _domain: &str, _decision: &str) {}

    /// Un Delete{Actor} a été traité : la ligne remote_identities est soft-deleted.
    /// Invoqué après la mutation de base ; idempotent (jamais déclenché si déjà disabled).
    async fn on_delete_actor(
        &self, _domain: &str, _ap_id: &str, _remote_identity_id: Uuid,
    ) {}
}

Implémentation fournie : NoopFediverseObserver — implémentation vide, câblée par défaut quand aucun observer n'est enregistré.

Règle contractuelle : les erreurs internes à un observer ne doivent jamais remonter sur le chemin de requête. L'observer est best-effort ; utiliser tracing::warn! plutôt que de propager.

Exemple : observer qui compte les signatures échouées.

use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use async_trait::async_trait;
use rustwarden_fediverse::observer::FediverseEventObserver;

#[derive(Default)]
struct SigFailCounter {
    count: AtomicU64,
}

#[async_trait]
impl FediverseEventObserver for SigFailCounter {
    async fn on_signature_failure(
        &self, domain: &str, _actor_uri: Option<&str>, reason: &str,
    ) {
        let n = self.count.fetch_add(1, Ordering::Relaxed) + 1;
        tracing::warn!(domain, reason, total = n, "signature failure");
    }
    // Toutes les autres méthodes héritent du no-op par défaut.
}

5. Authentification fédérée — le flux complet

sequenceDiagram
    participant Client as Client (navigateur b.tld)
    participant SP as SP — instance B
    participant IdP as IdP — instance A

    Client->>SP: POST /api/v1/auth/login/fediverse/initiate<br/>{handle: "@alice@a.tld", redirect_url?}
    SP->>SP: validate_redirect_url() — ANSSI F-NET-2
    SP->>SP: FediverseAuthService::initiate()<br/>→ challenge UUID + nonce 256-bit, TTL 5 min
    SP-->>Client: 201 {challenge_id, nonce, expires_at}

    Note over Client: Affiche page d'attente, commence à poller /status

    IdP->>SP: POST /fediverse/auth/inbox<br/>{type:"AuthRequest", actor, nonce, sig}
    SP->>SP: assert_domain_allowed() — trust policy
    SP->>SP: SignatureVerifier::verify(sig)
    alt signature invalide
        SP->>SP: observer.on_signature_failure()
        SP-->>IdP: 401 Unauthorized
    else signature valide
        SP->>SP: FediverseAuthService::record_inbound_request()<br/>→ stocke la requête (pending)
        SP-->>IdP: 200 OK
    end

    Note over IdP: L'utilisateur alice voit la demande de consentement

    IdP->>SP: POST /api/v1/fediverse/auth/consent<br/>{challenge_id, decision:"approve", remote_identity_id}
    SP->>SP: FediverseAuthService::record_decision()<br/>→ challenge.status = approved
    SP-->>IdP: 200 OK

    Client->>SP: GET /api/v1/auth/login/fediverse/status/{challenge_id}
    SP-->>Client: 200 {status: "approved"}

    Client->>SP: POST /api/v1/auth/login/fediverse/consume/{challenge_id}
    SP->>SP: FediverseAuthService::consume()<br/>→ vérifie status=approved, flip → consumed (one-shot)
    SP->>SP: lookup/create remote_identity
    SP->>SP: JwtService::mint() — identity_kind=remote, remote_id
    SP-->>Client: 200 {jwt, remote_identity_id, handle}

Les cinq handlers correspondants sont dans crates/rustwarden-fediverse/src/auth_routes.rs.

Machine à états du challenge :

ÉtatTransition suivante
pending→ approved (IdP consent) · → rejected · → expired (TTL dépassé)
approved→ consumed (consume one-shot)
rejectedterminal
expiredterminal
consumedterminal

6. Modèle de données fédération

Les huit tables fediverse sont toutes préfixées fediverse_ ou nommées remote_identity :

erDiagram
    remote_identity {
        uuid id PK
        string ap_id
        string username
        string domain
        string display_name
        string summary_html
        string inbox_url
        string shared_inbox_url
        string public_key_pem
        string icon_url
        string actor_type
        json raw_json
        timestamp last_fetched_at
        timestamp disabled_at
        timestamp created_at
        timestamp updated_at
    }

    fediverse_auth_challenge {
        uuid id PK
        string handle
        string nonce
        string status
        string redirect_url
        uuid remote_identity_id FK
        timestamp expires_at
        timestamp created_at
    }

    fediverse_pending_invite {
        uuid id PK
        uuid team_id FK
        string handle
        string role
        string status
        uuid invited_by_user_id FK
        string activity_id
        timestamp expires_at
        timestamp created_at
    }

    fediverse_outbound_queue {
        uuid id PK
        string activity_id
        string payload_json
        string target_inbox
        string target_domain
        string status
        int attempts
        int max_attempts
        string last_error
        timestamp next_retry_at
        timestamp created_at
        timestamp delivered_at
        uuid actor_user_id FK
    }

    fediverse_trusted_domain {
        uuid id PK
        string domain
        string note
        timestamp created_at
    }

    fediverse_blocked_domain {
        uuid id PK
        string domain
        string severity
        string reason
        timestamp created_at
    }

    fediverse_personal_access_token {
        uuid id PK
        uuid remote_identity_id FK
        string token_hash
        string name
        string scopes
        timestamp expires_at
        timestamp created_at
    }

    fediverse_resource_share {
        uuid id PK
        uuid remote_identity_id FK
        string resource_type
        string resource_id
        string permissions
        timestamp created_at
    }

    remote_identity ||--o{ fediverse_auth_challenge : "approuve"
    remote_identity ||--o{ fediverse_personal_access_token : "possède"
    remote_identity ||--o{ fediverse_resource_share : "accède à"

Soft-delete : remote_identity.disabled_at est renseigné quand un Delete{Actor} est reçu. Tous les artifacts liés (PATs, shares, memberships) sont cascadés via le hook on_delete_actor.


7. Traitement de l'inbox

flowchart TD
    RECV["POST /inbox\n(body ≤ 1 MiB — ANSSI F-NET-1)"]
    RECV --> PARSE_ACTOR["Extraire actor AP id depuis JSON"]
    PARSE_ACTOR --> TRUST["FediverseTrustService::assert_domain_allowed()"]
    TRUST -->|domaine bloqué / non autorisé| FORBIDDEN["403 Forbidden"]
    TRUST -->|OK| SIG_HEADER["Extraire header Signature"]
    SIG_HEADER -->|absent| UNAUTH1["401 Unauthorized\nmissing Signature header"]
    SIG_HEADER -->|présent| DESERIALIZE["Désérialiser JSON-LD → InboxActivity"]
    DESERIALIZE -->|erreur JSON| ABUSE["observer.on_inbox_abuse('malformed_json')\n→ 400 Bad Request"]
    DESERIALIZE -->|OK| VERIFY["SignatureVerifier::verify(VerifyCtx)"]
    VERIFY -->|échec| SIG_FAIL["observer.on_signature_failure()\n→ 401 Unauthorized"]
    VERIFY -->|OK| DISPATCH["Dispatch par activity.type"]

    DISPATCH -->|Update| UPDATE["FediverseSignalService::apply_update_actor()\nupsert remote_identities"]
    DISPATCH -->|Delete| DELETE["FediverseSignalService::apply_delete_actor_observed()\nsoft-delete + observer.on_delete_actor()"]
    DISPATCH -->|Revoke| REVOKE["FediverseSignalService::apply_revoke_membership()\nretrait team_members"]
    DISPATCH -->|autre| UNSUPPORTED["observer.on_inbox_abuse('unknown_activity_type')\n→ 501 Not Implemented"]

    UPDATE --> ACCEPTED["200 Accepted"]
    DELETE --> ACCEPTED
    REVOKE --> ACCEPTED

Limite de corps : 1 MiB, imposée par DefaultBodyLimit::max(1024 * 1024) dans le router — conforme au plafond Mastodon (ANSSI F-NET-1).

Trust policy : la vérification de domaine précède toute vérification cryptographique. Une instance non autorisée ne consomme même pas de cycles RSA.


8. HTTP Signatures (draft-cavage-12)

Le protocole implémenté est draft-cavage-http-signatures-12, l'algorithme rsa-sha256 (PKCS#1 v1.5 + SHA-256).

sequenceDiagram
    participant Sender as Expéditeur (instance A)
    participant Receiver as Récepteur (instance B)

    Sender->>Sender: Calculer SHA-256(body)
    Sender->>Sender: Encoder en base64 → valeur Digest header
    Sender->>Sender: Construire signing-string<br/>= "(request-target): post /inbox\nhost: b.tld\ndate: …\ndigest: SHA-256=…"
    Sender->>Sender: rsa_sha256_sign(private_pem, signing-string)
    Sender->>Sender: Encoder signature en base64
    Sender->>Receiver: POST /inbox\nSignature: keyId="…#main-key",algorithm="rsa-sha256",<br/>headers="(request-target) host date digest",signature="…"\nDate: …\nDigest: SHA-256=…

    Receiver->>Receiver: parse_signature_header() → ParsedSignature
    Receiver->>Receiver: Vérifier algorithm == "rsa-sha256"
    Receiver->>Receiver: validate_date_header() — drift ≤ 300 s
    Receiver->>Receiver: validate_digest() — SHA-256 constant-time (subtle)
    Receiver->>Receiver: build_signature_input() → re-construire signing-string
    Receiver->>Receiver: Résoudre public key PEM (remote_identities ou fetch actor)
    Receiver->>Receiver: rsa_sha256_verify(public_pem, signing-string, sig)
    alt vérification OK
        Receiver-->>Sender: 200 Accepted
    else vérification KO
        Receiver->>Receiver: observer.on_signature_failure()
        Receiver-->>Sender: 401 Unauthorized
    end

Protections implémentées :

VecteurContre-mesure
Replay temporelDate header validé ±300 s (ANSSI F-CRYPTO-2)
Substitution de corpsDigest: SHA-256=… vérifié en constant-time via subtle (ANSSI F-CRYPTO-1)
Algorithme faibleSeul rsa-sha256 accepté
Clé invalideRsaPublicKey::from_public_key_pem échoue explicitement

Les primitives bas niveau sont dans crates/rustwarden-fediverse/src/http_signatures.rs : parse_signature_header, build_signature_input, rsa_sha256_sign, rsa_sha256_verify, parse_and_verify_cavage.


9. File de livraison sortante

stateDiagram-v2
    [*] --> pending : FediverseOutboundService::enqueue()

    pending --> in_flight : drain_one() — worker tokio

    in_flight --> delivered : OutboundDelivery::deliver() → Ok
    in_flight --> pending : deliver() → Err\nattempts < max_attempts\nnext_retry_at = now + backoff

    pending --> dead : attempts >= max_attempts (8)

    delivered --> [*]
    dead --> [*]

Backoff exponentiel (RFC 0010) :

TentativeDélai avant retry
15 min
230 min
32 h
46 h
524 h
6+48 h (plafonné)
8 (max)→ dead

Le worker tokio appelle drain_one() en boucle. La colonne next_retry_at sert de garde temporel : le find() ne sélectionne que les lignes où next_retry_at IS NULL OR next_retry_at <= now.

Transport :

  • NoopDelivery — prétend que chaque livraison réussit. Utilisé en dev et dans les tests.
  • HttpSignedDelivery — signe la requête sortante avec la clé privée RSA de l'acteur local, puis POSTe vers target_inbox. Introduit par RFC 0010.

Le service est dans crates/rustwarden-fediverse/src/services/fediverse_outbound_service.rs.


10. Gestion de la liste de confiance

FediverseTrustService implémente la politique de domaine (RFC 0004). La configuration est stockée dans app_settings (clés fediverse_trust_mode, fediverse_enabled, fediverse_interop_scope).

Modes de confiance :

ModeComportement
allowlist (défaut)Seuls les domaines explicitement ajoutés à fediverse_trusted_domains sont acceptés. Politique zéro-confiance.
open_with_blocklistTout domaine est accepté sauf ceux listés dans fediverse_blocked_domains.

Sévérités de blocage :

SévéritéEffet
suspendRejette toutes les activités et demandes d'auth.
silenceAccepte mais signale silencieusement.
reject_authRejette l'auth uniquement ; les lookups WebFinger passent.

Scope d'interopérabilité :

ScopeEffet
rustwarden_only (défaut)Auth et invitations réservées aux instances Rustwarden.
rustwarden_plus_universal_readAuth Rustwarden + lectures WebFinger depuis n'importe quelle instance AP.
openTout protocole AP accepté (déconseillé en production).

Configuration minimale :

# Variables d'environnement obligatoires
FEDIVERSE_DOMAIN=mon-instance.example.org

# Variables optionnelles (valeurs par défaut)
FEDIVERSE_AUTHORIZED_FETCH=false
FEDIVERSE_OUTBOUND_WORKERS=4
FEDIVERSE_OUTBOUND_MAX_ATTEMPTS=8
FEDIVERSE_OUTBOUND_REQUEST_TIMEOUT_SECS=30
FEDIVERSE_FETCH_ACTOR_CACHE_TTL_SECS=86400
FEDIVERSE_AUTH_CHALLENGE_TTL_SECS=300
FEDIVERSE_DEBUG_LOCAL_FEDERATION=false   # true uniquement pour tests locaux HTTP

11. Invitations cross-instance

sequenceDiagram
    participant Admin as Admin (instance B)
    participant SP as SP — instance B
    participant IdP as IdP — instance C

    Admin->>SP: POST /api/v1/teams/{team_id}/members/invite-fediverse<br/>{handle: "@bob@c.tld", role: "member"}
    SP->>SP: FediverseInviteService::invite_remote()<br/>→ fediverse_pending_invite (pending, TTL 7 j)
    SP->>SP: FediverseOutboundService::enqueue()<br/>→ Invite{TeamMembership} JSON-LD
    SP-->>Admin: 201 {invite_id, activity_id, expires_at}

    SP->>IdP: POST c.tld/inbox — Invite{TeamMembership} (HTTP-Sig)

    Note over IdP: Bob voit l'invitation dans son interface

    IdP->>SP: POST /api/v1/fediverse/invites/{invite_id}/accept
    SP->>SP: FediverseInviteService::accept()<br/>→ lookup/create remote_identity pour @bob@c.tld<br/>→ insert team_members (remote_identity_id, role=member)
    SP->>SP: observer.on_invite_action(domain, "accept")
    SP-->>IdP: 200 OK

    Note over SP: Bob est maintenant membre de l'équipe sur B<br/>sans avoir de compte local

Machine à états des invitations :

ÉtatTransition
pending→ accepted · → rejected · → revoked (admin) · → expired (TTL)
acceptedterminal
rejectedterminal
revokedterminal
expiredterminal

12. Observers personnalisés — exemple rustwarden-fediverse-guard

flowchart LR
    FEDI["rustwarden-fediverse\nFediverseEventObserver trait"]

    subgraph GUARD ["rustwarden-fediverse-guard"]
        G_IMPL["GuardObserver\nimpl FediverseEventObserver"]
        G_COUNTER["Compteurs atomiques\npar domaine"]
        G_BAN["Auto-ban\n(blocklist DB)"]
    end

    FEDI -->|on_signature_failure| G_IMPL
    FEDI -->|on_replay_attempt| G_IMPL
    FEDI -->|on_inbox_abuse| G_IMPL
    FEDI -->|on_fetch_ssrf| G_IMPL

    G_IMPL --> G_COUNTER
    G_COUNTER -->|seuil dépassé| G_BAN
    G_BAN -->|insert| BLOCKED_DB[(fediverse_blocked_domains)]

rustwarden-fediverse-guard dépend de rustwarden-fediverse (et non l'inverse). Le seul couplage est l'implémentation du trait FediverseEventObserver. La crate guard peut ainsi évoluer indépendamment — ajouter de nouveaux heuristiques, modifier les seuils — sans toucher au protocole.

Voir docs/fediverse-guard.md pour la documentation opérationnelle du guard.


13. Configuration d'un consommateur — exemple complet

L'API de branchement expose plusieurs variantes de router builder :

FonctionUsage
routes::router(config, db)Déprécié depuis 0.2.0 — utilise AlwaysAcceptVerifier. Dev/test uniquement.
routes::router_with(config, resolver, verifier)Injecte resolver et verifier custom. Lit AuthConfig depuis les variables d'environnement.
routes::router_with_auth(config, resolver, verifier, auth_config)Injection complète sans lecture d'env. À préférer en production.
use std::sync::Arc;
use rustwarden_fediverse::{
    config::FediverseConfig,
    routes::router_with_auth,
    resolver::SeaOrmLocalUserResolver,
    http_signatures::HttpSigVerifier,
    observer::NoopFediverseObserver,
};
use rustwarden_core::config::auth::AuthConfig;

// Chargement de la configuration (validation stricte au démarrage).
let fedi_config = FediverseConfig::from_env()?;
let auth_config = Arc::new(AuthConfig::from_env()?);

// Composants par défaut.
let resolver = Arc::new(SeaOrmLocalUserResolver::new(db.clone()));
let verifier = Arc::new(HttpSigVerifier::with_resolver(db.clone()));

// Router fédération.
let fedi_router = router_with_auth(fedi_config, resolver, verifier, auth_config);

// Merge dans le routeur principal Axum.
let app = main_router.merge(fedi_router);

Pour brancher un observer (ex. guard) :

use rustwarden_fediverse::observer::FediverseEventObserver;

let observer: Arc<dyn FediverseEventObserver> = Arc::new(mon_guard_observer);

let fedi_router = router_with_auth(fedi_config, resolver, verifier, auth_config)
    .layer(axum::Extension(observer));

14. Routes exposées

Routes protocole (rustwarden-fediverse)

MéthodeCheminHandlerAuthDescription
GET/.well-known/webfingerwebfinger::handleNonDécouverte JRD RFC 7033
GET/.well-known/nodeinfonodeinfo::discoveryNonLien NodeInfo 2.1
GET/nodeinfo/2.1nodeinfo::detailNonStats instance
GET/users/{username}actor::handleNon (Accept-neg)Profil AP Person
POST/inboxinbox::handleSig HTTPActivités AP entrantes

Routes authentification AP-native (rustwarden-fediverse)

MéthodeCheminHandlerAuthDescription
POST/api/v1/auth/login/fediverse/initiateauth_routes::initiateNonInitie un challenge (SP side)
GET/api/v1/auth/login/fediverse/status/{id}auth_routes::statusNonSonde l'état du challenge
POST/api/v1/auth/login/fediverse/consume/{id}auth_routes::consumeNonÉchange le challenge approuvé contre un JWT
POST/fediverse/auth/inboxauth_routes::auth_inboxSig HTTPReçoit AuthRequest de l'IdP
POST/api/v1/fediverse/auth/consentauth_routes::consentJWT (IdP user)Décision approve/reject de l'IdP

Routes invitations cross-instance (rustwarden-fediverse)

MéthodeCheminHandlerAuthDescription
POST/api/v1/teams/{team_id}/members/invite-fediverseinvite_routes::inviteJWT adminCrée une invitation vers un handle distant
GET/api/v1/teams/{team_id}/invitesinvite_routes::list_invitesJWTListe les invitations en cours
POST/api/v1/teams/{team_id}/invites/{invite_id}/revokeinvite_routes::revokeJWT adminRévoque une invitation
GET/api/v1/fediverse/invitesinvite_routes::idp_listJWTListe les invitations pour un handle (IdP side)
POST/api/v1/fediverse/invites/{invite_id}/acceptinvite_routes::acceptJWTAccepte une invitation
POST/api/v1/fediverse/invites/{invite_id}/rejectinvite_routes::rejectJWTRejette une invitation

Routes UI (rustwarden-fediverse-ui)

Les handlers HTML (login, consentement, panneau admin) sont dans rustwarden-fediverse-ui. Voir docs/fediverse-consumer-guide.md §5 pour la liste complète.


15. Fédération AP vs OAuth/OIDC

DimensionOAuth 2.0 / OIDCActivityPub (fédération)
TopologieClient → serveur centralPair à pair, décentralisé
Protocole authAuthorization Code + PKCE, Token endpointChallenge/consent via activités AP
Transport identitéAccess token (JWT opaque ou structuré)JWT local émis après consume
Dépendance infrastructureProvider OIDC central disponibleInstances AP mutuellement joignables
RévocationBlacklist token ou introspectionDelete{Actor} + cascade on_delete_actor
Cas d'usage RustwardenSSO via Google/GitHub/Microsoft/DiscordSSO inter-instances Rustwarden, invitations multi-domaines

Les deux mécanismes coexistent. Un même utilisateur peut s'authentifier via Google OAuth sur son instance A, puis être invité (en tant que remote_identity) dans une équipe sur l'instance B via AP. Voir 09-hooks-and-events.md pour la documentation des hooks qui couvrent les deux flux.


16. Sécurité

La sécurité de la couche fédération est documentée dans deux fichiers dédiés :

  • docs/fediverse-security-model.md — modèle de menace, choix de conception, surfaces d'attaque et contre-mesures.
  • docs/ANSSI_FEDIVERSE_COMPLIANCE.md — conformité aux recommandations ANSSI : F-CRYPTO-1 (digest corps), F-CRYPTO-2 (replay Date), F-NET-1 (limite taille body), F-NET-2 (validation redirect URL), F-NET-5 (verifier production).

Points d'attention actuels (état 2026-05-17) :

RéférenceDescription
F-NET-5routes::router() dépréciée utilise AlwaysAcceptVerifier. Utiliser router_with_auth en production.
RFC 0005Les clés RSA d'acteur sont générées à la demande. La rotation de clé n'est pas encore implémentée.
RFC 0015Fenêtre de replay à 300 s. Un attaquant avec capture réseau dispose de 5 minutes. Une fenêtre plus courte (60 s) sera envisagée en V2.

17. Limites actuelles et roadmap V2

LimiteImpactPlan V2
Pas de rotation des clés RSA d'acteurCompromission longue durée non détectableRFC futur — rotation avec période de grâce
Fenêtre de replay 300 sAttaque en 5 min sur lien capturéRéduire à 60 s + nonce unique par requête
Zéro #[instrument] sur les servicesTraces non structurées, debugging difficileInstrumenter auth, token, team, totp (voir 10-testing-and-observability.md)
Zéro métriques PrometheusPas de SLO possible sur le protocole APCompteurs rustwarden_fediverse_* exposés sur /metrics
Worker outbound sans supervisionPanne silencieuse si worker bloquéGauge rustwarden_outbound_queue_depth + alerting
NoopDelivery en production possibleActivités perdues silencieusementHttpSignedDelivery à rendre obligatoire en prod
AlwaysAcceptVerifier accessible via router()Risque de mise en prod accidentelleSupprimer l'export public en V2

Pour la roadmap complète, voir docs/V2_ROADMAP.md.