2 points par GN⁺ 5 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Créer soi-même un serveur ActivityPub expose dès la première requête Follow à un 401 Unauthorized sans 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-12 et le standard RFC 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 Delete arrive avant un Create, 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 le POST, 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 hachage Digest, 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
  • 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é
  • 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

La forme des documents JSON-LD change constamment

  • Le format de transmission d’ActivityPub est JSON-LD, et une activité Create ayant le même sens peut être représentée de plusieurs façons
    • actor peut être une chaîne URI ou un objet Person inline
    • to peut être une seule chaîne ou un tableau
    • object peut ê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:Public ou Public
  • 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 Create puis un Delete, mais selon l’état du réseau, le serveur destinataire peut recevoir le Delete en 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
  • 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 Error et 410 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 actor est un objet inline ; lorsqu’on envoie vers Threads, il faut donc envoyer actor sous 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 attributedTo et la collection featured
  • 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 Follow ou Delete
  • 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.com dans 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_quote et quoteUrl sont unifiées derrière une seule API, avec le quote du nouveau FEP-044f
      • La propriété isCat de Misskey existe aussi comme type, ce qui permet de la traiter avec la sécurité du typage
  • 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 Delete ne peut pas dépasser un Create
      • Les activités ayant une clé différente partent en parallèle afin de préserver le débit
    • En cas de 404 Not Found ou de 410 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

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/relay fournit un serveur relay ActivityPub complet avec un simple appel de fonction
    • @fedify/backfill parcourt le fediverse et restaure les fils de discussion incomplets
  • Outils pour la boucle de développement

    • fedify init génère la structure d’un projet en une seule ligne
    • fedify tunnel expose le serveur local en HTTPS pour permettre des tests avec un vrai Mastodon
    • fedify inbox lance un serveur inbox temporaire pour recevoir les activités envoyées par le serveur
    • fedify lookup permet d’inspecter des objets publiés par d’autres serveurs
    • fedify lookup --authorized-fetch cré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/lint est 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/testing permettent d’exécuter des tests sans réseau
    • @fedify/debugger ajoute 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
  • 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

 
GN⁺ 5 시간 전
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.

    • La question de la « représentation standard d’un objet » devient encore plus importante, surtout quand on pense aux signatures. L’ancienne canonicalisation XML existait précisément pour ce problème de signature : garantir que la sérialisation en octets du destinataire corresponde à celle de l’expéditeur.
      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.
    • J’ai envisagé d’implémenter un serveur AP, mais je n’ai pas encore commencé, donc à prendre avec beaucoup de recul. Une piste utile pourrait être de découper l’application en services plus petits et de s’appuyer davantage sur le modèle d’acteurs pour donner l’impression d’une interface « intégrée ». On peut par exemple s’inspirer de la séparation MTA/MUA dans les serveurs mail.
      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.

    • L’article exagère vraiment des problèmes triviaux en les présentant comme un échec d’ActivityPub. Par exemple : avec 5 000 abonnés, une publication entraîne des milliers de livraisons HTTP ; si on fait ça directement dans le handler de requête, la réponse au bouton publier prend 30 secondes ou le serveur s’écroule, donc il faut utiliser une file.
      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.

    • git n’est pas un excellent choix, car l’historique dépend des commits parents. En revanche, un protocole de gossip à arbre de Merkle fonctionnant avec une stratégie de négociation similaire pourrait bien convenir.
  • 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.