From 437859008c0511b066d5f81e09cfd45b34c42fa6 Mon Sep 17 00:00:00 2001 From: alemi Date: Sat, 13 Apr 2024 05:26:50 +0200 Subject: [PATCH] feat: reimplemented from scratch http sig verify --- Cargo.toml | 1 - src/server/auth.rs | 144 +++++++++++++++++++----------------------- src/server/fetcher.rs | 19 ------ 3 files changed, 66 insertions(+), 98 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 79822fbc..1ba9a864 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ axum = "0.7" apb = { path = "apb", features = ["unstructured", "fetch", "orm"] } # nodeinfo = "0.0.2" # the version on crates.io doesn't re-export necessary types to build the struct!!! nodeinfo = { git = "https://codeberg.org/thefederationinfo/nodeinfo-rs", rev = "e865094804" } -http-signature-normalization = "0.7.0" # migrations sea-orm-migration = { version = "0.12", optional = true } # mastodon diff --git a/src/server/auth.rs b/src/server/auth.rs index d6947bbe..7725fe36 100644 --- a/src/server/auth.rs +++ b/src/server/auth.rs @@ -1,10 +1,6 @@ -use std::collections::BTreeMap; - -use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts, HeaderMap, StatusCode}}; +use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts}}; use base64::Engine; -use http_signature_normalization::Config; use openssl::{hash::MessageDigest, pkey::PKey, sign::Verifier}; -use reqwest::Method; use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter}; use crate::{errors::UpubError, model, server::Context}; @@ -68,49 +64,20 @@ where .get("Signature") .map(|v| v.to_str().unwrap_or("")) { - let mut signature_cfg = Config::new().mastodon_compat(); - let mut headers : BTreeMap = [ - ("Signature".to_string(), sig.to_string()), - ("Host".to_string(), header_get(&parts.headers, "Host")), - ("Date".to_string(), header_get(&parts.headers, "Date")), - ].into(); + let http_signature = HttpSignature::parse(sig); - if parts.method == Method::POST { - signature_cfg = signature_cfg.require_header("digest"); - headers.insert("Digest".to_string(), header_get(&parts.headers, "Digest")); - } - - let unverified = match signature_cfg.begin_verify( - parts.method.as_str(), - parts.uri.path_and_query().map(|x| x.as_str()).unwrap_or("/"), - headers - ) { - Ok(x) => x, - Err(e) => { - tracing::error!("failed preparing signature verification context: {e}"); - return Err(UpubError::internal_server_error()); - } - }; - - let user_id = unverified.key_id().replace("#main-key", ""); - if let Ok(user) = ctx.fetch().user(&user_id).await { - let valid = unverified.verify(|sig, to_sign| { - let pubkey = PKey::public_key_from_pem(user.public_key.as_bytes())?; - let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap(); - verifier.update(to_sign.as_bytes())?; - Ok(verifier.verify(&base64::prelude::BASE64_URL_SAFE.decode(sig).unwrap_or_default())?) as crate::Result - }); - - // TODO assert payload's digest is equal to signature's - - match valid { - // TODO introduce hardened mode which identifies remotes by user and not server - Ok(true) => identity = Identity::Remote(Context::server(&user_id)), - Ok(false) => return Err(UpubError::unauthorized()), - Err(e) => { - tracing::error!("failed verifying signature: {e}"); - }, - } + let user_id = http_signature.key_id.replace("#main-key", ""); + match ctx.fetch().user(&user_id).await { + Ok(user) => { + let to_sign = http_signature.build_string(parts); + // TODO assert payload's digest is equal to signature's + match verify_control_text(&to_sign, &user.public_key, &http_signature.signature) { + Ok(true) => identity = Identity::Remote(Context::server(&user_id)), + Ok(false) => tracing::warn!("invalid signature"), + Err(e) => tracing::error!("error verifying signature: {e}"), + } + }, + Err(e) => tracing::warn!("could not fetch user (won't verify): {e}"), } } @@ -118,7 +85,26 @@ where } } -#[allow(unused)] // TODO am i gonna reimplement http signatures for verification? + + +fn verify_control_text(txt: &str, key: &str, control: &str) -> crate::Result { + let pubkey = PKey::public_key_from_pem(key.as_bytes())?; + let mut verifier = Verifier::new(MessageDigest::sha256(), &pubkey).unwrap(); + verifier.update(txt.as_bytes())?; + Ok(verifier.verify(&base64::prelude::BASE64_URL_SAFE.decode(control).unwrap_or_default())?) +} + + + + + + + + + + + +#[derive(Debug, Clone, Default)] pub struct HttpSignature { key_id: String, algorithm: String, @@ -127,7 +113,38 @@ pub struct HttpSignature { } impl HttpSignature { - #[allow(unused)] // TODO am i gonna reimplement http signatures for verification? + pub fn parse(header: &str) -> Self { + let mut sig = HttpSignature::default(); + header.split(',') + .filter_map(|x| x.split_once('=')) + .map(|(k, v)| (k, v.trim_end_matches('"').trim_matches('"'))) + .for_each(|(k, v)| match k { + "keyId" => sig.key_id = v.to_string(), + "algorithm" => sig.algorithm = v.to_string(), + "signature" => sig.signature = v.to_string(), + "headers" => sig.headers = v.split(' ').map(|x| x.to_string()).collect(), + _ => tracing::warn!("unexpected field in http signature: '{k}=\"{v}\"'"), + }); + sig + } + + pub fn build_string(&self, parts: &Parts) -> String { + let mut out = Vec::new(); + for header in self.headers.iter() { + match header.as_str() { + "(request-target)" => out.push( + format!("(request-target): {}", parts.uri.path_and_query().map(|x| x.as_str()).unwrap_or("/")) + ), + // TODO other pseudo-headers, + _ => out.push(format!("{}: {}", + header.to_lowercase(), + parts.headers.get(header).map(|x| x.to_str().unwrap_or("")).unwrap_or("") + )), + } + } + out.join("\n") + } + pub fn digest(&self) -> MessageDigest { match self.algorithm.as_str() { "rsa-sha512" => MessageDigest::sha512(), @@ -141,32 +158,3 @@ impl HttpSignature { } } } - -impl TryFrom<&str> for HttpSignature { - type Error = StatusCode; // TODO: quite ad hoc... - - fn try_from(value: &str) -> Result { - let parameters : BTreeMap = value - .split(',') - .filter_map(|s| { // TODO kinda ugly, can be made nicer? - let (k, v) = s.split_once("=\"")?; - let (k, mut v) = (k.to_string(), v.to_string()); - v.pop(); - Some((k, v)) - }).collect(); - - let sig = HttpSignature { - key_id: parameters.get("keyId").ok_or(StatusCode::BAD_REQUEST)?.to_string(), - algorithm: parameters.get("algorithm").ok_or(StatusCode::BAD_REQUEST)?.to_string(), - headers: parameters.get("headers").map(|x| x.split(' ').map(|x| x.to_string()).collect()).unwrap_or(vec!["date".to_string()]), - signature: parameters.get("signature").ok_or(StatusCode::BAD_REQUEST)?.to_string(), - }; - - Ok(sig) - } -} - - -pub fn header_get(headers: &HeaderMap, k: &str) -> String { - headers.get(k).map(|x| x.to_str().unwrap_or("")).unwrap_or("").to_string() -} diff --git a/src/server/fetcher.rs b/src/server/fetcher.rs index 3dbb7e60..5daa769c 100644 --- a/src/server/fetcher.rs +++ b/src/server/fetcher.rs @@ -43,7 +43,6 @@ impl Fetcher { .header("Host", host.clone()) .header("Date", date.clone()); - // let mut signature_cfg = Config::new().mastodon_compat(); let mut to_sign_raw = format!("(request-target): {} {path}\nhost: {host}\ndate: {date}", method.to_string().to_lowercase()); let mut headers_to_inspect = "(request-target) host date"; @@ -51,27 +50,11 @@ impl Fetcher { let digest = format!("sha-256={}", base64::prelude::BASE64_STANDARD.encode(openssl::sha::sha256(payload.as_bytes()))); to_sign_raw = format!("(request-target): {} {path}\nhost: {host}\ndate: {date}\ndigest: {digest}", method.to_string().to_lowercase()); headers_to_inspect = "(request-target) host date digest"; - // headers.insert("Digest".to_string(), digest.clone()); - // signature_cfg = signature_cfg.require_header("digest"); client = client .header("Digest", digest) .body(payload.to_string()); } - // let signature_header = signature_cfg - // .begin_sign("POST", &path, headers) - // .unwrap() - // .sign(format!("{from}#main-key"), |to_sign| { - // // tracing::info!("signature string:\nlib>> {to_sign}\nraw>> {to_sign_raw}"); - // let mut signer = Signer::new(MessageDigest::sha256(), key)?; - // signer.update(to_sign.as_bytes())?; - // let signature = base64::prelude::BASE64_URL_SAFE.encode(signer.sign_to_vec()?); - // Ok(signature) as crate::Result<_> - // }) - // .unwrap() - // .signature_header() - // .replace("hs2019", "rsa-sha256"); // TODO what the fuck??? why isn't this customizable??? - let signature_header = { let mut signer = Signer::new(MessageDigest::sha256(), key).unwrap(); signer.update(to_sign_raw.as_bytes()).unwrap(); @@ -79,8 +62,6 @@ impl Fetcher { format!("keyId=\"{from}#main-key\",algorithm=\"rsa-sha256\",headers=\"{headers_to_inspect}\",signature=\"{signature}\"") }; - // tracing::info!("signature headers:\nlib>> {signature_header_lib}\nraw>> {signature_header}"); - client .header("Signature", signature_header) .send()