From bfb0a1d1b03148da74fd42837efc12a2c32f79ad Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 19 Jan 2024 00:28:02 +0100 Subject: [PATCH] SERVER HANDSHAKES ARE WORKING --- Cargo.lock | 2 + chat/Cargo.toml | 2 + chat/src/fed/mod.rs | 86 ++++++++++++++++++++++++++---------- chat/src/http/hello.rs | 66 ++++++++++++++++++++++++--- foxchat/src/fed/request.rs | 4 +- foxchat/src/fed/signature.rs | 12 ++--- foxchat/src/http/response.rs | 27 ++++++++--- 7 files changed, 157 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 867f7ec..40e3711 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -384,11 +384,13 @@ name = "chat" version = "0.1.0" dependencies = [ "axum", + "chrono", "clap", "color-eyre", "eyre", "foxchat", "rand", + "reqwest", "rsa", "serde", "serde_json", diff --git a/chat/Cargo.toml b/chat/Cargo.toml index 8ff139e..2f0efdf 100644 --- a/chat/Cargo.toml +++ b/chat/Cargo.toml @@ -22,3 +22,5 @@ tokio = { version = "1.35.1", features = ["macros", "rt-multi-thread"] } tracing-subscriber = "0.3.18" tracing = "0.1.40" tower-http = { version = "0.5.1", features = ["trace"] } +chrono = "0.4.31" +reqwest = { version = "0.11.23", features = ["json"] } diff --git a/chat/src/fed/mod.rs b/chat/src/fed/mod.rs index 2b6b7a5..816d6a7 100644 --- a/chat/src/fed/mod.rs +++ b/chat/src/fed/mod.rs @@ -9,6 +9,7 @@ use axum::{ }, Extension, }; +use chrono::{DateTime, Utc}; use foxchat::{ fed::{SERVER_HEADER, SIGNATURE_HEADER, USER_HEADER}, http::ApiError, @@ -19,6 +20,7 @@ use tracing::error; use crate::{app_state::AppState, model::identity_instance::IdentityInstance}; +/// A parsed and validated federation signature. pub struct FoxRequestData { pub instance: IdentityInstance, pub user_id: Option, @@ -32,17 +34,68 @@ where type Rejection = ApiError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let signature = FoxSignatureData::from_request_parts(parts, state).await?; let state: Extension> = Extension::from_request_parts(parts, state) .await .expect("AppState was not added as an extension"); + let instance = IdentityInstance::get(state.0.clone(), &signature.domain).await?; + let public_key = instance.parse_public_key()?; + if state.config.domain.clone() != signature.host { + return Err(FoxError::InvalidHeader.into()); + } + + if let Err(e) = verify_signature( + &public_key, + signature.signature, + signature.date, + &signature.host, + &signature.request_path, + signature.content_length, + signature.user_id.clone(), + ) { + error!( + "Verifying signature from request for {} from {}: {}", + parts.uri.path(), + signature.domain, + e + ); + + return Err(FoxError::InvalidSignature.into()); + } + + Ok(FoxRequestData { + instance, + user_id: signature.user_id, + }) + } +} + +// An unvalidated federation signature. Use with caution, you should almost always use `FoxRequestData` instead. +pub struct FoxSignatureData { + pub domain: String, + pub signature: String, + pub date: DateTime, + pub host: String, + pub request_path: String, + pub content_length: Option, + pub user_id: Option, +} + +#[async_trait] +impl FromRequestParts for FoxSignatureData +where + S: Send + Sync, +{ + type Rejection = ApiError; + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { let domain = parts .headers .get(SERVER_HEADER) .ok_or(FoxError::InvalidHeader)? - .to_str()?; - let instance = IdentityInstance::get(state.0, domain).await?; - let public_key = instance.parse_public_key()?; + .to_str()? + .to_string(); let date = parse_date( parts @@ -61,7 +114,8 @@ where .headers .get(HOST) .ok_or(FoxError::InvalidHeader)? - .to_str()?; + .to_str()? + .to_string(); let content_length = if let Some(raw_length) = parts.headers.get(CONTENT_LENGTH) { Some(raw_length.to_str()?.parse::()?) @@ -70,33 +124,19 @@ where }; let user_id = if let Some(raw_id) = parts.headers.get(USER_HEADER) { - Some(raw_id.to_str()?) + Some(raw_id.to_str()?.to_string()) } else { None }; - if let Err(e) = verify_signature( - &public_key, - signature, + Ok(FoxSignatureData { + domain, date, + signature, host, - parts.uri.path(), + request_path: parts.uri.path().to_string(), content_length, user_id, - ) { - error!( - "Verifying signature from request for {} from {}: {}", - parts.uri.path(), - domain, - e - ); - - return Err(FoxError::InvalidSignature.into()); - } - - Ok(FoxRequestData { - instance, - user_id: user_id.map(|v| v.to_string()), }) } } diff --git a/chat/src/http/hello.rs b/chat/src/http/hello.rs index 45d7414..c3932a9 100644 --- a/chat/src/http/hello.rs +++ b/chat/src/http/hello.rs @@ -1,21 +1,77 @@ use std::sync::Arc; use axum::{Extension, Json}; +use eyre::Context; use foxchat::{ + fed, http::ApiError, - s2s::http::{HelloRequest, HelloResponse, NodeResponse}, fed, + s2s::http::{HelloRequest, HelloResponse, NodeResponse}, + signature::verify_signature, + FoxError, }; +use rsa::{ + pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}, + RsaPublicKey, +}; +use tracing::error; +use ulid::Ulid; -use crate::{app_state::AppState, model::instance::Instance}; +use crate::{app_state::AppState, fed::FoxSignatureData, model::instance::Instance}; pub async fn post_hello( Extension(state): Extension>, + signature: FoxSignatureData, Json(data): Json, ) -> Result, ApiError> { let instance = Instance::get(&state.pool).await?; - let node = fed::get::(&instance.private_key, &state.config.domain, &data.host, "/_fox/ident/node", None).await?; + let node = fed::get::( + &instance.private_key, + &state.config.domain, + &data.host, + "/_fox/ident/node", + None, + ) + .await?; + let public_key = + RsaPublicKey::from_pkcs1_pem(&node.public_key).wrap_err("parsing remote public key")?; - // TODO: validate identity server's signature, probably adapt FoxRequestData.from_request_parts() (or extract it into a separate function? not sure if that's possible though) - todo!() + if state.config.domain.clone() != signature.host { + return Err(FoxError::InvalidHeader.into()); + } + + if let Err(e) = verify_signature( + &public_key, + signature.signature, + signature.date, + &signature.host, + &signature.request_path, + signature.content_length, + signature.user_id, + ) { + error!( + "Verifying signature from hello request from {}: {}", + signature.domain, e + ); + + return Err(FoxError::InvalidSignature.into()); + } + + sqlx::query!( + "insert into identity_instances (id, domain, base_url, public_key) values ($1, $2, $3, $4)", + Ulid::new().to_string(), + signature.domain, + format!("https://{}", signature.domain), + node.public_key, + ) + .execute(&state.pool) + .await?; + + Ok(Json(HelloResponse { + public_key: instance + .public_key + .to_pkcs1_pem(rsa::pkcs8::LineEnding::CR) + .wrap_err("formatting instance public key")?, + host: state.config.domain.clone(), + })) } diff --git a/foxchat/src/fed/request.rs b/foxchat/src/fed/request.rs index 537aa8d..409f653 100644 --- a/foxchat/src/fed/request.rs +++ b/foxchat/src/fed/request.rs @@ -30,7 +30,7 @@ pub async fn get( self_domain: &str, host: &str, path: &str, - user_id: Option<&str>, + user_id: Option, ) -> Result { let (signature, date) = build_signature( private_key, @@ -61,7 +61,7 @@ pub async fn post( self_domain: &str, host: &str, path: &str, - user_id: Option<&str>, + user_id: Option, body: &T, ) -> Result { let body = serde_json::to_string(body)?; diff --git a/foxchat/src/fed/signature.rs b/foxchat/src/fed/signature.rs index 0aa106a..44b07d1 100644 --- a/foxchat/src/fed/signature.rs +++ b/foxchat/src/fed/signature.rs @@ -13,7 +13,7 @@ pub fn build_signature( host: &str, request_path: &str, content_length: Option, - user_id: Option<&str>, + user_id: Option, ) -> (String, DateTime) { let mut rng = rand::thread_rng(); let signing_key = SigningKey::::new(private_key.clone()); @@ -32,13 +32,13 @@ fn plaintext_string( host: &str, request_path: &str, content_length: Option, - user_id: Option<&str>, + user_id: Option, ) -> String { let raw_time = format_date(time); let raw_content_length = content_length .map(|i| i.to_string()) .unwrap_or("".to_owned()); - let raw_user_id = user_id.unwrap_or(""); + let raw_user_id = user_id.unwrap_or("".into()); format!( "{}:{}:{}:{}:{}", @@ -61,14 +61,14 @@ pub fn verify_signature( host: &str, // from Host header, verify that it's actually your host request_path: &str, // from router content_length: Option, // from Content-Length header - user_id: Option<&str>, // from X-Foxchat-User header + user_id: Option, // from X-Foxchat-User header ) -> Result { let verifying_key = VerifyingKey::::new(public_key.clone()); let now = Utc::now(); - if (now - Duration::minutes(1)) < time { + if (now - Duration::minutes(1)) > time { return Err(FoxError::SignatureDateOutOfRange("request was made too long ago").into()); - } else if (now + Duration::minutes(1)) > time { + } else if (now + Duration::minutes(1)) < time { return Err(FoxError::SignatureDateOutOfRange("request was made in the future").into()); } diff --git a/foxchat/src/http/response.rs b/foxchat/src/http/response.rs index 0fd4602..4623ad5 100644 --- a/foxchat/src/http/response.rs +++ b/foxchat/src/http/response.rs @@ -86,10 +86,30 @@ impl From for ApiError { code: ErrorCode::ObjectNotFound, message: "Object not found".into(), }, + FoxError::SignatureDateOutOfRange(s) => ApiError { + status: StatusCode::BAD_REQUEST, + code: ErrorCode::InvalidSignature, + message: format!("Signature date out of range: {}", s), + }, + FoxError::ResponseNotOk => ApiError { + status: StatusCode::INTERNAL_SERVER_ERROR, + code: ErrorCode::InternalServerError, + message: "Error response from remote server".into(), + }, FoxError::InvalidServer => ApiError { status: StatusCode::BAD_REQUEST, code: ErrorCode::InvalidServer, - message: "Invalid domain or server".into(), + message: "Invalid remote server".into(), + }, + FoxError::InvalidHeader => ApiError { + status: StatusCode::BAD_REQUEST, + code: ErrorCode::InvalidHeader, + message: "Invalid header value".into(), + }, + FoxError::InvalidDate => ApiError { + status: StatusCode::BAD_REQUEST, + code: ErrorCode::InvalidHeader, + message: "Invalid date value in header".into(), }, FoxError::MissingSignature => ApiError { status: StatusCode::BAD_REQUEST, @@ -101,11 +121,6 @@ impl From for ApiError { code: ErrorCode::InvalidSignature, message: "Invalid signature".into(), }, - _ => ApiError { - status: StatusCode::INTERNAL_SERVER_ERROR, - code: ErrorCode::InternalServerError, - message: "Internal server error".into(), - }, } } }