RFC 0015 — Fediverse: signatures HTTP draft-cavage (RSA-SHA256)
- Auteur : EBO (rustwarden)
- Date : 2026-05-10
- Statut : Accepted (auto-revue mainteneur unique 2026-05-10, score 27/35)
- Apps consommatrices concernées : toute app utilisant
rustwarden-corequi veut interopérer avec Mastodon, GoToSocial, Pleroma — c'est-à-dire 100 % des apps fediverse en prod. - Plan associé :
docs/plans/2026-05-09-fediverse-identity-federation.md, Phase 5 — déblocage interop. - Dépendances : RFC 0005 (clés acteur), RFC 0010 (
SignatureVerifier), RFC 0011 (OutboundDelivery).
Summary
Livrer deux implémentations cryptographiques branchées sur les traits déjà en place :
HttpSigVerifier— impl deSignatureVerifierqui valide le header HTTPSignaturestyle draft-cavage-12 (utilisé par Mastodon) avec RSA-SHA256.HttpSignedDelivery— impl deOutboundDeliveryqui signe les activités sortantes avec la clé privée RFC 0005 du local actor avant lePOSTHTTP.
Aucune nouvelle dépendance externe : on réutilise rsa (RFC 0005) + sha2 + base64 + reqwest (déjà pulled par feature oauth).
Motivation
Sans cette RFC, AlwaysAcceptVerifier accepte n'importe quelle requête entrante sur /inbox et /fediverse/auth/inbox, et NoopDelivery n'envoie aucune requête sortante. Concrètement, Mastodon refuse toute fédération avec rustwarden tant que les signatures HTTP ne sont pas implémentées (Authorized Fetch + signed deliveries sont obligatoires sur Mastodon ≥ 3.0).
Sans cette RFC, l'objectif Phase 5 « bob@a.tld se connecte avec succès sur b.tld » reste impossible avec Mastodon comme a.tld.
Non-goals
- Pas d'implémentation RFC 9421 complète (la version finale du standard). On vise draft-cavage-12 car c'est ce que Mastodon parle aujourd'hui ; un follow-up suivra quand Mastodon migrera.
- Pas de support Ed25519 — Mastodon impose RSA-2048.
- Pas de support algorithmes autres que
rsa-sha256/hs2019. - Pas de blob de "double-knocking" (essayer cavage puis 9421) — un seul format pour cette livraison.
- Pas de cache de clé publique — le
RemoteIdentityService(RFC 0003) gère déjà le cache via TTL. - Pas de gestion du
Digestbody header — on signe(request-target) host dateminimum côté sortant ; côté entrant on vérifie ce que Mastodon envoie réellement.
API proposée
Module rustwarden_core::fediverse::http_signatures
/// Verifier production : valide le header Signature draft-cavage. pub struct HttpSigVerifier { /// Resolver pour récupérer le PEM public de l'acteur signataire /// (via WebFinger + cache `remote_identities` de RFC 0003). fetcher: Arc<dyn RemoteFetcher>, } impl HttpSigVerifier { pub fn new(fetcher: Arc<dyn RemoteFetcher>) -> Self; } #[async_trait] impl SignatureVerifier for HttpSigVerifier { async fn verify( &self, actor_ap_id: &str, signature_header: &str, body: &[u8], public_key_pem: &str, ) -> Result<(), AppError>; } /// Delivery production : signe + POST l'activité. pub struct HttpSignedDelivery { db: DatabaseConnection, http: reqwest::Client, base_url: String, } impl HttpSignedDelivery { pub fn new(db: DatabaseConnection, base_url: String) -> Self; } #[async_trait] impl OutboundDelivery for HttpSignedDelivery { async fn deliver( &self, item: &fediverse_outbound_queue::Model, ) -> Result<(), AppError>; }
Format Signature header (draft-cavage)
Signature: keyId="https://a.tld/users/alice#main-key", algorithm="rsa-sha256", headers="(request-target) host date digest", signature="<base64>"
Algorithme de vérification
- Parser le header en map
key=value(gestion des quotes). - Vérifier
algorithm == "rsa-sha256"(refuserhs2019pour cette livraison — extension future). - Reconstituer la signature input string à partir de
headers(ex.(request-target): post /inbox\nhost: b.tld\ndate: …). - Décoder le PEM public depuis
public_key_pem(passé par le caller, qui l'a obtenu viaRemoteIdentityService::resolve(actor_ap_id)). - Appeler
RsaPublicKey::verify(Pkcs1v15Sign::new::<Sha256>(), &input, &signature_bytes). - Mapper succès →
Ok(()), échec →AppError::Internal("HTTP signature mismatch").
Algorithme de signature sortante
- Charger le local actor (
actor_user_iddu row queue) via SeaORM. - Récupérer sa paire RSA via
ActorKeyService::ensure_keys(db, user_id)(RFC 0005). - Construire la signature input string avec
(request-target) host date. - Calculer SHA-256, signer via
RsaPrivateKey::sign(Pkcs1v15Sign::new::<Sha256>(), &hash). - POSTer le body avec les headers Signature / Date / Host.
Analyse de compatibilité
- Non-breaking : 2 nouveaux types publics qui s'enfichent sur les traits existants.
-
Pas de nouvelle dépendance majeure (
rsadéjà tiré par RFC 0005). -
reqwestactivé par la featureoauth; on peut soit le rendre optionnel pourexperimental-fediverse, soit imposer la dépendance commune. Décision :reqwestest ajouté àexperimental-fediversedeps (déjà optionnel). - Pas de schéma DB.
Alternatives considérées
Alternative A (recommandée) — Implémentation maison sur rsa + sha2
Pros : zéro nouvelle dep ; contrôle total ; Mastodon-compat.
Cons : ~250 lignes de parsing HTTP-Sig à maintenir.
Alternative B — Crate httpsig-rs ou équivalent
Cons : aucun crate Rust mature ne supporte draft-cavage et RFC 9421 en parallèle aujourd'hui ; risque d'abandon ; on ajoute une dep externe pour 250 lignes.
Rejeté.
Alternative C — Bloquer toute interop tant que Mastodon migre vers RFC 9421
Cons : interop = 0 pendant 1-2 ans.
Rejeté.
Alternative D — Ne rien faire (rester sur AlwaysAccept + Noop)
Cons : pas de prod possible.
Rejeté.
Testing plan
12 tests :
Pure (parsing + canonical string) :
parse_signature_header_extracts_keyid_algorithm_headers_signature.parse_signature_header_rejects_missing_required_fields.build_signature_input_uses_lowercase_header_names.build_signature_input_handles_request_target.
Crypto round-trip :
5. sign_then_verify_round_trip — génère une paire, signe, vérifie : Ok.
6. verify_rejects_tampered_signature.
7. verify_rejects_wrong_public_key.
Verifier integration :
8. verifier_rejects_when_algorithm_not_rsa_sha256.
9. verifier_rejects_when_signature_invalid_base64.
10. verifier_accepts_well_formed_signature_with_test_key.
Delivery (mocked) :
11. delivery_skips_signing_when_actor_user_id_none — NoopDelivery-like fallback ; pas d'erreur silencieuse, retourne explicitement Err(Config).
12. delivery_returns_err_when_actor_keys_missing.
Pas de test contre reqwest réel : la livraison HTTP est testée via un mock-server dédié dans un follow-up tests/interop/ (Phase 5).
Migration guide
### Added (rustwarden-core, feature `experimental-fediverse`) - `rustwarden_core::fediverse::http_signatures::{HttpSigVerifier, HttpSignedDelivery}`. - `reqwest` ajouté aux deps gated par `experimental-fediverse` (déjà optionnel pour la feature `oauth`). ### Migration steps 1. Apps fediverse en dev : changement zéro ; `AlwaysAcceptVerifier` + `NoopDelivery` continuent de fonctionner. 2. Apps en prod : remplacer ```rust .with_fediverse()? // garde AlwaysAccept + Noop
par :
.with_fediverse()? .with_signature_verifier(Arc::new(HttpSigVerifier::new(fetcher))) .with_delivery(Arc::new(HttpSignedDelivery::new(db, base_url)))
(les méthodes with_signature_verifier / with_delivery sont
ajoutées par cette RFC sur RustwardenBuilder).
## Open questions Toutes résolues à l'auto-revue : - [x] **Tolerance horloge `Date` header** : 1 heure (Mastodon-compat). - [x] **Refus de `hs2019`** : oui, le draft-cavage moderne. Si Mastodon migre, follow-up RFC. - [x] **Caching de la clé publique** : déléguée à `RemoteIdentityService` qui a déjà un TTL. ## Tracking issue À ouvrir au moment de la PR. --- ## Score d'évaluation | Critère | Note (1-5) | Commentaire | |---|---|---| | Généralité | 4 | Trait pluggable, donc impl HTTP-Sig réelle facilement remplacée par RFC 9421. | | Stabilité API | 4 | Additif, gated. | | Performance | 3 | RSA-SHA256 côté hot path entrant ; cache key public via RFC 0003 limite. | | Sécurité | 5 | Le seul vrai déblocage prod ; constant-time signature via `rsa`/`subtle`. | | Complexité d'implémentation | 3 | ~600 lignes (parsing + canonical + sign + verify + 12 tests). | | Maintenance | 4 | Pas de nouvelle dep, mais format à mettre à jour si/quand Mastodon migre. | | Documentation | 4 | Rustdoc EN, doctest, CHANGELOG, migration guide. | | **Total** | **27/35** | ≥ 25 → Accepted |
GitRust