feat: verify inbox http signatures
This commit is contained in:
parent
d66f09d130
commit
46bbeea3ab
1 changed files with 63 additions and 38 deletions
|
@ -1,10 +1,13 @@
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts, StatusCode}};
|
use axum::{extract::{FromRef, FromRequestParts}, http::{header, request::Parts, HeaderMap, StatusCode}};
|
||||||
use openssl::hash::MessageDigest;
|
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 sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
use crate::{model, server::Context};
|
use crate::{errors::UpubError, model, server::Context};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Identity {
|
pub enum Identity {
|
||||||
|
@ -33,7 +36,7 @@ where
|
||||||
Context: FromRef<S>,
|
Context: FromRef<S>,
|
||||||
S: Send + Sync,
|
S: Send + Sync,
|
||||||
{
|
{
|
||||||
type Rejection = StatusCode;
|
type Rejection = UpubError;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let ctx = Context::from_ref(state);
|
let ctx = Context::from_ref(state);
|
||||||
|
@ -52,47 +55,65 @@ where
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Some(x)) => identity = Identity::Local(x.actor),
|
Ok(Some(x)) => identity = Identity::Local(x.actor),
|
||||||
Ok(None) => return Err(StatusCode::UNAUTHORIZED),
|
Ok(None) => return Err(UpubError::unauthorized()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("failed querying user session: {e}");
|
tracing::error!("failed querying user session: {e}");
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR)
|
return Err(UpubError::internal_server_error())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if let Some(sig) = parts
|
if let Some(sig) = parts
|
||||||
// .headers
|
.headers
|
||||||
// .get("Signature")
|
.get("Signature")
|
||||||
// .map(|v| v.to_str().unwrap_or(""))
|
.map(|v| v.to_str().unwrap_or(""))
|
||||||
// {
|
{
|
||||||
// let signature = HttpSignature::try_from(sig)?;
|
let mut signature_cfg = Config::new()
|
||||||
// let user_id = signature.key_id.split('#').next().unwrap_or("").to_string();
|
.dont_use_created_field()
|
||||||
// let data : String = signature.headers.iter()
|
.require_header("host")
|
||||||
// .map(|header| {
|
.require_header("date");
|
||||||
// if header == "(request-target)" {
|
let mut headers : BTreeMap<String, String> = [
|
||||||
// format!("(request-target): {} {}", parts.method, parts.uri)
|
("Signature".to_string(), sig.to_string()),
|
||||||
// } else {
|
("Host".to_string(), header_get(&parts.headers, "Host")),
|
||||||
// format!(
|
("Date".to_string(), header_get(&parts.headers, "Date")),
|
||||||
// "{header}: {}",
|
].into();
|
||||||
// parts.headers.get(header)
|
|
||||||
// .map(|h| h.to_str().unwrap_or(""))
|
|
||||||
// .unwrap_or("")
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .collect::<Vec<String>>() // TODO can we avoid this unneeded allocation?
|
|
||||||
// .join("\n");
|
|
||||||
|
|
||||||
// let user = ctx.fetch().user(&user_id).await.map_err(|_e| StatusCode::UNAUTHORIZED)?;
|
if parts.method == Method::POST {
|
||||||
// let pubkey = PKey::public_key_from_pem(user.public_key.as_bytes()).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
|
signature_cfg = signature_cfg.require_header("digest");
|
||||||
// let mut verifier = Verifier::new(signature.digest(), &pubkey).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
|
headers.insert("Digest".to_string(), header_get(&parts.headers, "Digest"));
|
||||||
// verifier.update(data.as_bytes()).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)?;
|
}
|
||||||
// if verifier.verify(signature.signature.as_bytes()).map_err(|_e| StatusCode::INTERNAL_SERVER_ERROR)? {
|
|
||||||
// identity = Identity::Remote(user_id);
|
let unverified = match signature_cfg.begin_verify(
|
||||||
// } else {
|
parts.method.as_str(),
|
||||||
// return Err(StatusCode::FORBIDDEN);
|
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", "");
|
||||||
|
let user = ctx.fetch().user(&user_id).await?;
|
||||||
|
let pubkey = PKey::public_key_from_pem(user.public_key.as_bytes())?;
|
||||||
|
|
||||||
|
let valid = unverified.verify(|sig, to_sign| {
|
||||||
|
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<bool>
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
return Err(UpubError::unauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO assert payload's digest is equal to signature's
|
||||||
|
|
||||||
|
// TODO introduce hardened mode which identifies remotes by user and not server
|
||||||
|
identity = Identity::Remote(Context::server(&user_id));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(AuthIdentity(identity))
|
Ok(AuthIdentity(identity))
|
||||||
}
|
}
|
||||||
|
@ -146,3 +167,7 @@ impl TryFrom<&str> for HttpSignature {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub fn header_get(headers: &HeaderMap, k: &str) -> String {
|
||||||
|
headers.get(k).map(|x| x.to_str().unwrap_or("")).unwrap_or("").to_string()
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue