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
This commit is contained in:
Riley Apeldoorn 2024-05-02 19:29:32 +02:00
parent 8d350e8cd9
commit 564771931f
10 changed files with 878 additions and 593 deletions

View file

@ -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");

View file

@ -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<Response, Message> {
let Ok(parsed) = object_ulid.parse::<Key>() 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, <Option<String>>::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,32 +102,68 @@ 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<Response, Message> {
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::<puppy::data::Id>(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!({
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();
Ok(respond(200, Some(encoded), [WF_CONTENT_TYPE]))
}
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`].
fn get_handle<'x>((k, v): &'x (&str, &str)) -> Option<Handle<'x>> {
// We're looking for the `resource` query parameter.
if *k == "resource" {
// This prefix needs to exist according to spec.
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('@')?;
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": [
{
@ -112,35 +172,267 @@ pub mod wf {
"href": id
},
]
})
}
}
type Request = hyper::Request<hyper::body::Incoming>;
type Response<T = Full<Bytes>> = hyper::Response<T>;
/// Initialize the http server loop.
pub async fn start(context: Context) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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);
}
});
let encoded = serde_json::to_vec(&jrd).unwrap();
respond(200, Some(encoded), [WF_CONTENT_TYPE])
}
Some(_) => todo!(),
None => todo!("bad request: could not find valid resource parameter"),
}
}
}
pub struct Handle<'x> {
username: &'x str,
instance: &'x str,
}
// 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>,
}
/// Parse the `resource` parameter into a [`Handle`].
pub fn get_handle<'x>((k, v): &'x (&str, &str)) -> Option<Handle<'x>> {
// We're looking for the `resource` query parameter.
if *k == "resource" {
// This prefix needs to exist according to spec.
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 })
} else {
None
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<Bytes>) -> 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<Response, Infallible> {
// 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<Response, Message> {
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<Response, Message> {
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<Response, Message>,
) -> Result<Response, Message> {
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<const N: usize>(
status: u16,
body: Option<impl Into<Bytes>>,
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<Value>,
/// 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,
};
}

View file

@ -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<dyn std::error::Error + Send + Sync>> {
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<hyper::body::Incoming>;
type Response<T = Full<Bytes>> = hyper::Response<T>;
// 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<Bytes>) -> 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<Response, Infallible> {
// 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<const N: usize>(
status: u16,
body: Option<impl Into<Bytes>>,
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();
}

149
bin/server/src/sig.rs Normal file
View file

@ -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<B>(&self, req: &Request<B>) -> 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<VerificationKey, FetchError> {
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,
}

166
lib/fetch/src/client.rs Normal file
View file

@ -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<Value, FetchError> {
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<http::Response<String>, 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<reqwest::Body> = 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<http::request::Builder, FetchError> {
// 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::<http::Uri>()
.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)
}
}

View file

@ -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<String>,
summary: Option<String>,
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<Value, FetchError> {
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<http::Response<String>, 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<String>, 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,
}
}
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,
}),
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, .. })
}
/// 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<T = String> {
pub id: String,
pub actor: String,
pub object: Box<Object>,
pub kind: T,
}
impl<K> Activity<K>
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<Value> {
// 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::<http::Uri>().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<http::Response<String>> {
let date = Utc::now().format(RFC_822).to_string();
let uri = target.parse::<http::Uri>().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<reqwest::Body> = 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<String>,
/// 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<reqwest::Error> 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() }
}
}
}
}

100
lib/fetch/src/object.rs Normal file
View file

@ -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<T = String> {
pub id: String,
pub actor: String,
pub object: Box<Object>,
pub kind: T,
}
impl<K> Activity<K>
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<String>,
/// 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<String>,
summary: Option<String>,
},
}
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,
}),
}
}
}

View file

@ -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<Public>;
@ -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<String, Value>) -> Option<Key> {
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::<Public>::serialize
pub fn upgrade(self) -> Key<Public> {
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<Path>) -> Private {
pub fn load(path: impl AsRef<Path>) -> Option<Private> {
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<Path>) {
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 {

View file

@ -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<Verifier>,
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<Context> {
let store = Store::open(&config.state_dir, crate::data::schema())?;
Ok(Context { config, store })
}
/// Do a data store [transaction][store::Transaction].
pub fn run<T>(&self, f: impl FnOnce(&Transaction<'_>) -> Result<T>) -> Result<T> {
@ -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<T>(
schema: store::types::Schema,
test: impl FnOnce(Context) -> Result<T>,
) -> Result<T> {
Store::test(schema, |store| {
test(Context { config, store, verifier: None })
})
Store::test(schema, |store| test(Context { config, store }))
}

View file

@ -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<fetch::Object> {
pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::object::Object> {
let Some(obj) = tx.get_mixin::<Object>(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<fetch::Obje
let Some(PublicKey { key_id, key_pem }) = tx.get_mixin(key)? else {
return Err(Error::MissingData { node: key, prop: "PublicKey" });
};
Ok(fetch::Object::Actor(fetch::Actor {
Ok(fetch::object::Object::Actor(fetch::object::Actor {
id: obj.id.0.clone().into(),
inbox: inbox.into(),
account_name: account_name.0,
display_name,
public_key: fetch::Key {
public_key: fetch::object::PublicKey {
owner: obj.id.0.into(),
id: key_id.into(),
inner: key_pem,
@ -65,7 +65,7 @@ pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::Obje
panic!("expected a `Create`");
};
let Id(actor) = tx.get_alias(actor)?.unwrap();
Ok(fetch::Object::Activity(fetch::Activity {
Ok(fetch::object::Object::Activity(fetch::object::Activity {
id: obj.id.0.into(),
actor: actor.into(),
object: Box::new(get_local_ap_object(tx, object)?),
@ -76,7 +76,7 @@ pub fn get_local_ap_object(tx: &Transaction<'_>, key: Key) -> Result<fetch::Obje
let Some(Content { content, warning, .. }) = tx.get_mixin(key)? else {
panic!()
};
Ok(fetch::Object::Object {
Ok(fetch::object::Object::Object {
id: obj.id.0.clone().into(),
summary: warning,
content,
@ -199,134 +199,3 @@ pub mod config {
pub port: u16,
}
}
pub mod auth {
use fetch::signatures::{Private, Public, Signature};
use serde_json::{json, Value};
use crate::{config::Config, Context};
/// 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"];
impl Verifier {
/// 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<fetch::Key<Public>, 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<impl AsRef<[u8]> + std::fmt::Debug>,
) -> Result<Option<Signer>, 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 }))
}
}
}