- Créer soi-même un serveur ActivityPub expose dès la première requête
Followà un401 Unauthorizedsans explication, et Fedify est un framework TypeScript qui déplace la charge liée aux signatures, à JSON-LD, à la livraison et à la sécurité hors du code applicatif - L’authentification du fédivers utilise à la fois le brouillon expiré
draft-cavage-http-signatures-12et le standardRFC 9421; si l’on inclut aussi la signature des documents, il faut gérer quatre mécanismes de signature ainsi que des clés RSA et Ed25519 - Une même activité ActivityPub peut arriver en JSON-LD sous plusieurs formes — chaîne, tableau, objet inline, référence URI, etc. — si bien qu’une implémentation maison finit par disséminer du code défensif dans toute la base de code
- Dans la livraison distribuée, des problèmes comme les « posts zombies », où un
Deletearrive avant unCreate, peuvent survenir ; il faut alors files d’attente, retries, idempotence, garantie d’ordre et disjoncteurs - Fedify fournit des intégrations avec 13 frameworks web, des adaptateurs KV et files de messages, ainsi qu’une CLI, un linter, un débogueur et OpenTelemetry, afin de permettre de développer des apps fédérées sans connaissance détaillée d’ActivityPub
Les problèmes rencontrés quand on implémente directement ActivityPub
- Pour envoyer une première activité
Followà Mastodon, il faut produire le JSON, signer la requête HTTP puis effectuer lePOST, mais en cas d’échec, on peut ne recevoir qu’une seule ligne :401 Unauthorized- La cause peut être un décalage d’horloge dans l’en-tête
Date, une erreur de hachageDigest, la casse de(request-target), la façon de représenter la clé publique, etc. - Si le serveur distant n’indique pas la raison, il faut déboguer en lisant le code d’autres serveurs
- La cause peut être un décalage d’horloge dans l’en-tête
- Fedify est né lors de la création de Hollo, un serveur de microblogging mono-utilisateur
- Comme la charge d’implémentation d’ActivityPub engloutissait le développement du produit, il est devenu un framework nécessaire avant même l’application
- Les difficultés se concentrent principalement sur les standards de signature, la forme des documents JSON-LD, la livraison distribuée, les usages propres aux différentes implémentations et les réglages de sécurité par défaut
Il n’existe pas un seul standard de signature
- L’authentification entre serveurs utilise des signatures HTTP, mais dans le fédivers réel coexistent le brouillon expiré draft-cavage-http-signatures-12 et le standard RFC 9421
- Comme il est impossible de savoir à l’avance quelle signature un serveur acceptera, il faut signer avec une méthode, puis, en cas de rejet, signer de nouveau avec l’autre et mémoriser par serveur celle qui a fonctionné
- Cette procédure est appelée double-knocking
- Les signatures HTTP ne prouvent que l’émetteur de la requête ; dans des situations comme l’inbox forwarding, où une activité reçue est transmise à un tiers, il faut aussi une signature attachée au document lui-même
- Les mécanismes nécessaires sont au nombre de quatre au total, dont Linked Data Signatures et Object Integrity Proofs
- Il faut gérer en parallèle deux types de clés, RSA et Ed25519
La forme des documents JSON-LD change constamment
- Le format de transmission d’ActivityPub est JSON-LD, et une activité
Createayant le même sens peut être représentée de plusieurs façonsactorpeut être une chaîne URI ou un objetPersoninlinetopeut être une seule chaîne ou un tableauobjectpeut être aussi bien un objet inline qu’une URI
- L’adresse qui désigne le public peut elle aussi s’écrire valablement sous trois formes :
https://www.w3.org/ns/activitystreams#Public,as:PublicouPublic - Pour traiter cela conformément à la spécification, il faut normaliser via un processeur JSON-LD, après expansion puis compaction
- Beaucoup d’implémentations le traitent comme du « simple JSON » et cassent silencieusement avec la forme émise par tel ou tel serveur
- Si l’on implémente soi-même, du code défensif apparaît partout pour vérifier si une valeur est une chaîne, un tableau, un objet ou une URI à récupérer
Livraison distribuée et « posts zombies »
- Si un utilisateur publie un message puis repère aussitôt une faute et le supprime, le serveur envoie un
Createpuis unDelete, mais selon l’état du réseau, le serveur destinataire peut recevoir leDeleteen premier- S’il ignore la suppression d’un message qui n’existe pas encore puis traite ensuite le
Create, le message que l’auteur croit supprimé reste présent sur ce serveur
- S’il ignore la suppression d’un message qui n’existe pas encore puis traite ensuite le
- Avec 5 000 abonnés, une seule publication génère des milliers de livraisons HTTP ; si elles sont traitées dans le gestionnaire de requête, la réponse au bouton de publication ralentit ou le serveur peut s’effondrer
- Même avec une file d’attente, il faut décider du calendrier de retry des livraisons échouées, du backoff exponentiel, du nombre de tentatives, de la différence entre
500 Internal Server Erroret410 Gone, du nettoyage des abonnés de serveurs disparus et du traitement des hôtes en panne longue durée - Ce domaine relève davantage de l’ingénierie des systèmes distribués que d’une simple implémentation de protocole
La spécification ne suffit pas à garantir l’interopérabilité
- Même en respectant parfaitement la spécification, les problèmes d’interopérabilité avec les implémentations réelles du fédivers demeurent
- Le secure mode de Mastodon utilise l’authorized fetch, qui exige une signature HTTP même pour les requêtes
GET- Si les deux serveurs sont en secure mode, une impasse apparaît : pour récupérer la clé publique de l’autre, il faut signer, mais pour vérifier la signature, l’autre doit d’abord récupérer ma clé publique
- La communauté contourne le problème en signant avec un instance actor représentant le serveur lui-même, mais cela ne figure pas dans la spécification
- Threads ne parvient pas à analyser les activités dont
actorest un objet inline ; lorsqu’on envoie vers Threads, il faut donc envoyeractorsous forme d’URI - Lemmy rejette silencieusement les requêtes si certains champs de l’actor
Group, que Mastodon n’exige pas, sont absents- Les exemples sont la collection de moderators reliée par
attributedToet la collectionfeatured
- Les exemples sont la collection de moderators reliée par
- Misskey possède ses propres extensions de vocabulaire, et rien que pour les quote posts, trois noms de propriété différents sont utilisés selon les implémentations
- L’interopérabilité n’est pas un travail que l’on règle une fois pour toutes : c’est un domaine à maintenir en continu
L’état par défaut d’une implémentation maison n’est pas sûr
- Si l’on saute la vérification des signatures des activités entrantes, n’importe qui peut injecter de faux
FollowouDelete - Si l’on ne restreint pas le chargeur de documents, une activité malveillante peut pointer vers
http://169.254.169.254/ou le réseau interne et transformer le serveur en proxy SSRF - Si l’on omet de vérifier la provenance des objets embarqués, n’importe quel serveur peut émettre un document donnant l’impression qu’une personne donnée a dit quelque chose
- Ces pièges ne se voient pas immédiatement et, jusqu’à leur exploitation, tout peut sembler fonctionner
Ce que Fedify prend en charge à votre place
- Fedify est une bibliothèque TypeScript pour créer des applications serveur fédérées avec ActivityPub et les standards associés
- Elle s’exécute sur Deno, Node.js et Bun, et prend aussi en charge les runtimes edge comme Cloudflare Workers
- Son objectif de conception est de retirer du code applicatif les signatures, JSON-LD, la livraison, les différences entre implémentations et les détails de sécurité
-
Gestion des signatures
- En enregistrant un actor dispatcher et un key pair dispatcher, on peut publier un actor sur le fédivers
- Toutes les requêtes sortantes sont signées
- Avec des clés RSA, elle émet des HTTP Signatures et des Linked Data Signatures
- En ajoutant une clé Ed25519, elle ajoute aussi des Object Integrity Proofs
- Les quatre mécanismes peuvent coexister dans une même activité, et le destinataire vérifie avec la méthode la plus robuste qu’il comprend
- Fedify gère directement le double-knocking
- Le premier contact part en RFC 9421, puis, en cas de refus, une nouvelle tentative est faite en draft-cavage
- La méthode qui a réussi est mise en cache par serveur
- Si la réponse de refus contient un challenge
Accept-Signature, la requête est resignée avec les composants demandés par le serveur
- Les signatures entrantes sont vérifiées avant que le code applicatif ne les voie, et les activités dont la vérification échoue n’atteignent pas les listeners
- Le simple enregistrement d’un actor dispatcher crée aussi un serveur WebFinger RFC 7033, ce qui permet de trouver un actor sous la forme
@alice@example.comdans la barre de recherche de Mastodon
-
Manipuler des types plutôt que JSON-LD
- Fedify fournit environ 80 classes couvrant l’ensemble de l’Activity Vocabulary et les principales extensions de fournisseurs
- Les classes sont typées et immuables, et leurs accesseurs absorbent les différences de formes de documents autorisées par JSON-LD
lookupObject()prend un handle et exécute toute la procédure de recherche, y compris la découverte WebFinger- Les accesseurs comme
getFollowers()fonctionnent de la même manière que la valeur soit une référence URI ou un objet inline, et les valeurs récupérées sont mises en cache - Les différences propres aux fournisseurs sont également masquées derrière l’API
- Les trois propriétés de citation
quoteUri,_misskey_quoteetquoteUrlsont unifiées derrière une seule API, avec lequotedu nouveau FEP-044f - La propriété
isCatde Misskey existe aussi comme type, ce qui permet de la traiter avec la sécurité du typage
- Les trois propriétés de citation
-
Infrastructure de livraison et garantie d’ordre
- Si l’on connecte une file de messages à
createFederation(), la livraison passe en arrière-plan et, en cas d’échec, des nouvelles tentatives automatiques sont effectuées par défaut jusqu’à 10 fois avec backoff exponentiel - Lorsqu’un billet doit être livré à des milliers de followers, un fan-out en deux étapes s’active
- Un message agrégé unique est placé dans la file
- Un worker en arrière-plan le découpe en tâches de livraison par serveur
- Le bouton de publication répond immédiatement
- Comme une même activité peut arriver deux fois à cause des nouvelles tentatives, Fedify ignore les doublons avant les handlers grâce à un cache d’idempotence qui conserve les activités traitées pendant 24 heures
- Si l’appel à
sendActivity()précise{ orderingKey: post.id }, les activités partageant la même orderingKey sont livrées à chaque serveur destinataire dans l’ordre d’envoi- Un
Deletene peut pas dépasser unCreate - Les activités ayant une clé différente partent en parallèle afin de préserver le débit
- Un
- En cas de
404 Not Foundou de410 Gone, les nouvelles tentatives s’arrêtent et le handler d’échec permanent de livraison enregistré est appelé - En cas d’envoi vers une shared inbox, il est aussi possible de recevoir la liste des followers qui se trouvent derrière afin de nettoyer les comptes disparus
- Pour les hôtes qui échouent de façon répétée, le disjoncteur, activé par défaut, suspend les livraisons et vérifie périodiquement la récupération
- Si l’on connecte une file de messages à
Pratiques propres aux implémentations et valeurs par défaut de sécurité
- Pour l’authorized fetch, Fedify connecte
.authorize()au dispatcher et transmet au callback l’identité vérifiée du demandeur- Les traitements comme les listes de blocage ou les collections privées peuvent être écrits dans la logique applicative
- Il existe aussi un pattern pris en charge pour le problème de blocage mutuel lié à l’instance actor
- Le problème des actors inline de Threads est traité par l’activity transformer, activé par défaut, qui remplace les actors inline des activités sortantes par des URI
- La collection moderators exigée par Lemmy peut être exposée en quelques lignes avec la custom collection API, et le contexte JSON-LD de Lemmy est inclus à l’avance
- Lorsqu’un nouveau problème d’interopérabilité est découvert, le correctif est intégré à Fedify plutôt qu’à chaque application
- Les valeurs par défaut de sécurité privilégient la sûreté
- La vérification des signatures n’est pas une fonctionnalité à activer, mais une fonctionnalité à désactiver pour les tests
- Le chargeur de documents bloque par défaut les plages d’adresses privées et loopback, et prend aussi en compte le DNS rebinding
- Pour être exposé au SSRF, il faut activer explicitement une option dont le nom indique qu’elle est destinée aux tests
- Si l’origine d’un objet embarqué diffère de celle du document parent, l’accesseur ne lui fait pas confiance et le récupère à nouveau depuis la source
- Ce modèle de sécurité fondé sur l’origine repose sur FEP-fe34
Pile existante et outils de développement
- Fedify est conçu pour s’adapter aux piles web existantes et propose 13 intégrations de frameworks web
- Express, Hono, Fastify, Koa, NestJS, Elysia
- Next.js, Nuxt, SvelteKit, Astro, SolidStart, Fresh
- Le middleware gère la négociation de contenu, ce qui permet à une même URL de servir du HTML aux navigateurs et du JSON-LD au fediverse
- Le stockage propre à Fedify ne nécessite qu’une interface clé-valeur
- Des adaptateurs Redis, PostgreSQL, MySQL/MariaDB, SQLite, Deno KV, Cloudflare Workers KV et in-memory sont disponibles
- La file de messages propose 8 options, dont PostgreSQL, Redis et AMQP/RabbitMQ ; si aucune ne convient, il est possible d’implémenter directement l’interface
- Les données de domaine peuvent rester telles quelles dans votre base de données et votre ORM existants
- Si vous exploitez déjà la federation avec une autre bibliothèque, un guide de migration et des scripts de migration de données permettent de passer depuis activitypub-express, entre autres, sans perdre vos abonnés existants
- Des packages de plus haut niveau sont également fournis
@fedify/relayfournit un serveur relay ActivityPub complet avec un simple appel de fonction@fedify/backfillparcourt le fediverse et restaure les fils de discussion incomplets
-
Outils pour la boucle de développement
fedify initgénère la structure d’un projet en une seule lignefedify tunnelexpose le serveur local en HTTPS pour permettre des tests avec un vrai Mastodonfedify inboxlance un serveur inbox temporaire pour recevoir les activités envoyées par le serveurfedify lookuppermet d’inspecter des objets publiés par d’autres serveursfedify lookup --authorized-fetchcrée une paire de clés jetable, monte un serveur ActivityPub temporaire et envoie des requêtes signées aux objets placés derrière le secure mode@fedify/lintest un linter dédié à ActivityPub qui détecte 20 bugs d’interopérabilité, par exemple lorsqu’un actor n’a pas d’inbox- Les mocks de
@fedify/testingpermettent d’exécuter des tests sans réseau @fedify/debuggerajoute un tableau de bord de debug en une ligne, pour voir en temps réel dans le navigateur les activités et les résultats de vérification des signatures- En production, une instrumentation OpenTelemetry est intégrée, avec 28 types de spans et 37 métriques
- Un guide de monitoring et un outil de test de charge pour ActivityPub,
fedify bench, sont également fournis - La documentation officielle se compose d’un manuel de 30 chapitres et de 5 tutoriels, et couvre des pratiques concrètes comme les requêtes PromQL et règles d’alerte pour observer le backlog des files, ou les propriétés nécessaires pour que les avatars s’affichent dans Mastodon
Exemples d’utilisation existants et prise en main
- Fedify est utilisé dans des services réels
- Le service ActivityPub de Ghost
- Encyclia, qui relie les profils de chercheurs ORCID au fediverse
- SiliconBeest, qui fonctionne en serverless sur Cloudflare Workers
- La plateforme de blogging coréenne Typo Blue
- Hollo, une plateforme de microblogging mono-utilisateur
- Hackers' Pub, opéré par la communauté
- Les tutoriels proposent des exemples selon l’échelle
- Un serveur mono-fichier de quelques dizaines de lignes peut être suivi par Mastodon
- Un service de partage d’images d’environ 750 lignes interopère avec Pixelfed pour les abonnements, les likes et les commentaires
- Le tutoriel de plateforme communautaire couvre une federation bidirectionnelle avec le vrai lemmy.ml
- L’objectif de Fedify n’est pas de former davantage d’experts ActivityPub, mais de permettre aux développeurs de créer des apps fédérées sans connaître les détails d’ActivityPub
- La commande de démarrage est
npm init @fedify - Pour obtenir de l’aide, vous pouvez utiliser le salon Matrix ou les GitHub Discussions
1 commentaires
Avis sur Lobste.rs
C’est pour ça qu’il y a autant de forks de projets ActivityPub : il est plus facile de comprendre l’approche de quelqu’un d’autre que de tout implémenter soi-même.
Ce que propose l’auteur ne semble pas très différent, en pratique, des forks de Misskey ou de Pleroma qu’on voit souvent. Même une bibliothèque a son propre point de vue et sa propre approche, et ne semble pas laisser énormément de contrôle. Cela dit, elle a l’avantage de ne pas imposer l’UI comme lorsqu’on fork un serveur entier.
Du point de vue de quelqu’un qui implémente AP, la partie la plus difficile est qu’il n’existe pas de bonne façon d’utiliser correctement JSON-LD. Si l’on pouvait convertir facilement des objets vers une représentation standard, les interactions suivraient naturellement ; mais l’utiliser comme de vrais documents liés est beaucoup trop inefficace, et l’utiliser comme de simples documents JSON bruts vous noie sous d’innombrables cas particuliers. Jusqu’ici, j’ai choisi la deuxième approche et j’en suis mort.
Ce n’est pas exactement le même problème que dans le monde JSON-LD, mais ce n’est pas totalement sans rapport non plus.
Cela dit, je pense qu’une bonne partie des technologies voisines de JSON souffrent de problèmes similaires. Il y a trop de façons, avec JSON Schema, de représenter le même schéma logique, ce qui rend l’interaction avec les technologies autour de JSON Schema ridiculement horrible. Les schémas OpenAPI, en particulier, sont une horreur proche mais pas identique, et même sans tenir compte du nombre de versions de brouillons de schémas, c’est déjà suffisamment mauvais.
Le service « MTA » d’AP serait chargé d’envoyer les messages depuis la boîte d’envoi et de recevoir les messages dans la boîte de réception. Les documents JSON-LD seraient, du point de vue de ce service, presque de simples blocs de données. Il faut un peu de parsing pour déterminer l’expéditeur et le destinataire, mais pas beaucoup plus. Le stockage pourrait aussi être basé sur des fichiers ; si je me souviens bien, go-ap procède ainsi.
Le « MUA » d’AP serait l’application proprement dite. C’est elle qui doit comprendre la sémantique de JSON-LD. On pourrait utiliser quelque chose comme PostgreSQL pour stocker les documents en jsonb, puis fournir une forme plus compatible SQL avec des colonnes générées et des vues. On pourrait alors décider, selon le type d’objet, de la meilleure façon de représenter le document.
Autre exemple : un service de recherche pourrait lui aussi être modélisé comme un acteur et renvoyer les résultats via une boîte d’envoi temporaire.
C’est une liste extrêmement précieuse des comportements atypiques de plusieurs implémentations et de leurs contournements.
Malheureusement, GoActivityPub n’en implémente même pas encore la moitié.
J’ai apprécié que l’article commence sur des aspects techniques, mais à mi-chemin il a semblé bifurquer vers la promotion de son framework, ce qui a rendu la lecture moins agréable.
C’est une bonne chose que, dans certains univers qui utilisent TypeScript, on puisse éviter de redécouvrir ces particularités d’implémentation. Mais, comme modèle mental, si l’on dispose d’un registre indiquant « dans telles conditions et telle situation, on obtient tel résultat, et il faut telle correction », alors des personnes hors de TypeScript — par exemple l’auteur du projet frère GoActivityPub — peuvent aussi bénéficier de ce travail pénible. L’article en couvre bien quelques-unes, mais il reste un instantané à un moment donné, tandis que le projet semble vouloir accumuler au fil du temps tous les bugs d’interopérabilité.
L’alternative actuelle, à mes yeux, consiste à lire tous les messages de commit qui n’ont pas été écrits par un humain, en essayant de distinguer les bugs propres à Fedify des bugs d’interopérabilité.
C’est particulièrement ironique alors même que le dépôt semble être « à fond » sur l’IA, sans tenir ce genre de registre. Tout le discours promotionnel que j’ai entendu sur les LLM, c’était qu’ils automatisent les tâches répétitives. Dans ce cas, Claude pourrait créer des issues GitHub ou, mieux encore, documenter dans un fichier .md du dépôt les observations faites et la manière dont Fedify les corrige. Comme il y a déjà son propre débogueur et des « bonnes pratiques » dont je ne comprends pas le sens, ce serait une tâche parfaitement adaptée.
Pourquoi exécuter en ligne des requêtes vers des services tiers ? C’est la base d’une application web. Si vous devez communiquer avec un service tiers, envoyez ça dans une tâche en arrière-plan. Si l’information n’est pas nécessaire pour répondre à la requête, envoyez-la dans une tâche en arrière-plan. Les problèmes qui surviennent parce qu’on fait ce genre de requêtes dans un handler relèvent de la faute auto-infligée du type marcher sur un râteau et se le prendre dans la figure ; cela n’a rien à voir avec ActivityPub.
Si une livraison échoue, il faut réessayer ; décider du calendrier, d’un backoff exponentiel, du nombre de tentatives, de la façon de traiter un 500 Internal Server Error par rapport à un 410 Gone, etc., ce sont simplement des problèmes ordinaires de développement d’applications web. Ce sont les problèmes qu’on rencontre quand une file de tâches appelle des services tiers, pas des problèmes propres à ActivityPub. La plupart des frameworks web ont des valeurs par défaut raisonnables. Il ne faut vraiment réfléchir qu’au moment de décider, selon l’erreur, s’il faut réessayer ou non. Réessayer un 410 est du gaspillage, mais ce n’est pas un problème urgent à résoudre. Cela augmentera la pression mémoire sur la file de tâches, mais il est peu probable que ça fasse tomber l’application en quelques heures.
« Regardez si c’est rejeté, signez à nouveau autrement, et souvenez-vous de la méthode qui a marché pour chaque serveur » : mais qu’est-ce que je suis en train de lire ? Est-ce pour ça que le développement de Mastodon est lent ?
« Une publication représente des milliers de livraisons HTTP », et cela dans Ruby, un langage réputé pour exceller en programmation de systèmes réseau et en mise en file d’attente.
C’est difficile à croire, et c’est bien que ce soit encapsulé dans une bibliothèque, mais quand même.
Après avoir implémenté ActivityPub en Java, j’en suis arrivé à la conclusion que ce type de protocole inter-serveurs ferait mieux d’être construit au-dessus de git.
Une grande partie de la complexité existe pour résoudre à nouveau des problèmes que git résout déjà mieux. Si l’on modélise cela comme des documents JSON dans un dépôt git, on n’a plus à gérer la pagination. Le protocole garantit déjà de n’envoyer que les données absentes, fournit les signatures de commits, garantit l’ordre des événements, résout aussi les problèmes mentionnés dans cet article, et donne l’historique gratuitement. On pourrait presque formuler une sorte de dixième loi de Greenspun disant que ce genre de protocole contient une demi-implémentation de git, lente et pleine de bugs.
Cet article se lit comme un texte de mauvaise qualité généré par IA.
Plus précisément, je ne comprends pas pourquoi il est écrit sous forme narrative. Les faits qu’il transmet auraient pu être présentés de manière bien plus concise et moins biaisée, et le récit n’est pas convaincant. D’autant qu’il contient beaucoup de formulations typiques de l’IA.
Je n’ai pas pris plaisir à le lire. Je remercie tout de même l’auteur d’avoir signalé ces problèmes, et j’espère pouvoir les lire et les corriger d’une autre manière.