From 564771931f64dfbac3f8a800a1978a31dc715944 Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Thu, 2 May 2024 19:29:32 +0200 Subject: [PATCH] Major refactor * Reorganize the fetch component * Organize the server code a little more * Move verification to the server and clean it up * Improve the error handling around the fetch code --- bin/pupctl/src/main.rs | 13 +- bin/server/src/api.rs | 386 +++++++++++++++++++++++++++++++----- bin/server/src/main.rs | 238 ++-------------------- bin/server/src/sig.rs | 149 ++++++++++++++ lib/fetch/src/client.rs | 166 ++++++++++++++++ lib/fetch/src/lib.rs | 230 +++++++-------------- lib/fetch/src/object.rs | 100 ++++++++++ lib/fetch/src/signatures.rs | 24 ++- lib/puppy/src/context.rs | 24 +-- lib/puppy/src/lib.rs | 141 +------------ 10 files changed, 878 insertions(+), 593 deletions(-) create mode 100644 bin/server/src/sig.rs create mode 100644 lib/fetch/src/client.rs create mode 100644 lib/fetch/src/object.rs diff --git a/bin/pupctl/src/main.rs b/bin/pupctl/src/main.rs index 85761d9..7ba95ba 100644 --- a/bin/pupctl/src/main.rs +++ b/bin/pupctl/src/main.rs @@ -1,11 +1,12 @@ +//! Control program for the ActivityPub federated social media server. #![feature(iterator_try_collect)] + use puppy::{ actor::Actor, - auth::Verifier, config::Config, - data::{schema, Bite, Profile}, + data::{Bite, Profile}, post::Author, - store::{util::IterExt as _, Store}, + store::util::IterExt as _, Context, }; @@ -17,9 +18,8 @@ fn main() -> puppy::Result<()> { state_dir: ".state".to_string(), port: 1312, }; - let verifier = Verifier::load(&config); - let db = Store::open(&config.state_dir, schema())?; - let cx = Context::new(config, db.clone(), verifier); + let cx = Context::load(config)?; + let db = cx.store(); println!("creating actors"); let riley = get_or_create_actor(&cx, "riley")?; let linen = get_or_create_actor(&cx, "linen")?; @@ -31,7 +31,6 @@ fn main() -> puppy::Result<()> { if true { println!("making riley follow linen"); - cx.run(|tx| { if !riley.follows(&tx, &linen)? { println!("follow relation does not exist yet"); diff --git a/bin/server/src/api.rs b/bin/server/src/api.rs index 8359192..4c88321 100644 --- a/bin/server/src/api.rs +++ b/bin/server/src/api.rs @@ -1,5 +1,23 @@ //! API endpoints and request handlers. +use std::convert::Infallible; +use std::net::SocketAddr; +use std::sync::Arc; + +use http_body_util::{BodyExt as _, Full}; +use hyper::body::Bytes; +use hyper::server::conn::http1; +use hyper::service::service_fn; +use hyper_util::rt::TokioIo; +use hyper::Method; +use puppy::Context; +use serde_json::{from_slice, json, Value}; +use tokio::net::TcpListener; + +use crate::sig::{Signer, Verdict, Verifier, VERIFIER_MOUNT}; + +use self::error::Message; + pub mod ap { //! ActivityPub handlers. @@ -7,14 +25,17 @@ pub mod ap { use hyper::body::Bytes; use puppy::{ actor::Actor, - auth::{Signer, Verifier}, - config::Config, data::{Id, PrivateKey, PublicKey}, - fetch::{signatures::Private, SigningKey}, + fetch::signatures::{Private, SigningKey}, get_local_ap_object, Context, Key, }; use serde_json::{to_string, Value}; - use crate::{respond, Response}; + + use crate::sig::{Signer, Verifier}; + use super::{ + error::{self, Message}, + respond, Response, + }; /// Proxy a request through the instance. pub async fn proxy(cx: &Context, params: &[(&str, &str)]) -> Response { @@ -53,23 +74,26 @@ pub mod ap { } /// Serve an ActivityPub object as json-ld. - pub fn serve_object(cx: &Context, object_ulid: &str) -> Response { + pub fn serve_object(cx: &Context, object_ulid: &str) -> Result { let Ok(parsed) = object_ulid.parse::() else { - return respond(400, Some("improperly formatted id"), []); + return Err(Message { + error: "improperly formatted ulid", + ..error::BAD_REQUEST + }); }; let result = cx.run(|tx| get_local_ap_object(&tx, parsed)); let Ok(object) = result else { - return respond(404, >::None, []); + return Err(error::NOT_FOUND); }; let json = to_string(&object.to_json_ld()).unwrap(); - respond(200, Some(json), [AP_CONTENT_TYPE]) + Ok(respond(200, Some(json), [AP_CONTENT_TYPE])) } const AP_CONTENT_TYPE: (&str, &str) = ("content-type", "application/activity+json"); /// Serve the special actor used for signing requests. - pub fn serve_verifier_actor(cx: &Context) -> Response { - let body = cx.verifier().to_json_ld(); + pub fn serve_verifier_actor(verifier: &Verifier) -> Response { + let body = verifier.to_json_ld(); let encoded = serde_json::to_vec(&body).unwrap(); respond(200, Some(encoded), [AP_CONTENT_TYPE]) } @@ -78,69 +102,337 @@ pub mod ap { pub mod wf { //! WebFinger endpoints and related stuff. - use puppy::{data::Username, Context, Error}; - use serde_json::json; + use puppy::{ + data::{Id, Username}, + Context, + }; + use serde_json::{json, Value}; - use crate::{respond, Response}; + use super::{ + error::{Message, BAD_REQUEST, INTERNAL, NOT_FOUND}, + respond, Response, + }; const WF_CONTENT_TYPE: (&str, &str) = ("content-type", "application/jrd+json"); - pub fn resolve(cx: &Context, query: &[(&str, &str)]) -> Response { - match query.iter().find_map(get_handle) { - // Serve JRDs for local actors. + /// Respond to a webfinger request. + pub fn resolve(cx: &Context, params: &[(&str, &str)]) -> Result { + match params.iter().find_map(get_handle) { Some(handle) if cx.config().wf_domain == handle.instance => { - let id = { - let user = cx - .store() - .lookup(Username(handle.username.to_string())) - .unwrap() - .unwrap(); - let id = cx - .store() - .get_alias::(user) - .unwrap() - .unwrap() - .0; - id + let username = Username(handle.username.to_string()); + let Ok(Some(user)) = cx.store().lookup(username) else { + do yeet NOT_FOUND; }; - let jrd = json!({ - "subject": format!("acct:{}@{}", handle.username, handle.instance), - "links": [ - { - "rel": "self", - "type": "application/activity+json", - "href": id - }, - ] - }); + let Ok(Some(Id(id))) = cx.store().get_alias(user) else { + do yeet INTERNAL; + }; + let jrd = make_jrd(handle, &id); let encoded = serde_json::to_vec(&jrd).unwrap(); - respond(200, Some(encoded), [WF_CONTENT_TYPE]) + Ok(respond(200, Some(encoded), [WF_CONTENT_TYPE])) } - Some(_) => todo!(), - None => todo!("bad request: could not find valid resource parameter"), + Some(_) | None => Err(Message { + error: "missing/invalid resource parameter", + ..BAD_REQUEST + }), } } + #[derive(Clone, Copy)] pub struct Handle<'x> { username: &'x str, instance: &'x str, } /// Parse the `resource` parameter into a [`Handle`]. - pub fn get_handle<'x>((k, v): &'x (&str, &str)) -> Option> { + fn get_handle<'x>((k, v): &'x (&str, &str)) -> Option> { // We're looking for the `resource` query parameter. if *k == "resource" { // This prefix needs to exist according to spec. - v.strip_prefix("acct:")? + let (username, instance) = v + .strip_prefix("acct:")? // Some implementations may prefix with `@`. its ok if it's there and its also ok // if its not there, so we use `trim_start_matches` instead of `strip_prefix`. .trim_start_matches('@') // Split on the middle `@` symbol, which separates the username and instance bits - .split_once('@') - // Convert to a structured format. - .map(|(username, instance)| Handle { username, instance }) + .split_once('@')?; + Some(Handle { username, instance }) } else { None } } + + /// Construct a "JSON resource descriptor". + fn make_jrd(handle: Handle<'_>, id: &str) -> Value { + json!({ + "subject": format!("acct:{}@{}", handle.username, handle.instance), + "links": [ + { + "rel": "self", + "type": "application/activity+json", + "href": id + }, + ] + }) + } +} + +type Request = hyper::Request; +type Response> = hyper::Response; + +/// Initialize the http server loop. +pub async fn start(context: Context) -> Result<(), Box> { + let addr = SocketAddr::from(([127, 0, 0, 1], context.config().port)); + + let listener = TcpListener::bind(addr).await?; + let verifier = Arc::new(Verifier::load(context.config())); + + loop { + let (stream, _) = listener.accept().await?; + let io = TokioIo::new(stream); + + let cx = context.clone(); + let verifier = verifier.clone(); + + tokio::spawn(async move { + if let Err(err) = http1::Builder::new() + .serve_connection(io, service_fn(|req| handle(req, &verifier, cx.clone()))) + .await + { + eprintln!("Error serving connection: {:?}", err); + } + }); + } +} + +// A parsed HTTP request for easy handling. +struct Req<'a> { + method: &'a Method, + body: Bytes, + // The content-types in the accept header + accept: Vec<&'a str>, + // URI bits + params: Vec<(&'a str, &'a str)>, + path: Vec<&'a str>, +} + +impl Req<'_> { + /// Get the path segments (non-empty parts of the path string separated by the '/' character). + fn path(&self) -> &[&str] { + &self.path + } + /// Turn an HTTP request into a more simple form so we can process it more easily. + fn simplify<'x>(r: &'x http::Request) -> Req<'x> { + let path: Vec<&str> = r + .uri() + .path() + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + let params: Vec<(&str, &str)> = r + .uri() + .query() + .into_iter() + .flat_map(|s| s.split('&')) + .filter_map(|s| s.split_once('=')) + .collect(); + let accept = r + .headers() + .iter() + .find_map(|(k, v)| (k == "accept").then_some(v)) + .and_then(|val| val.to_str().ok()) + .iter() + .flat_map(|s| s.split(' ')) + .filter(|s| !s.is_empty()) + .collect(); + Req { + method: r.method(), + body: r.body().clone(), + accept, + params, + path, + } + } +} + +/// The request handler. +async fn handle(req: Request, verifier: &Verifier, cx: Context) -> Result { + // We need to fetch the entire body of the request for signature validation, because that involves making + // a digest of the request body in some cases. + // TODO: defer loading the body until it is needed. + let request = { + let (req, body) = req.into_parts(); + let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else { + todo!(); + }; + http::Request::from_parts(req, body) + }; + // Simplified representation of a request, so we can pattern match on it more easily in the dispatchers. + let req = Req::simplify(&request); + // We'll use the path to pick where specifically to send the request. + // Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast + // with a clear error message will save anyone trying to get *their* signatures implementation a major + // headache. + let res = match verifier.verify(&request).await { + // If the request was signed and the signature was accepted, they can access the protected endpoints. + Verdict::Verified(sig) => dispatch_signed(cx, &verifier, &req, sig).await, + // Unsigned requests can see a smaller subset of endpoints, most notably the verification actor. + Verdict::Unsigned => dispatch_public(cx, &verifier, &req).await, + // If a signature was provided *but it turned out to be unverifiable*, show them the error message. + Verdict::Rejected { reason, signature_str } => Err(Message { + error: "signature verification failed", + status: 403, + detail: Some(json!({ + "signature": signature_str, + "reason": reason, + })), + }), + }; + // If one of the endpoints gave us an error message, we convert that into a response and then + // serve it to the client. In either case, we just serve a response. + Ok(res.unwrap_or_else(|msg| req.error(msg))) +} + +const POST: &Method = &Method::POST; +const GET: &Method = &Method::GET; + +/// Handle a signed and verified request. +/// +/// This function is where all requests to a protected endpoint have to go through. If the request +/// was signed but does not target a protected endpoint, this function will fall back to the +/// [`dispatch_public`] handler. +async fn dispatch_signed( + cx: Context, + verifier: &Verifier, + req: &Req<'_>, + sig: Signer, +) -> Result { + match (req.method, req.path()) { + // Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch". + // The one exception for this is `/s/request-verifier`, which is where the request + // verification actor lives. + (GET, ["o", ulid]) => ap::serve_object(&cx, ulid), + // POSTs to an actor's inbox need to be signed to prevent impersonation. + (POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| try { + // We only handle the intermediate parsing of the json, full resolution of the + // activity object will happen inside the inbox handler itself. + ap::inbox(&cx, ulid, sig, json) + }), + // Try the resources for which no signature is required as well. + _ => dispatch_public(cx, verifier, req).await, + } +} + +/// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the +/// function will return a 404 response. If the path *does* exist, but the signature is not +/// valid, they will also get a 404. +async fn dispatch_public( + cx: Context, + verifier: &Verifier, + req: &Req<'_>, +) -> Result { + match (req.method, req.path()) { + (GET, ["proxy"]) => Ok(ap::proxy(&cx, &req.params).await), + (GET, [".well-known", "webfinger"]) => wf::resolve(&cx, &req.params), + // TODO: nicer solution for this + (GET, VERIFIER_MOUNT) => Ok(ap::serve_verifier_actor(&verifier)), + _ => Err(error::NOT_FOUND), + } +} + +fn with_json( + body: &[u8], + f: impl FnOnce(Value) -> Result, +) -> Result { + let Ok(json) = from_slice(body) else { + return Err(Message { + error: "could not decode json", + ..error::BAD_REQUEST + }); + }; + f(json) +} + +/// A quick, simple way to construct a response. +fn respond( + status: u16, + body: Option>, + headers: [(&str, &str); N], +) -> Response { + let mut resp = Response::<()>::builder().status(status); + for (name, data) in headers { + resp = resp.header(name, data); + } + resp.body(match body { + Some(bytes) => Full::new(bytes.into()), + None => Full::new(Bytes::default()), + }) + .unwrap() +} + +mod error { + //! Pre-baked error responses. + + use serde_json::{json, Value}; + use super::Response; + + /// An error message shown to an end user of the API. + pub struct Message { + /// The main error message. + pub error: &'static str, + /// Only shown if the `accept` header included json. + pub detail: Option, + /// The status code for the response. + pub status: u16, + } + + impl super::Req<'_> { + /// Generate an error response for the request. + pub fn error(&self, err: Message) -> Response { + let resp = Response::<()>::builder().status(err.status); + // If the accept header wants json, we will give them a nice structured error + // message. Otherwise, we throw a short bit of text at them. + if self.accepts_json() { + let json = json!({ + "error": err.error, + "details": err.detail, + }); + let body = serde_json::to_vec_pretty(&json).unwrap(); + resp.header("content-type", "application/json") + .body(body.try_into().unwrap()) + .unwrap() + } else { + resp.header("content-type", "text/plain") + .body(err.error.try_into().unwrap()) + .unwrap() + } + } + /// Check whether the requester wants json from us. + pub fn accepts_json(&self) -> bool { + self.accept + .iter() + .filter_map(|s| s.split_once('/')) + .any(|(k, v)| k == "application" && v.split('+').any(|p| p == "json")) + } + } + + /// A 404 NOT FOUND response. + pub const NOT_FOUND: Message = Message { + error: "not found", + detail: None, + status: 404, + }; + + /// A basic 400 BAD REQUEST response. + pub const BAD_REQUEST: Message = Message { + error: "bad request", + detail: None, + status: 400, + }; + + /// A basic 500 INTERNAL SERVER ERROR message. + pub const INTERNAL: Message = Message { + error: "internal server error", + detail: None, + status: 500, + }; } diff --git a/bin/server/src/main.rs b/bin/server/src/main.rs index 3d213b1..afadb70 100644 --- a/bin/server/src/main.rs +++ b/bin/server/src/main.rs @@ -1,233 +1,29 @@ +//! The ActivityPuppy social media server. +//! +//! This crate contains the implementation of the ActivityPuppy's server binary. Also see the library, +//! [`puppy`], and the other two major components: [`store`] for persistence and [`fetch`] for the +//! federation implementation. +//! +//! [`store`]: puppy::store +//! [`fetch`]: puppy::fetch #![feature(try_blocks, yeet_expr)] -use std::convert::Infallible; -use std::net::SocketAddr; +use puppy::{config::Config, Context}; -use http_body_util::{BodyExt as _, Full}; -use hyper::body::Bytes; -use hyper::server::conn::http1; -use hyper::service::service_fn; -use hyper_util::rt::TokioIo; -use puppy::auth::{self, SigError, Signer, Verifier}; -use puppy::store::Store; -use puppy::{auth::verify_signature, config::Config, Context}; -use serde_json::{from_slice, Value}; -use tokio::net::TcpListener; +mod sig; +mod api; +/// Starts up the whole shebang. #[tokio::main] async fn main() { + // TODO: load the config from a file or something. let config = Config { ap_domain: "test.piss-on.me".to_string(), wf_domain: "test.piss-on.me".to_string(), - state_dir: "state".to_string(), + state_dir: ".state".to_string(), port: 1312, }; - let store = Store::open(&config.state_dir, puppy::data::schema()).unwrap(); - let verifier = Verifier::load(&config); - let context = Context::new(config, store, verifier); - start(context).await.unwrap(); -} - -pub async fn start(context: Context) -> Result<(), Box> { - let addr = SocketAddr::from(([127, 0, 0, 1], context.config().port)); - let listener = TcpListener::bind(addr).await?; - - // We start a loop to continuously accept incoming connections - loop { - let (stream, _) = listener.accept().await?; - - // Use an adapter to access something implementing `tokio::io` traits as if they implement - // `hyper::rt` IO traits. - let io = TokioIo::new(stream); - - let context = context.clone(); - // Spawn a tokio task to serve multiple connections concurrently - tokio::task::spawn(async move { - // Finally, we bind the incoming connection to our `hello` service - if let Err(err) = http1::Builder::new() - // `service_fn` converts our function in a `Service` - .serve_connection(io, service_fn(|req| handle(req, context.clone()))) - .await - { - eprintln!("Error serving connection: {:?}", err); - } - }); - } -} - -type Request = hyper::Request; -type Response> = hyper::Response; - -// A parsed HTTP request for easy handling. -struct Req<'a> { - method: &'a Method, - body: Bytes, - // URI bits - params: Vec<(&'a str, &'a str)>, - path: Vec<&'a str>, -} - -impl Req<'_> { - fn path(&self) -> &[&str] { - &self.path - } -} - -fn simplify<'x>(r: &'x http::Request) -> Req<'x> { - let path: Vec<&str> = r - .uri() - .path() - .split('/') - .filter(|s| !s.is_empty()) - .collect(); - let params: Vec<(&str, &str)> = r - .uri() - .query() - .into_iter() - .flat_map(|s| s.split('&')) - .filter_map(|s| s.split_once('=')) - .collect(); - Req { - method: r.method(), - body: r.body().clone(), - params, - path, - } -} - -use hyper::Method; - -const POST: &Method = &Method::POST; -const GET: &Method = &Method::GET; - -/// The request handler. -async fn handle(req: Request, cx: Context) -> Result { - // We need to fetch the entire body of the request for signature validation, because that involves making - // a digest of the request body in some cases. - let request = { - let (req, body) = req.into_parts(); - let Ok(body) = body.collect().await.map(|b| b.to_bytes()) else { - return Ok(error::invalid_body("Could not get request body")); - }; - http::Request::from_parts(req, body) - }; - // Simplified representation of a request, so we can pattern match on it more easily in the dispatchers. - let req = simplify(&request); - // We'll use the path to pick where specifically to send the request. - // Check request signature at the door. Even if it isn't needed for a particular endpoint, failing fast - // with a clear error message will save anyone trying to get *their* signatures implementation a major - // headache. - let res = match verify_signature(&cx, &request).await { - // If the request was signed and the signature was accepted, they can access the protected endpoints. - Ok(Some(sig)) => dispatch_signed(cx, req, sig).await, - // Unsigned requests can see a smaller subset of endpoints, most notably the verification actor. - Ok(None) => dispatch_public(cx, req).await, - // If a signature was provided *but it turned out to be unverifiable*, show them the error message. - Err(err) => error::bad_signature(match err { - SigError::VerificationFailed { error } => format!("Verification failed: {error}"), - SigError::ParseSignature { error } => format!("Failed to parse signature: {error}"), - SigError::FailedToFetchKey { keyid } => format!("Failed to fetch {keyid}"), - }), - }; - Ok(res) -} - -/// Handle a signed and verified request. -/// -/// This function is where all requests to a protected endpoint have to go through. If the request -/// was signed but does not target a protected endpoint, this function will fall back to the -/// [`dispatch_public`] handler. -async fn dispatch_signed(cx: Context, req: Req<'_>, sig: Signer) -> Response { - eprintln!("Dispatching signed request"); - match (req.method, req.path()) { - // Viewing ActivityPub objects requires a signed request, i.e. "authorized fetch". - // The one exception for this is `/s/request-verifier`, which is where the request - // verification actor lives. - (GET, ["o", ulid]) => api::ap::serve_object(&cx, ulid), - // POSTs to an actor's inbox need to be signed to prevent impersonation. - (POST, ["o", ulid, "inbox"]) => with_json(&req.body, |json| { - // We only handle the intermediate parsing of the json, full resolution of the - // activity object will happen inside the inbox handler itself. - api::ap::inbox(&cx, ulid, sig, json) - }), - // Try the resources for which no signature is required as well. - _ => dispatch_public(cx, req).await, - } -} - -/// Dispatch `req` to an unprotected endpoint. If the requested path does not exist, the -/// function will return a 404 response. -async fn dispatch_public(cx: Context, req: Req<'_>) -> Response { - eprintln!("Dispatching public request"); - match (req.method, req.path()) { - (GET, ["proxy"]) => api::ap::proxy(&cx, &req.params).await, - (GET, [".well-known", "webfinger"]) => api::wf::resolve(&cx, &req.params), - (GET, auth::VERIFIER_MOUNT) => api::ap::serve_verifier_actor(&cx), - (m, p) => { - eprintln!("404: {m} {p:?}"); - error::not_found() - } - } -} - -fn with_json(body: &[u8], f: impl FnOnce(Value) -> Response) -> Response { - let Ok(json) = from_slice(body) else { - return error::invalid_body("Could not decode as JSON"); - }; - f(json) -} - -/// A quick, simple way to construct a response. -fn respond( - status: u16, - body: Option>, - headers: [(&str, &str); N], -) -> Response { - let mut resp = Response::<()>::builder().status(status); - for (name, data) in headers { - resp = resp.header(name, data); - } - resp.body(match body { - Some(bytes) => Full::new(bytes.into()), - None => Full::new(Bytes::default()), - }) - .unwrap() -} - -mod api; - -mod error { - //! Pre-baked error responses. - - use http_body_util::Full; - use super::Response; - - /// 404 response. - pub fn not_found() -> Response { - let body = Full::new("Not found".into()); - Response::<()>::builder() - .status(404) - .header("content-type", "text/plain") - .body(body) - .unwrap() - } - - /// 403 response indicating a bad request signature. - pub fn bad_signature(err: String) -> Response { - let body = Full::new(err.into()); - Response::<()>::builder() - .status(403) - .header("content-type", "text/plain") - .body(body) - .unwrap() - } - - pub fn invalid_body(err: impl ToString) -> Response { - let body = Full::new(err.to_string().into()); - Response::<()>::builder() - .status(400) - .header("content-type", "text/plain") - .body(body) - .unwrap() - } + let context = Context::load(config).unwrap(); + // Start the web server + api::start(context).await.unwrap(); } diff --git a/bin/server/src/sig.rs b/bin/server/src/sig.rs new file mode 100644 index 0000000..9b2b816 --- /dev/null +++ b/bin/server/src/sig.rs @@ -0,0 +1,149 @@ +//! Verification of HTTP signatures. + +use http::Request; +use puppy::fetch::{ + signatures::{Private, Public, Signature, SigningKey, VerificationKey, Key}, + FetchError, +}; +use serde_json::{json, Value}; + +use puppy::config::Config; + +/// Checks request signatures. +#[derive(Clone)] +pub struct Verifier { + actor_id: String, + key_id: String, + private: Private, + public: Public, +} + +const VERIFIER_PATH: &str = "/s/request-verifier"; +/// The path at which the request verification actor will present itself. +pub const VERIFIER_MOUNT: &[&str] = &["s", "request-verifier"]; + +/// A "verdict" about a signed request, passed by a [`Verifier`]. +pub enum Verdict { + /// The signature checks out. + Verified(Signer), + /// The signature does not contain a signature header. This may be intentional, or a client error. + Unsigned, + /// The signature failed to verify due to an error related to the signature itself. + Rejected { + signature_str: String, + reason: String, + }, +} + +impl Verifier { + /// Get the JSON-LD representation of the verifier actor. + pub fn to_json_ld(&self) -> Value { + json!({ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + "id": self.actor_id, + "name": "Public key fetcher", + "publicKey": { + "id": self.key_id, + "owner": self.actor_id, + "publicKeyPem": self.public.encode_pem() + }, + "type": "Service", + }) + } + /// Load the server's verifier actor. + /// + /// Each server has one special actor for fetching public keys. Unlike all other objects, + /// acquiring that actor's JSON-LD representation does not require a request signature. + /// + /// It doesn't have any data in the data store. Due to its exceptional nature, we just put + /// the private key in the [`state_dir`][Config::state_dir]. The very first time you load + /// the verifier, it generates the required private keys. + pub fn load(cfg: &Config) -> Verifier { + let Config { ap_domain, state_dir, .. } = cfg; + let key_path = format!("{state_dir}/fetcher.pem"); + // Read the private key from the state directory, or generate a new one if it couldn't + // be read. + let private = Private::load(&key_path).unwrap_or_else(|| { + let (private, _) = Private::gen(); + private.save(key_path); + private + }); + Verifier { + actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"), + key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"), + public: private.get_public(), + private, + } + } + /// Does the HTTP signature verification process, and returns a "proof" of the signature in the form + /// of the [`Signer`], which contains information about who signed a particular request. + pub async fn verify(&self, req: &Request) -> Verdict { + // TODO: implement the whole verification thing as a middleware so we can intercept requests + // like these, instead of coupling this tightly with the router. + if req.uri().path() == VERIFIER_PATH { + // HACK: Allow access to the request verifier actor without checking the signature. + return Verdict::Unsigned; + } + + let Some(header) = req.headers().get("signature") else { + return Verdict::Unsigned; + }; + + let signature_str = header + .to_str() + .expect("signature header value should be valid ascii") + .to_string(); + + let sig = match Signature::derive(&req) { + Err(error) => return Verdict::Rejected { signature_str, reason: error }, + Ok(signature) => signature, + }; + + // Fetch the signer's public key using our private key. + let fetch_result = self.fetch_public_key(sig.key_id()).await; + let public_key = match fetch_result { + Ok(public_key) => public_key, + Err(err) => { + return Verdict::Rejected { + reason: format!("could not fetch public key: {err}"), + signature_str, + } + } + }; + + // TODO: verify digest also + if let Err(error) = public_key.verify(&sig) { + Verdict::Rejected { signature_str, reason: error } + } else { + Verdict::Verified(Signer { ap_id: public_key.owner }) + } + } + /// Send a request to get the public key from an ID. This request will be signed with the + /// verifier actor's public key. + async fn fetch_public_key(&self, uri: &str) -> Result { + let json = puppy::fetch::resolve(&self.signing_key(), uri).await?; + let Some(key) = Key::from_json(json) else { + return Err(FetchError::BadJson( + "invalid public key structure".to_string(), + )); + }; + Ok(key.upgrade()) + } + /// Get the key that the verification actor signs requests with. + fn signing_key(&self) -> SigningKey { + Key { + id: self.key_id.clone(), + owner: self.actor_id.clone(), + inner: self.private.clone(), + } + } +} + +/// An ActivityPub actor that signed a request. +pub struct Signer { + /// The ActivityPub ID (a URL) of the signer of the request. + pub ap_id: String, +} diff --git a/lib/fetch/src/client.rs b/lib/fetch/src/client.rs new file mode 100644 index 0000000..7cebb09 --- /dev/null +++ b/lib/fetch/src/client.rs @@ -0,0 +1,166 @@ +use chrono::Utc; +use http_body_util::BodyExt as _; +use reqwest::Body; +use serde_json::Value; +use derive_more::Display; + +use crate::{ + object::Activity, + signatures::{SigningKey, Options}, + FetchError, +}; + +/// The name of the server software, used for generating the user agent string. +/// +/// See also [`VERSION`]. +pub const SOFTWARE: &str = "ActivityPuppy"; +/// The current version of the server software, which is incorporated into the user agent string +/// for all outbound requests made by ActivityPuppy. +pub const VERSION: &str = "0.0.1-dev"; + +/// Content-type/accept header for ActivityPub requests. +pub const ACTIVITYPUB_TYPE: &str = "application/activity+json"; + +/// A client for sending ActivityPub and WebFinger requests with. +pub struct Client { + inner: reqwest::Client, +} + +impl Client { + /// Constructs a new federation client. + pub fn new() -> Client { + Client { inner: reqwest::Client::new() } + } + /// Deliver an [`Activity`] to a particular `inbox`. + /// + /// Note that in order for the request to be considered valid by most implementations, `key.owner` + /// must equal `payload.actor`. + pub async fn deliver(&self, key: &SigningKey, payload: &Activity, inbox: &str) { + todo!() + } + /// A high-level function to resolve a single ActivityPub ID using a signed request. + pub async fn resolve(&self, key: &SigningKey, url: &str) -> Result { + let system = Subsystem::Resolver; + + let mut req = system + .new_request(url)? + .header("accept", ACTIVITYPUB_TYPE) + .body(()) + .unwrap(); + + key.sign(Options::MODERN, &req) + .map_err(FetchError::Sig)? + .commit(&mut req); + + let request = req.map(|()| Body::default()).try_into()?; + let response = self.inner.execute(request).await?; + + if response.status().is_success() { + response.json().await.map_err(From::from) + } else { + Err(FetchError::NotSuccess { + status: response.status().as_u16(), + body: response.text().await?, + url: url.to_string(), + }) + } + } + /// Forwards a request and returns the raw response, so that it can be analyzed for debugging. + /// + /// It exists solely as a debugging tool! + pub async fn proxy( + &self, + key: &SigningKey, + url: &str, + ) -> Result, FetchError> { + let system = Subsystem::DevProxy; + + let mut req = system + .new_request(url)? + .header("accept", ACTIVITYPUB_TYPE) + .body(()) + .unwrap(); + + println!("[{system}]: using modern config"); + key.sign(Options::MODERN, &req) + .expect("signing error") + .commit(&mut req); + + let resp = reqwest::Client::new() + .execute(req.map(|_| Body::default()).try_into().unwrap()) + .await?; + + let http_resp: http::Response = resp.into(); + let (res, body) = http_resp.into_parts(); + let body = body.collect().await.unwrap().to_bytes(); + let http_resp = + http::Response::from_parts(res, String::from_utf8_lossy(body.as_ref()).into_owned()); + Ok(http_resp) + } +} + +/// Identifies a specific subsystem that makes an outgoing request. +/// +/// This allows us to precisely track each outgoing request, as well as generate a meaningful +/// user-agent header. It is also used to generate a "base request". +#[derive(Clone, Copy, Display)] +enum Subsystem { + /// The subsystem that dereferences ActivityPub URLs to JSON values. + /// + /// In addition, the resolver is used for resolving webfinger handles to ActivityPub actors. + #[display = "resolver"] + Resolver, + /// The subsystem responsible for delivering activities to inboxes. + #[display = "delivery"] + Delivery, + /// For testing the resolver and signatures. + #[display = "devproxy"] + DevProxy, +} + +impl Subsystem { + /// Get the user agent string for the subsystem. + fn user_agent(&self) -> String { + format!("{SOFTWARE}/{VERSION} [{}]", match self { + Subsystem::Resolver => "resolver", + Subsystem::Delivery => "delivery", + Subsystem::DevProxy => "devproxy", + }) + } + /// Construct a new request for this subsystem. + /// + /// This will set the following headers, which are common to all requests made by the fetch + /// system: + /// + /// - `user-agent`, which depends on the particular subsystem in use + /// - `date`, which is generated from the current time + /// - `host`, which is derived from `target` + /// + /// This function returns an error if the `target` is not a valid URI. It panics if the URI + /// does not have a host specified. + fn new_request(self, target: &str) -> Result { + // Format our time like "Sun, 06 Nov 1994 08:49:37 GMT" + const RFC_822: &str = "%a, %d %b %Y %H:%M:%S GMT"; + let date = Utc::now().format(RFC_822).to_string(); + + let uri = target + .parse::() + .map_err(|e| FetchError::InvalidURI { + url: target.to_string(), + error: e.to_string(), + })?; + + let Some(host) = uri.host() else { + // SECURITY: Refuse to resolve URLs to local resources using local keys. + panic!("refusing to resolve a relative URL: {target}") + }; + + let req = http::Request::builder() + .uri(target) + .header("user-agent", self.user_agent()) + .header("date", date) + .header("host", host); + + Ok(req) + } +} diff --git a/lib/fetch/src/lib.rs b/lib/fetch/src/lib.rs index 406e766..0a129c8 100644 --- a/lib/fetch/src/lib.rs +++ b/lib/fetch/src/lib.rs @@ -1,179 +1,85 @@ #![feature(iter_intersperse, yeet_expr, iterator_try_collect, try_blocks)] -use chrono::Utc; -use http_body_util::BodyExt as _; -use reqwest::Body; -use serde_json::{json, Value}; +use std::error::Error; -use crate::signatures::{Options, HS2019}; +use derive_more::Display; +use serde_json::Value; + +use object::Activity; +use signatures::SigningKey; pub use http; -pub use signatures::{Key, SigningKey, VerificationKey}; pub mod signatures; +pub mod object; -pub enum Object { - Activity(Activity), - Actor(Actor), - Object { - id: String, - kind: String, - content: Option, - summary: Option, +pub use client::Client; +mod client; + +/// Deliver an activity to an inbox. +pub async fn deliver(key: &SigningKey, activity: Activity, inbox: &str) { + Client::new().deliver(key, &activity, inbox).await +} + +/// Resolve an ActivityPub ID to a JSON value. +/// +/// Note: This creates a new [`Client`] every time you call it, so if you're gonna call it more than just +/// a couple of times, create a `Client` and call its inherent methods instead. +pub async fn resolve(key: &SigningKey, target: &str) -> Result { + Client::new().resolve(key, target).await +} + +/// Proxy a GET request through this server. +/// +/// Should only be used for manually testing stuff. +pub async fn forward(key: &SigningKey, target: &str) -> Result, FetchError> { + Client::new().proxy(key, target).await +} + +/// Errors that may occur during the execution of HTTP request routines. +#[derive(Debug, Display)] +pub enum FetchError { + /// Some error internal to the request sending process occurred. + #[display(fmt = "internal error: {error} (url={url:?})")] + Internal { url: Option, error: String }, + /// The URI was not valid and therefore the request could not be made. + #[display(fmt = "invalid uri: {error} (url={url})")] + InvalidURI { url: String, error: String }, + /// A non-success status code was encountered. + #[display(fmt = "non-2xx status code: {status} (url={url})")] + NotSuccess { + status: u16, + url: String, + body: String, }, + /// The JSON body of a response could not be loaded. The string inside is the error + /// message produced by the JSON deserializer. + #[display(fmt = "deserialization error: {}", self.0)] + BadJson(String), + /// An error that occurred while generating a signature for a a request. + #[display(fmt = "signing error: {}", self.0)] + Sig(String), } -impl Object { - pub fn id(&self) -> &str { - match self { - Object::Activity(a) => &a.id, - Object::Actor(a) => &a.id, - Object::Object { id, .. } => id, - } +impl FetchError { + /// Check whether the error is due to a 403 UNAUTHORIZED response status code. + pub fn is_unauthorized(&self) -> bool { + matches!(self, FetchError::NotSuccess { status: 403, .. }) } - pub fn to_json_ld(&self) -> Value { - match self { - Object::Activity(a) => a.to_json_ld(), - Object::Actor(a) => a.to_json_ld(), - Object::Object { id, kind, content, summary } => json!({ - "id": id.to_string(), - "type": kind, - "content": content, - "summary": summary, - }), - } + /// Check whether the error is due to a 404 NOT FOUND response status code. + pub fn is_not_found(&self) -> bool { + matches!(self, FetchError::NotSuccess { status: 404, .. }) } } -pub struct Activity { - pub id: String, - pub actor: String, - pub object: Box, - pub kind: T, -} - -impl Activity -where - K: ToString, -{ - pub fn to_json_ld(&self) -> Value { - json!({ - "@context": [ - "https://www.w3.org/ns/activitystreams", - { "Bite": "https://ns.mia.jetzt/as#Bite" }, - ], - "id": self.id.to_string(), - "actor": self.actor.to_string(), - "type": self.kind.to_string(), - "object": self.object.to_json_ld() - }) - } -} - -/// Deliver an [`Activity`] to a particular `inbox`. -pub async fn deliver(key: &SigningKey, activity: Activity, inbox: &str) -> () { - todo!() -} - -// Sun, 06 Nov 1994 08:49:37 GMT -const RFC_822: &str = "%a, %d %b %Y %H:%M:%S GMT"; -pub async fn resolve(key: &SigningKey, target: &str) -> reqwest::Result { - // TODO: make this retry with different signature options and remember what works for the - // particular host. - println!("[resolver]: resolving url {target} using key {}", key.id); - - let uri = target.parse::().unwrap(); - let host = uri.host().unwrap(); - let date = Utc::now().format(RFC_822).to_string(); - let mut req = http::Request::builder() - .uri(target) - .header("accept", "application/activity+json") - .header("user-agent", "ActivityPuppy/0.0.0 (resolver)") - .header("date", date) - .header("host", host) - // Empty body - .body(()) - .unwrap(); - - println!("[resolver]: constructed request {req:#?}"); - - // hs2019 works with masto - println!("[resolver]: using modern config"); - let sig = key.sign(Options::MODERN, &req).expect("signing error"); - - println!("[resolver]: constructed signature {sig:#?}"); - sig.commit(&mut req); - - reqwest::Client::new() - .execute(req.map(|_| Body::default()).try_into().unwrap()) - .await? - .error_for_status()? - .json() - .await -} - -pub async fn forward(key: &SigningKey, target: &str) -> reqwest::Result> { - let date = Utc::now().format(RFC_822).to_string(); - let uri = target.parse::().unwrap(); - let host = uri.host().unwrap(); - let mut req = http::Request::get(target) - .header("accept", "application/activity+json") - .header("user-agent", "ActivityPuppy/0.0.0 (resolver)") - .header("date", date) - .header("host", host) - // Empty body - .body(()) - .unwrap(); - // hs2019 works with masto - println!("[proxy]: using modern config"); - key.sign(Options::MODERN, &req) - .expect("signing error") - .commit(&mut req); - - let resp = reqwest::Client::new() - .execute(req.map(|_| Body::default()).try_into().unwrap()) - .await?; - - let http_resp: http::Response = resp.into(); - let (res, body) = http_resp.into_parts(); - let body = body.collect().await.unwrap().to_bytes(); - let http_resp = - http::Response::from_parts(res, String::from_utf8_lossy(body.as_ref()).into_owned()); - Ok(http_resp) -} - -/// An actor is an entity capable of producing Takes. -pub struct Actor { - /// The URL pointing to this object. - pub id: String, - /// Where others should send activities. - pub inbox: String, - /// Note: this maps to the `preferredUsername` property. - pub account_name: String, - /// Note: this maps to the `name` property. - pub display_name: Option, - /// Public counterpart to the signing key used to sign activities - /// generated by the actor. - pub public_key: Key, -} - -impl Actor { - pub fn to_json_ld(&self) -> Value { - json!({ - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - "id": self.id.to_string(), - "inbox": self.inbox.to_string(), - "outbox": self.inbox.to_string().replace("inbox", "outbox"), - "preferredUsername": self.account_name, - "name": self.display_name, - "type": "Person", - "publicKey": { - "id": self.public_key.id, - "publicKeyPem": self.public_key.inner, - "owner": self.id.to_string(), +#[doc(hidden)] +impl From for FetchError { + fn from(error: reqwest::Error) -> FetchError { + match error.source().and_then(|e| e.downcast_ref()) { + Some(e @ serde_json::Error { .. }) => FetchError::BadJson(e.to_string()), + None => { + let url = error.url().map(|u| u.to_string()); + FetchError::Internal { url, error: error.to_string() } } - }) + } } } diff --git a/lib/fetch/src/object.rs b/lib/fetch/src/object.rs new file mode 100644 index 0000000..4449062 --- /dev/null +++ b/lib/fetch/src/object.rs @@ -0,0 +1,100 @@ +//! ActivityPub vocabulary as interpreted by ActivityPuppy. + +use serde_json::{json, Value}; + +pub use crate::signatures::Key as PublicKey; + +pub struct Activity { + pub id: String, + pub actor: String, + pub object: Box, + pub kind: T, +} + +impl Activity +where + K: ToString, +{ + pub fn to_json_ld(&self) -> Value { + json!({ + "@context": [ + "https://www.w3.org/ns/activitystreams", + { "Bite": "https://ns.mia.jetzt/as#Bite" }, + ], + "id": self.id.to_string(), + "actor": self.actor.to_string(), + "type": self.kind.to_string(), + "object": self.object.to_json_ld() + }) + } +} + +/// An actor is an entity capable of producing Takes. +pub struct Actor { + /// The URL pointing to this object. + pub id: String, + /// Where others should send activities. + pub inbox: String, + /// Note: this maps to the `preferredUsername` property. + pub account_name: String, + /// Note: this maps to the `name` property. + pub display_name: Option, + /// Public counterpart to the signing key used to sign activities + /// generated by the actor. + pub public_key: PublicKey, +} + +impl Actor { + pub fn to_json_ld(&self) -> Value { + json!({ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + ], + "id": self.id.to_string(), + "inbox": self.inbox.to_string(), + "outbox": self.inbox.to_string().replace("inbox", "outbox"), + "preferredUsername": self.account_name, + "name": self.display_name, + "type": "Person", + "publicKey": { + "id": self.public_key.id, + "publicKeyPem": self.public_key.inner, + "owner": self.id.to_string(), + } + }) + } +} + +pub enum Object { + Activity(Activity), + Actor(Actor), + Object { + id: String, + kind: String, + content: Option, + summary: Option, + }, +} + +impl Object { + pub fn id(&self) -> &str { + match self { + Object::Activity(a) => &a.id, + Object::Actor(a) => &a.id, + Object::Object { id, .. } => id, + } + } + pub fn to_json_ld(&self) -> Value { + match self { + Object::Activity(a) => a.to_json_ld(), + Object::Actor(a) => a.to_json_ld(), + Object::Object { id, kind, content, summary } => json!({ + "id": id.to_string(), + "type": kind, + "content": content, + "summary": summary, + }), + } + } +} diff --git a/lib/fetch/src/signatures.rs b/lib/fetch/src/signatures.rs index ca8a2e1..8f659f0 100644 --- a/lib/fetch/src/signatures.rs +++ b/lib/fetch/src/signatures.rs @@ -40,6 +40,9 @@ use serde_json::{Map, Value}; use self::new::{decode, encode, sha256, IR}; +/// Size of the RSA private keys puppy generates. +const KEY_SIZE: usize = 2048; + /// A key that can be used to verify a request signature. pub type VerificationKey = Key; @@ -73,7 +76,7 @@ impl Key { json.get("publicKey")?.as_object().and_then(Key::from_map) }) } - /// Construct + /// Try to interpret the given map as a public key. fn from_map(map: &Map) -> Option { Some(Key { id: map.get("id")?.as_str().map(str::to_owned)?, @@ -82,6 +85,10 @@ impl Key { }) } /// "Upgrade" a pem-encoded public key to a key that can actually be used for requests. + /// + /// The inverse of this is [`Key::serialize`], which turns `inner` back into a string. + /// + /// [`Key::serialize`]: Key::::serialize pub fn upgrade(self) -> Key { let inner = Public::decode_pem(&self.inner); Key { @@ -100,8 +107,7 @@ impl Private { /// Generate a new keypair. pub fn gen() -> (Private, Public) { let mut rng = rand::thread_rng(); - let bits = 512; - let private = RsaPrivateKey::new(&mut rng, bits).unwrap(); + let private = RsaPrivateKey::new(&mut rng, KEY_SIZE).unwrap(); let public = private.to_public_key(); (Private(private), Public(public)) } @@ -110,11 +116,19 @@ impl Private { Public(self.0.to_public_key()) } /// Load a private key from a file on disk. - pub fn load(path: impl AsRef) -> Private { + pub fn load(path: impl AsRef) -> Option { use rsa::pkcs8::DecodePrivateKey; + let path = path.as_ref(); DecodePrivateKey::read_pkcs8_pem_file(path) .map(Private) - .unwrap() + .ok() + } + /// Store the private key at `path`. + pub fn save(&self, path: impl AsRef) { + use rsa::pkcs8::EncodePrivateKey; + self.0 + .write_pkcs8_pem_file(path, LineEnding::default()) + .expect("writing a private key to a file should not fail") } /// PEM-encode the key PKCS#8 style. pub fn encode_pem(&self) -> String { diff --git a/lib/puppy/src/context.rs b/lib/puppy/src/context.rs index 0a52594..9fe481a 100644 --- a/lib/puppy/src/context.rs +++ b/lib/puppy/src/context.rs @@ -1,24 +1,24 @@ use store::{Key, Store, Transaction}; -use crate::{auth::Verifier, config::Config, Result}; +use crate::{config::Config, Result}; /// The context of a running ActivityPuppy. /// /// This type provides access to the data store and configuration. #[derive(Clone)] pub struct Context { - verifier: Option, config: Config, store: Store, } impl Context { - pub fn new(config: Config, store: Store, verifier: Verifier) -> Context { - Context { - verifier: Some(verifier), - config, - store, - } + fn new(config: Config, store: Store) -> Context { + Context { config, store } + } + /// Load the server context from the configuration. + pub fn load(config: Config) -> Result { + let store = Store::open(&config.state_dir, crate::data::schema())?; + Ok(Context { config, store }) } /// Do a data store [transaction][store::Transaction]. pub fn run(&self, f: impl FnOnce(&Transaction<'_>) -> Result) -> Result { @@ -36,10 +36,6 @@ impl Context { pub fn mk_url(&self, key: Key) -> String { format!("https://{}/o/{key}", self.config.ap_domain) } - /// Get the verification actor. - pub fn verifier(&self) -> &Verifier { - self.verifier.as_ref().unwrap() - } } /// Load a context for running tests in. @@ -49,7 +45,5 @@ pub fn test_context( schema: store::types::Schema, test: impl FnOnce(Context) -> Result, ) -> Result { - Store::test(schema, |store| { - test(Context { config, store, verifier: None }) - }) + Store::test(schema, |store| test(Context { config, store })) } diff --git a/lib/puppy/src/lib.rs b/lib/puppy/src/lib.rs index f615348..eabcf56 100644 --- a/lib/puppy/src/lib.rs +++ b/lib/puppy/src/lib.rs @@ -31,7 +31,7 @@ mod interact; /// Retrieve an ActivityPub object from the database. /// /// Fails with `Error::Missing` if the required properties are not present. -pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result { +pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result { let Some(obj) = tx.get_mixin::(key)? else { // We need this data in order to determine the object type. If the passed key does not // have this data, it must not be an ActivityPub object. @@ -48,12 +48,12 @@ pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result, key: Key) -> Result, key: Key) -> Result Result, String> { - let json = fetch::resolve(&self.signing_key(), uri).await.unwrap(); - Ok(fetch::Key::from_json(dbg!(json)).unwrap().upgrade()) - } - - /// Get the key that the verification actor signs requests with. - fn signing_key(&self) -> fetch::SigningKey { - fetch::Key { - id: self.key_id.clone(), - owner: self.actor_id.clone(), - inner: self.private.clone(), - } - } - - /// Get the JSON-LD representation of the verifier actor. - pub fn to_json_ld(&self) -> Value { - json!({ - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1", - ], - "id": self.actor_id, - "name": "Public key fetcher", - "publicKey": { - "id": self.key_id, - "owner": self.actor_id, - "publicKeyPem": self.public.encode_pem() - }, - "type": "Service", - }) - } - - /// Load the actor's verifier actor. - pub fn load(cfg: &Config) -> Verifier { - println!("[*] loading private key"); - let Config { ap_domain, state_dir, .. } = cfg; - let private = Private::load(format!("{state_dir}/fetcher.pem")); - println!("* done loading private key"); - Verifier { - actor_id: format!("https://{ap_domain}{VERIFIER_PATH}"), - key_id: format!("https://{ap_domain}{VERIFIER_PATH}#sig-key"), - public: private.get_public(), - private, - } - } - } - - /// An ActivityPub actor that signed a request. - pub struct Signer { - /// The ActivityPub ID (a URL) of the signer of the request. - pub ap_id: String, - } - - pub enum SigError { - FailedToFetchKey { keyid: String }, - ParseSignature { error: String }, - VerificationFailed { error: String }, - } - - // TODO: make it so we don't have to know what an "http request" is in this crate. - /// Check the signature for a request. - pub async fn verify_signature( - cx: &Context, - req: &fetch::http::Request + std::fmt::Debug>, - ) -> Result, SigError> { - println!(">>> starting signature verification for {req:#?}"); - - if req.uri().path() == VERIFIER_PATH { - // HACK: Allow access to the request verifier actor without checking the signature. - return Ok(None); - } - println!(">>> not going for the verifier!"); - - if req.headers().get("signature").is_none() { - // Request is not signed. - return Ok(None); - }; - println!(">>> has signature"); - - // Parse the signature. - let sig = match Signature::derive(&req) { - Ok(signature) => signature, - Err(error) => { - println!(">>> signature could not be parsed: {error}"); - return Err(SigError::ParseSignature { error }); - } - }; - println!(">>> signature is syntatically valid"); - - // Fetch the public key using the verifier private key. - let verifier = cx.verifier(); - let Ok(public_key) = verifier.fetch_public_key(sig.key_id()).await else { - return Err(SigError::FailedToFetchKey { - keyid: sig.key_id().to_string(), - }); - }; - println!(">>> public key fetched"); - - // Verify the signature header on the request. - if let Err(error) = public_key.verify(&sig) { - println!(">>> rejected"); - Err(SigError::VerificationFailed { error }) - } else { - println!(">>> request verified"); - Ok(Some(Signer { ap_id: public_key.owner })) - } - } -}