Skip to main content

ojak/federation/
signer.rs

1//! Who signed a GET: verified only when a dispatcher asks.
2//!
3//! The checks are the inbox's, for a request without a body: the signature's
4//! shape, that it was made for this host and recently, that the key is one its
5//! actor publishes, and the signature against the key. Keys are kept in the
6//! key-value store, the cache the inbox will share, and a key that no longer
7//! verifies is fetched again once, since its actor may have rotated it.
8
9use super::{BoxFuture, Context, DynKv, Error, authority};
10use crate::fetch::Fetcher;
11use crate::sig::SenderKey;
12use crate::sig::verification::{self, Policy, PublishedKey, Request};
13use serde_json::{Value, json};
14use std::sync::Arc;
15use std::time::Duration;
16use url::Url;
17
18type KeyFn<D> =
19    Arc<dyn Fn(Context<D>) -> BoxFuture<'static, Result<Option<SenderKey>, Error>> + Send + Sync>;
20
21pub(super) type FetcherFn<D> = Arc<dyn Fn(&D) -> Arc<Fetcher> + Send + Sync>;
22pub(super) type KvFn<D> = Arc<dyn Fn(&D) -> Arc<dyn DynKv> + Send + Sync>;
23pub(super) type KeyFetchedFn<D> =
24    Arc<dyn Fn(Context<D>, Value) -> BoxFuture<'static, ()> + Send + Sync>;
25pub(super) type KnownKeyFn<D> = Arc<
26    dyn Fn(Context<D>, String) -> BoxFuture<'static, Result<Option<KnownKey>, Error>> + Send + Sync,
27>;
28
29pub(super) struct SignedFetch<D> {
30    pub(super) fetcher: Arc<Fetcher>,
31    pub(super) kv: Arc<dyn DynKv>,
32    pub(super) key_ttl: Duration,
33    pub(super) key: KeyFn<D>,
34    pub(super) fetcher_for: Option<FetcherFn<D>>,
35    pub(super) kv_for: Option<KvFn<D>>,
36    pub(super) known_key: Option<KnownKeyFn<D>>,
37    pub(super) key_fetched: Option<KeyFetchedFn<D>>,
38}
39
40impl<D> SignedFetch<D> {
41    /// The fetcher for a request's data.
42    pub(super) fn fetcher(&self, data: &D) -> Arc<Fetcher> {
43        match &self.fetcher_for {
44            Some(fetcher) => fetcher(data),
45            None => self.fetcher.clone(),
46        }
47    }
48
49    /// The key-value store for a request's data.
50    pub(super) fn kv(&self, data: &D) -> Arc<dyn DynKv> {
51        match &self.kv_for {
52            Some(kv) => kv(data),
53            None => self.kv.clone(),
54        }
55    }
56}
57
58/// A key the application already holds for a key ID: its PEM, and the actor
59/// that published it.
60#[derive(Clone, Debug)]
61pub struct KnownKey {
62    pub pem: String,
63    pub actor: Url,
64}
65
66/// A key, as it is cached: the key and the actor that publishes it, and
67/// the actor's document when it was just fetched.
68struct Published {
69    key: PublishedKey,
70    actor: Url,
71    document: Option<Value>,
72}
73
74impl Published {
75    fn to_json(&self) -> Value {
76        match &self.key {
77            PublishedKey::RsaPem(pem) => json!({"pem": pem, "actor": self.actor.as_str()}),
78            PublishedKey::Ed25519(bytes) => json!({
79                "ed25519": base64::Engine::encode(&base64::engine::general_purpose::STANDARD, bytes),
80                "actor": self.actor.as_str(),
81            }),
82        }
83    }
84
85    fn from_json(value: &Value) -> Option<Self> {
86        let key = match (value.get("pem"), value.get("ed25519")) {
87            (Some(pem), _) => PublishedKey::RsaPem(pem.as_str()?.to_owned()),
88            (None, Some(bytes)) => PublishedKey::Ed25519(
89                base64::Engine::decode(&base64::engine::general_purpose::STANDARD, bytes.as_str()?)
90                    .ok()?
91                    .try_into()
92                    .ok()?,
93            ),
94            (None, None) => return None,
95        };
96        Some(Self {
97            key,
98            actor: Url::parse(value.get("actor")?.as_str()?).ok()?,
99            document: None,
100        })
101    }
102}
103
104/// How a GET was signed, as [`Context::signing`] tells it.
105#[derive(Clone, Debug, PartialEq, Eq)]
106pub enum Signing {
107    /// Not signed.
108    Unsigned,
109    /// Signed by this actor, whose published key verifies the signature.
110    Verified(Url),
111    /// Signed with a key on this host, which [`super::Builder::blocked`]
112    /// refuses: nothing was fetched from it, and nothing verified.
113    Blocked(String),
114    /// Signed, but the signature does not hold, for this reason.
115    Invalid(String),
116}
117
118pub(super) async fn verify<D: Clone + Send + Sync + 'static>(context: &Context<D>) -> Signing {
119    let signed = context.inner.request.as_ref().is_some_and(|info| {
120        info.headers
121            .iter()
122            .any(|(name, _)| name.eq_ignore_ascii_case("signature"))
123    });
124    if !signed {
125        return Signing::Unsigned;
126    }
127    match authenticate_signed(context, b"").await {
128        Ok(signer) => Signing::Verified(signer),
129        Err(Refusal::Blocked(host)) => Signing::Blocked(host),
130        Err(Refusal::Invalid(why)) => Signing::Invalid(why),
131    }
132}
133
134/// Why a signature was not taken.
135enum Refusal {
136    /// Its key is on a blocked host.
137    Blocked(String),
138    Invalid(String),
139}
140
141impl From<String> for Refusal {
142    fn from(why: String) -> Self {
143        Self::Invalid(why)
144    }
145}
146
147impl From<&str> for Refusal {
148    fn from(why: &str) -> Self {
149        Self::Invalid(why.to_owned())
150    }
151}
152
153/// Who signed the request `context` was made for, carrying `body`: the actor
154/// whose published key the signature verifies with, after the signature has
155/// been held to the policy. Why not, when it is not.
156pub(super) async fn authenticate<D: Clone + Send + Sync + 'static>(
157    context: &Context<D>,
158    body: &[u8],
159) -> Result<Url, String> {
160    authenticate_signed(context, body)
161        .await
162        .map_err(|refusal| match refusal {
163            Refusal::Blocked(host) => format!("signed with a key on {host}, which is blocked"),
164            Refusal::Invalid(why) => why,
165        })
166}
167
168/// The key id the request `context` was made for is signed with, when its key
169/// is on a host [`super::Builder::blocked`] refuses: `Ok(None)` for a request
170/// that is unsigned, malformed or signed from elsewhere, which the rest of
171/// the checks then take up.
172///
173/// Mastodon's `SignatureVerification#keypair_from_key_id` asks this first of
174/// any signed request, and refuses one from such a host with 403 before it
175/// looks for the key.
176pub(super) async fn blocked_key_id<D: Clone + Send + Sync + 'static>(
177    context: &Context<D>,
178    body: &[u8],
179) -> Result<Option<String>, Error> {
180    let Some(blocked) = &context.inner.federation.blocked else {
181        return Ok(None);
182    };
183    let Some(info) = context.inner.request.as_ref() else {
184        return Ok(None);
185    };
186    let headers: Vec<(&str, &str)> = info
187        .headers
188        .iter()
189        .map(|(name, value)| (name.as_str(), value.as_str()))
190        .collect();
191    let request = Request {
192        method: &info.method,
193        path_and_query: &info.path_and_query,
194        headers: &headers,
195        body,
196    };
197    let Ok(signature) = verification::parse(&request) else {
198        return Ok(None);
199    };
200    let Some(host) = Url::parse(&signature.key_id)
201        .ok()
202        .and_then(|key| key.host_str().map(str::to_owned))
203    else {
204        return Ok(None);
205    };
206    Ok(blocked(context.clone(), host)
207        .await?
208        .then_some(signature.key_id))
209}
210
211async fn authenticate_signed<D: Clone + Send + Sync + 'static>(
212    context: &Context<D>,
213    body: &[u8],
214) -> Result<Url, Refusal> {
215    let settings = context
216        .inner
217        .federation
218        .signed_fetch
219        .as_ref()
220        .ok_or("signed fetches are not configured")?;
221    let info = context.inner.request.as_ref().ok_or("no request")?;
222    let headers: Vec<(&str, &str)> = info
223        .headers
224        .iter()
225        .map(|(name, value)| (name.as_str(), value.as_str()))
226        .collect();
227    let request = Request {
228        method: &info.method,
229        path_and_query: &info.path_and_query,
230        headers: &headers,
231        body,
232    };
233    let signature = verification::parse(&request).map_err(|error| error.to_string())?;
234    // A key on a blocked server is neither looked for nor fetched, as the
235    // inbox fetches nothing for an activity from one.
236    if let (Some(blocked), Some(host)) = (
237        &context.inner.federation.blocked,
238        Url::parse(&signature.key_id)
239            .ok()
240            .and_then(|key| key.host_str().map(str::to_owned)),
241    ) {
242        match blocked(context.clone(), host.clone()).await {
243            Ok(true) => return Err(Refusal::Blocked(host)),
244            Ok(false) => {}
245            Err(error) => {
246                context.report(&error);
247                return Err(format!("could not tell whether {host} is blocked").into());
248            }
249        }
250    }
251    let canonical = authority(context.origin());
252    let hosts = [info.host.as_str(), canonical.as_str()];
253    verification::check(
254        &signature,
255        &request,
256        &Policy::new(&hosts),
257        chrono::Utc::now().timestamp(),
258    )
259    .map_err(|error| error.to_string())?;
260
261    find_key(context, settings, &signature.key_id, |key| {
262        verification::verify(&signature, &request, key.as_key()).is_ok()
263    })
264    .await
265    .map_err(Refusal::from)
266}
267
268/// The actor publishing `key_id`, when the key passes `check`: the copy the
269/// application holds first, then a cached one, then one fetched from its
270/// actor, since a key that no longer passes may have been rotated.
271async fn find_key<D: Clone + Send + Sync + 'static>(
272    context: &Context<D>,
273    settings: &SignedFetch<D>,
274    key_id: &str,
275    check: impl Fn(&PublishedKey) -> bool,
276) -> Result<Url, String> {
277    // A key the application holds, as eunha holds remote accounts' keys, is
278    // tried first; one that does not verify is fetched fresh below.
279    if let Some(known) = &settings.known_key {
280        match known(context.clone(), key_id.to_owned()).await {
281            Ok(Some(known)) => {
282                if check(&PublishedKey::RsaPem(known.pem)) {
283                    return Ok(known.actor);
284                }
285            }
286            Ok(None) => {}
287            Err(error) => context.report(&error),
288        }
289    }
290
291    let kv = settings.kv(context.data());
292    let cache_key = ["ojak", "key", key_id];
293    match kv.get(&cache_key).await {
294        Ok(Some(cached)) => {
295            if let Some(published) = Published::from_json(&cached)
296                && check(&published.key)
297            {
298                return Ok(published.actor);
299            }
300        }
301        Ok(None) => {}
302        Err(error) => context.report(&Error::from(error)),
303    }
304
305    let published = fetch_key(context, settings, key_id)
306        .await
307        .ok_or_else(|| format!("no key {key_id} published by its actor"))?;
308    let verified = check(&published.key);
309    // A verified key that came with its actor's document, handed to an
310    // application that stores actors and gives their keys back through
311    // `known_key`, is the application's to keep. Caching it here as well held
312    // every new actor's key twice, and in memory for `key_ttl`. A key that
313    // does not verify is still cached, so that a run of forged requests does
314    // not fetch it again for each one.
315    let kept_by_application = verified
316        && settings.known_key.is_some()
317        && settings.key_fetched.is_some()
318        && published.document.is_some();
319    if !kept_by_application
320        && let Err(error) = kv
321            .set(&cache_key, published.to_json(), Some(settings.key_ttl))
322            .await
323    {
324        context.report(&Error::from(error));
325    }
326    if verified {
327        // The actor's document, fetched for its key and established as served
328        // from its own origin: an application that stores actors stores this
329        // one now rather than fetching it again.
330        if let (Some(hook), Some(document)) = (&settings.key_fetched, published.document) {
331            hook(context.clone(), document).await;
332        }
333        Ok(published.actor)
334    } else {
335        Err("the signature does not verify".into())
336    }
337}
338
339/// Who the Linked Data Signature (`RsaSignature2017`) on `document` says
340/// made it: the actor `actor`, when the signature verifies with a key `actor`
341/// publishes. Mastodon's `actor_from_verified_ld_signature`, for an activity
342/// passed on by a server other than its actor's.
343///
344/// With it comes the registry the signature was checked over when that is
345/// not ojak's own: one holding the contexts the application loaded for it
346/// ([`super::Builder::remote_contexts`]).
347pub(super) async fn verify_linked_data<D: Clone + Send + Sync + 'static>(
348    context: &Context<D>,
349    document: &Value,
350    actor: &str,
351) -> Result<(Url, Option<ojak_jsonld::Registry>), String> {
352    use crate::sig::linked_data::Signed;
353
354    let settings = context
355        .inner
356        .federation
357        .signed_fetch
358        .as_ref()
359        .ok_or("signed fetches are not configured")?;
360    let Some(creator) = crate::sig::linked_data::creator(document) else {
361        return Err("no RsaSignature2017".into());
362    };
363    // A key on a blocked server is neither looked for nor fetched, as
364    // Mastodon checks `domain_not_allowed?` on the signature's creator.
365    if let (Some(blocked), Some(host)) = (
366        &context.inner.federation.blocked,
367        Url::parse(creator)
368            .ok()
369            .and_then(|key| key.host_str().map(str::to_owned)),
370    ) {
371        match blocked(context.clone(), host.clone()).await {
372            Ok(true) => return Err(format!("signed with a key on {host}, which is blocked")),
373            Ok(false) => {}
374            Err(error) => {
375                context.report(&error);
376                return Err(format!("could not tell whether {host} is blocked"));
377            }
378        }
379    }
380    // Contexts ojak does not ship are loaded, as Mastodon's document loader
381    // fetches them, only for a creator on a server that is not blocked.
382    let registry = match &context.inner.federation.remote_contexts {
383        Some(remote) => {
384            crate::contexts::resolve(&crate::fetch::REGISTRY, document, remote.limits, |iri| {
385                (remote.load)(context.clone(), iri)
386            })
387            .await
388            .map_err(|error| error.to_string())?
389        }
390        None => None,
391    };
392    let signed = Signed::read(
393        registry.as_ref().unwrap_or(&crate::fetch::REGISTRY),
394        document,
395        chrono::Utc::now().timestamp(),
396    )
397    .map_err(|error| error.to_string())?;
398    let signer = find_key(context, settings, signed.creator(), |key| match key {
399        PublishedKey::RsaPem(pem) => signed.verify(pem).is_ok(),
400        PublishedKey::Ed25519(_) => false,
401    })
402    .await?;
403    if signer.as_str() != actor {
404        return Err(format!("signed by {signer}, not {actor}"));
405    }
406    Ok((signer, registry))
407}
408
409/// Who an FEP-8b32 integrity proof on `document` says made it: the actor
410/// `actor`, when the proof verifies with a key the actor lists as an
411/// `assertionMethod` and that key is on the actor's origin.
412pub(super) async fn prove<D: Clone + Send + Sync + 'static>(
413    context: &Context<D>,
414    document: &Value,
415    actor: &str,
416) -> Result<Url, String> {
417    use crate::sig::integrity;
418
419    let settings = context
420        .inner
421        .federation
422        .signed_fetch
423        .as_ref()
424        .ok_or("signed fetches are not configured")?;
425    // Every proof by a key on the actor's origin is tried, in order; a
426    // document may carry one per suite, or one by a key its actor no longer
427    // lists beside one by the key it does.
428    let proofs: Vec<_> = integrity::integrity_proofs(document, chrono::Utc::now().timestamp())
429        .into_iter()
430        .filter(|(_, _, method)| crate::origin::same_origin(method, actor))
431        .collect();
432    if proofs.is_empty() {
433        return Err(format!(
434            "no usable integrity proof by a key on {actor}'s origin"
435        ));
436    }
437    let actor_url = Url::parse(actor).map_err(|error| error.to_string())?;
438    let key = match (settings.key)(context.clone()).await {
439        Ok(key) => key,
440        Err(error) => {
441            context.report(&error);
442            None
443        }
444    };
445    let fetched = settings
446        .fetcher(context.data())
447        .document(&actor_url, key.as_ref())
448        .await
449        .map_err(|error| error.to_string())?;
450    let mut why = String::new();
451    for (proof, _, method) in proofs {
452        let Some(multibase) = assertion_method(&fetched.json, &method) else {
453            why = format!("{actor} does not list {method} as an assertionMethod");
454            continue;
455        };
456        let verified = integrity::decode_multikey(&multibase).and_then(|public_key| {
457            integrity::verify_object_integrity_proof(document, &proof, &public_key)
458        });
459        match verified {
460            Ok(()) => return Url::parse(&fetched.id).map_err(|error| error.to_string()),
461            Err(error) => why = error.to_string(),
462        }
463    }
464    Err(why)
465}
466
467/// Fetch an activity forwarded by a server other than its actor's from
468/// where its `id` says it lives, and establish it there: served from its own
469/// origin, by the actor the forwarded copy named. What comes back is the
470/// activity to process, and its actor; the forwarded copy was only a claim.
471pub(super) async fn establish<D: Clone + Send + Sync + 'static>(
472    context: &Context<D>,
473    document: &Value,
474    actor: &str,
475) -> Result<(Value, Url), String> {
476    let settings = context
477        .inner
478        .federation
479        .signed_fetch
480        .as_ref()
481        .ok_or("signed fetches are not configured")?;
482    let id = document
483        .get("id")
484        .and_then(Value::as_str)
485        .ok_or("a forwarded activity with no id cannot be fetched")?;
486    let url = Url::parse(id).map_err(|error| format!("{id}: {error}"))?;
487    if !matches!(url.scheme(), "http" | "https") {
488        return Err(format!("{id} cannot be fetched from its origin"));
489    }
490    let key = match (settings.key)(context.clone()).await {
491        Ok(key) => key,
492        Err(error) => {
493            context.report(&error);
494            None
495        }
496    };
497    let fetched = settings
498        .fetcher(context.data())
499        .document(&url, key.as_ref())
500        .await
501        .map_err(|error| format!("{id}: {error}"))?;
502    if fetched.id != id {
503        return Err(format!("{id} is served as {}", fetched.id));
504    }
505    if super::inbox::actor_of(&fetched.json) != Some(actor) {
506        return Err(format!("{id} as its origin serves it is not by {actor}"));
507    }
508    let actor = Url::parse(actor).map_err(|error| error.to_string())?;
509    Ok((fetched.json, actor))
510}
511
512/// The `publicKeyMultibase` of the `assertionMethod` `method` an actor lists.
513fn assertion_method(actor: &Value, method: &str) -> Option<String> {
514    let methods = match actor.get("assertionMethod")? {
515        Value::Array(items) => items.iter().collect(),
516        item => vec![item],
517    };
518    methods.into_iter().find_map(|item| {
519        (item.get("id").and_then(Value::as_str) == Some(method))
520            .then(|| item.get("publicKeyMultibase")?.as_str().map(str::to_owned))
521            .flatten()
522    })
523}
524
525/// Fetch the actor the key ID points at, and read the key from it if the
526/// actor publishes it as its own.
527async fn fetch_key<D: Clone + Send + Sync + 'static>(
528    context: &Context<D>,
529    settings: &SignedFetch<D>,
530    key_id: &str,
531) -> Option<Published> {
532    let owner = Url::parse(verification::key_owner(key_id)).ok()?;
533    let key = match (settings.key)(context.clone()).await {
534        Ok(key) => key,
535        Err(error) => {
536            context.report(&error);
537            None
538        }
539    };
540    let document = settings
541        .fetcher(context.data())
542        .document(&owner, key.as_ref())
543        .await
544        .ok()?;
545    let key = verification::published_key(&document.json, key_id)?;
546    Some(Published {
547        key,
548        actor: Url::parse(&document.id).ok()?,
549        document: Some(document.json),
550    })
551}