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.mdetdocs/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 depuisremote_identitiesquandpublic_key_pemest vide. Valide la fenêtre temporelle (Date±300 s) et leDigestSHA-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 :
| État | Transition suivante |
|---|---|
pending | → approved (IdP consent) · → rejected · → expired (TTL dépassé) |
approved | → consumed (consume one-shot) |
rejected | terminal |
expired | terminal |
consumed | terminal |
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 :
| Vecteur | Contre-mesure |
|---|---|
| Replay temporel | Date header validé ±300 s (ANSSI F-CRYPTO-2) |
| Substitution de corps | Digest: SHA-256=… vérifié en constant-time via subtle (ANSSI F-CRYPTO-1) |
| Algorithme faible | Seul rsa-sha256 accepté |
| Clé invalide | RsaPublicKey::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) :
| Tentative | Délai avant retry |
|---|---|
| 1 | 5 min |
| 2 | 30 min |
| 3 | 2 h |
| 4 | 6 h |
| 5 | 24 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 verstarget_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 :
| Mode | Comportement |
|---|---|
allowlist (défaut) | Seuls les domaines explicitement ajoutés à fediverse_trusted_domains sont acceptés. Politique zéro-confiance. |
open_with_blocklist | Tout domaine est accepté sauf ceux listés dans fediverse_blocked_domains. |
Sévérités de blocage :
| Sévérité | Effet |
|---|---|
suspend | Rejette toutes les activités et demandes d'auth. |
silence | Accepte mais signale silencieusement. |
reject_auth | Rejette l'auth uniquement ; les lookups WebFinger passent. |
Scope d'interopérabilité :
| Scope | Effet |
|---|---|
rustwarden_only (défaut) | Auth et invitations réservées aux instances Rustwarden. |
rustwarden_plus_universal_read | Auth Rustwarden + lectures WebFinger depuis n'importe quelle instance AP. |
open | Tout 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 :
| État | Transition |
|---|---|
pending | → accepted · → rejected · → revoked (admin) · → expired (TTL) |
accepted | terminal |
rejected | terminal |
revoked | terminal |
expired | terminal |
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 :
| Fonction | Usage |
|---|---|
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éthode | Chemin | Handler | Auth | Description |
|---|---|---|---|---|
GET | /.well-known/webfinger | webfinger::handle | Non | Découverte JRD RFC 7033 |
GET | /.well-known/nodeinfo | nodeinfo::discovery | Non | Lien NodeInfo 2.1 |
GET | /nodeinfo/2.1 | nodeinfo::detail | Non | Stats instance |
GET | /users/{username} | actor::handle | Non (Accept-neg) | Profil AP Person |
POST | /inbox | inbox::handle | Sig HTTP | Activités AP entrantes |
Routes authentification AP-native (rustwarden-fediverse)
| Méthode | Chemin | Handler | Auth | Description |
|---|---|---|---|---|
POST | /api/v1/auth/login/fediverse/initiate | auth_routes::initiate | Non | Initie un challenge (SP side) |
GET | /api/v1/auth/login/fediverse/status/{id} | auth_routes::status | Non | Sonde l'état du challenge |
POST | /api/v1/auth/login/fediverse/consume/{id} | auth_routes::consume | Non | Échange le challenge approuvé contre un JWT |
POST | /fediverse/auth/inbox | auth_routes::auth_inbox | Sig HTTP | Reçoit AuthRequest de l'IdP |
POST | /api/v1/fediverse/auth/consent | auth_routes::consent | JWT (IdP user) | Décision approve/reject de l'IdP |
Routes invitations cross-instance (rustwarden-fediverse)
| Méthode | Chemin | Handler | Auth | Description |
|---|---|---|---|---|
POST | /api/v1/teams/{team_id}/members/invite-fediverse | invite_routes::invite | JWT admin | Crée une invitation vers un handle distant |
GET | /api/v1/teams/{team_id}/invites | invite_routes::list_invites | JWT | Liste les invitations en cours |
POST | /api/v1/teams/{team_id}/invites/{invite_id}/revoke | invite_routes::revoke | JWT admin | Révoque une invitation |
GET | /api/v1/fediverse/invites | invite_routes::idp_list | JWT | Liste les invitations pour un handle (IdP side) |
POST | /api/v1/fediverse/invites/{invite_id}/accept | invite_routes::accept | JWT | Accepte une invitation |
POST | /api/v1/fediverse/invites/{invite_id}/reject | invite_routes::reject | JWT | Rejette 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
| Dimension | OAuth 2.0 / OIDC | ActivityPub (fédération) |
|---|---|---|
| Topologie | Client → serveur central | Pair à pair, décentralisé |
| Protocole auth | Authorization Code + PKCE, Token endpoint | Challenge/consent via activités AP |
| Transport identité | Access token (JWT opaque ou structuré) | JWT local émis après consume |
| Dépendance infrastructure | Provider OIDC central disponible | Instances AP mutuellement joignables |
| Révocation | Blacklist token ou introspection | Delete{Actor} + cascade on_delete_actor |
| Cas d'usage Rustwarden | SSO via Google/GitHub/Microsoft/Discord | SSO 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érence | Description |
|---|---|
| F-NET-5 | routes::router() dépréciée utilise AlwaysAcceptVerifier. Utiliser router_with_auth en production. |
| RFC 0005 | Les clés RSA d'acteur sont générées à la demande. La rotation de clé n'est pas encore implémentée. |
| RFC 0015 | Fenê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
| Limite | Impact | Plan V2 |
|---|---|---|
| Pas de rotation des clés RSA d'acteur | Compromission longue durée non détectable | RFC futur — rotation avec période de grâce |
| Fenêtre de replay 300 s | Attaque en 5 min sur lien capturé | Réduire à 60 s + nonce unique par requête |
Zéro #[instrument] sur les services | Traces non structurées, debugging difficile | Instrumenter auth, token, team, totp (voir 10-testing-and-observability.md) |
| Zéro métriques Prometheus | Pas de SLO possible sur le protocole AP | Compteurs rustwarden_fediverse_* exposés sur /metrics |
| Worker outbound sans supervision | Panne silencieuse si worker bloqué | Gauge rustwarden_outbound_queue_depth + alerting |
NoopDelivery en production possible | Activités perdues silencieusement | HttpSignedDelivery à rendre obligatoire en prod |
AlwaysAcceptVerifier accessible via router() | Risque de mise en prod accidentelle | Supprimer l'export public en V2 |
Pour la roadmap complète, voir docs/V2_ROADMAP.md.
GitRust