RFC 0016 — Fediverse: élargissement SignatureVerifier + HttpSignedDelivery complet
- 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 fediverse en production qui veut activer la vérification HTTP-Sig réelle entrante et la livraison signée sortante.
- Plan associé : Phase 5 — production. Suite directe de RFC 0015.
- Dépendances : RFC 0010 (
SignatureVerifier), RFC 0011 (OutboundDelivery), RFC 0015 (primitifs HTTP-Sig).
Summary
Deux changements techniquement liés :
-
Élargir le trait
SignatureVerifier: la signature actuelle ne porte que(actor_ap_id, signature_header, body, public_key_pem). La vérification draft-cavage exige aussi method, path et leHeaderMap(host, date, digest, etc.). Nouvelle signature :async fn verify(&self, ctx: &VerifyCtx<'_>) -> Result<(), AppError>;
où
VerifyCtxcontient les champs nécessaires. Les impls existantes (AlwaysAcceptVerifier, mocks de tests) sont mises à jour. Les handlersinbox::handleetauth_routes::auth_inboxconsomment désormais le contexte complet. -
Livrer
HttpSignedDelivery— implOutboundDeliveryqui utilisereqwest::Client+rsa_sha256_sign(RFC 0015) pour signer puis POSTer chaque activité. Branchable surRustwardenBuilderviawith_delivery(Arc::new(HttpSignedDelivery::new(...))).
Motivation
Sans l'élargissement, parse_and_verify_cavage (RFC 0015) reste inutilisable depuis les inbox handlers — qui n'ont pas accès au HeaderMap à travers le trait. Sans HttpSignedDelivery, les activités sortantes restent en NoopDelivery.
Cette RFC est le dernier maillon avant que rustwarden parle réellement avec Mastodon.
Non-goals
- Pas de support
Digestbody header dans cette RFC — Mastodon le set en sortant mais ne l'exige pas en entrant pour notre usage initial. Follow-up. - Pas de retry HTTP-spécifique (4xx vs 5xx,
Retry-After) — RFC 0010 retry ladder s'applique. - Pas de pool de connexion HTTP custom —
reqwest::Clientpar défaut. - Pas de migration des callers
verify(actor, sig, body, pem)vers la nouvelle signature au-delà des sites internes ; les apps consommatrices qui implémentaient leurSignatureVerifierdoivent migrer.
API proposée
Nouvelle struct VerifyCtx
/// Contexte d'une requête entrante à vérifier (RFC 0016). pub struct VerifyCtx<'a> { /// AP id de l'actor signataire (extrait du body). pub actor_ap_id: &'a str, /// HTTP method en lowercase (`"post"`). pub method: &'a str, /// Path-and-query (`"/inbox"`, `"/fediverse/auth/inbox"`). pub path: &'a str, /// Tous les headers de la requête (lookup case-insensitive). pub headers: &'a axum::http::HeaderMap, /// Body brut (pour digest/audit). pub body: &'a [u8], /// PEM public résolu pour `actor_ap_id` (passé par le caller — /// laissé vide quand le resolveur est délégué à l'impl du trait). pub public_key_pem: &'a str, }
Trait SignatureVerifier v2
#[async_trait] pub trait SignatureVerifier: Send + Sync { async fn verify(&self, ctx: &VerifyCtx<'_>) -> Result<(), AppError>; }
Impls livrées
AlwaysAcceptVerifier— refactor trivial.HttpSigVerifier— nouvelle impl. Litctx.headers["signature"], construitrequest_target = format!("{} {}", ctx.method, ctx.path), appelleparse_and_verify_cavage(...). Quandpublic_key_pemest vide, l'impl peut consulterRemoteIdentityService(mais cette RFC reste minimaliste : si vide →Err(Validation("public key not resolved"))).
HttpSignedDelivery
pub struct HttpSignedDelivery { db: DatabaseConnection, http: reqwest::Client, base_url: String, } #[async_trait] impl OutboundDelivery for HttpSignedDelivery { async fn deliver(&self, item: &fediverse_outbound_queue::Model) -> Result<(), AppError>; }
Algorithme :
- Si
item.actor_user_idestNone→Err(Config("HttpSignedDelivery requires actor_user_id")). - Charger la paire RSA via
ActorKeyService::ensure_keys(db, actor_user_id). - Construire
(request-target) host date+ signer. - POST
item.payload_jsonàitem.target_inboxavec headersDate,Host,Signature,Content-Type: application/activity+json. 2xx→Ok(()). Tout le reste →Err(Internal("HTTP <status>")). La logique retry/backoff de RFC 0010 reprend depuisdrain_one.
Analyse de compatibilité
-
Breaking au niveau du trait
SignatureVerifier— toute app consommatrice qui a implémenté son propre verifier doit migrer. Mitigation :experimental-fediversegating + l'API était déjà labellisée expérimentale dans RFC 0010. - Pas de schéma DB.
-
HttpSignedDeliveryest purement additif.
Alternatives considérées
Alternative A (recommandée) — Trait v2 + HttpSignedDelivery en une RFC
Pros : cohérence ; one-shot pour la prod ; les deux changements partagent le contexte.
Cons : breaking pour le trait. Mitigation par feature gating.
Alternative B — Garder le trait v1, ajouter une seconde méthode verify_with_request
Cons : surface API plus large à maintenir, deux chemins de vérif divergents.
Rejeté.
Alternative C — Ne rien faire (différer encore)
Cons : interop Mastodon impossible.
Rejeté.
Testing plan
8 tests :
Trait migration :
always_accept_v2_accepts_any_ctx.http_sig_verifier_returns_err_when_signature_header_missing.http_sig_verifier_returns_err_when_public_key_pem_empty.http_sig_verifier_accepts_valid_signature— round-trip avec une paire générée.
Inbox/auth integration :
5. inbox_handle_uses_widened_verifier — smoke test que les handlers passent bien VerifyCtx.
6. auth_inbox_uses_widened_verifier.
HttpSignedDelivery (mocked) :
7. delivery_returns_err_when_actor_user_id_none.
8. delivery_signs_input_string_with_actor_keys — pas d'envoi HTTP réel ; on intercepte la signature via une impl test du reqwest::Client … en pratique on extrait la primitive sign pour la tester séparément (déjà couvert par RFC 0015).
Note : un test d'intégration end-to-end avec un fake HTTP server arrivera dans un follow-up tests/interop/.
Migration guide
### Changed (rustwarden-core, feature `experimental-fediverse`) - **Breaking** : `SignatureVerifier::verify(...)` change de signature. Avant : ```rust async fn verify(&self, actor_ap_id: &str, sig: &str, body: &[u8], pem: &str) -> Result<(), AppError>;
Après :
async fn verify(&self, ctx: &VerifyCtx<'_>) -> Result<(), AppError>;
- Les handlers
inbox::handleetauth_routes::auth_inboxreconstituentVerifyCtxà partir duRequest.
Added
rustwarden_core::services::fediverse_signal_service::VerifyCtx.rustwarden_core::fediverse::http_signatures::HttpSigVerifier.rustwarden_core::fediverse::http_signatures::HttpSignedDelivery.reqwestajouté àexperimental-fediversedependencies.
Migration steps pour les apps consommatrices
- Mettre à jour les impls custom de
SignatureVerifier:// Avant async fn verify(&self, actor: &str, sig: &str, body: &[u8], pem: &str) { … } // Après async fn verify(&self, ctx: &VerifyCtx<'_>) { … }
- Activer la production-grade verification :
builder.with_signature_verifier(Arc::new(HttpSigVerifier::default()))
- Activer la livraison signée :
builder.with_delivery(Arc::new(HttpSignedDelivery::new(db, base_url)))
## Open questions Toutes résolues à l'auto-revue : - [x] **Tolerance horloge `Date`** : 1 h, contrôlée par `HttpSigVerifier::clock_skew_secs` (paramétrable, 3600 par défaut). - [x] **`actor_user_id` obligatoire pour HttpSignedDelivery** : oui, `Err(Config)` sinon. Les apps qui veulent que des activités non-attribuées soient livrées doivent piocher un actor system par défaut. - [x] **`reqwest` partagé avec `oauth`** : oui, on ajoute `reqwest` à `experimental-fediverse` ; pas de duplication de version. ## Tracking issue À ouvrir au moment de la PR. --- ## Score d'évaluation | Critère | Note (1-5) | Commentaire | |---|---|---| | Généralité | 4 | `VerifyCtx` réutilisable pour autres protocoles signés. | | Stabilité API | 3 | Breaking sur le trait, mitigé par gating expérimental. | | Performance | 4 | Une signature RSA par activité — coût attendu de la fédération. | | Sécurité | 5 | C'est **le** déblocage prod ; la TLS est gérée par `reqwest`. | | Complexité d'implémentation | 3 | ~700 lignes (struct + 2 impls + 8 tests). | | Maintenance | 4 | Pas de nouvelle dep majeure. | | Documentation | 4 | Rustdoc EN, doctest, CHANGELOG, migration guide complet. | | **Total** | **27/35** | ≥ 25 → Accepted |
GitRust