SERVER HANDSHAKES ARE WORKING

This commit is contained in:
sam 2024-01-19 00:28:02 +01:00
parent 041531e88a
commit bfb0a1d1b0
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
7 changed files with 157 additions and 42 deletions

2
Cargo.lock generated
View file

@ -384,11 +384,13 @@ name = "chat"
version = "0.1.0"
dependencies = [
"axum",
"chrono",
"clap",
"color-eyre",
"eyre",
"foxchat",
"rand",
"reqwest",
"rsa",
"serde",
"serde_json",

View file

@ -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"] }

View file

@ -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<String>,
@ -32,17 +34,68 @@ where
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let signature = FoxSignatureData::from_request_parts(parts, state).await?;
let state: Extension<Arc<AppState>> = 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<Utc>,
pub host: String,
pub request_path: String,
pub content_length: Option<usize>,
pub user_id: Option<String>,
}
#[async_trait]
impl<S> FromRequestParts<S> for FoxSignatureData
where
S: Send + Sync,
{
type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
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::<usize>()?)
@ -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()),
})
}
}

View file

@ -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<Arc<AppState>>,
signature: FoxSignatureData,
Json(data): Json<HelloRequest>,
) -> Result<Json<HelloResponse>, ApiError> {
let instance = Instance::get(&state.pool).await?;
let node = fed::get::<NodeResponse>(&instance.private_key, &state.config.domain, &data.host, "/_fox/ident/node", None).await?;
let node = fed::get::<NodeResponse>(
&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(),
}))
}

View file

@ -30,7 +30,7 @@ pub async fn get<R: DeserializeOwned>(
self_domain: &str,
host: &str,
path: &str,
user_id: Option<&str>,
user_id: Option<String>,
) -> Result<R> {
let (signature, date) = build_signature(
private_key,
@ -61,7 +61,7 @@ pub async fn post<T: Serialize, R: DeserializeOwned>(
self_domain: &str,
host: &str,
path: &str,
user_id: Option<&str>,
user_id: Option<String>,
body: &T,
) -> Result<R> {
let body = serde_json::to_string(body)?;

View file

@ -13,7 +13,7 @@ pub fn build_signature(
host: &str,
request_path: &str,
content_length: Option<usize>,
user_id: Option<&str>,
user_id: Option<String>,
) -> (String, DateTime<Utc>) {
let mut rng = rand::thread_rng();
let signing_key = SigningKey::<Sha256>::new(private_key.clone());
@ -32,13 +32,13 @@ fn plaintext_string(
host: &str,
request_path: &str,
content_length: Option<usize>,
user_id: Option<&str>,
user_id: Option<String>,
) -> 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<usize>, // from Content-Length header
user_id: Option<&str>, // from X-Foxchat-User header
user_id: Option<String>, // from X-Foxchat-User header
) -> Result<bool> {
let verifying_key = VerifyingKey::<Sha256>::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());
}

View file

@ -86,10 +86,30 @@ impl From<FoxError> 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<FoxError> for ApiError {
code: ErrorCode::InvalidSignature,
message: "Invalid signature".into(),
},
_ => ApiError {
status: StatusCode::INTERNAL_SERVER_ERROR,
code: ErrorCode::InternalServerError,
message: "Internal server error".into(),
},
}
}
}