fedimovies/src/http_signatures/verify.rs
2021-09-14 12:24:05 +00:00

198 lines
6.3 KiB
Rust

use actix_web::{
HttpRequest,
http::{HeaderMap, Method, Uri},
};
use regex::Regex;
use crate::activitypub::actor::Actor;
use crate::activitypub::fetcher::fetch_profile_by_actor_id;
use crate::config::Config;
use crate::database::{Pool, get_database_client};
use crate::errors::DatabaseError;
use crate::models::profiles::queries::{
get_profile_by_actor_id,
create_profile,
};
use crate::utils::crypto::{deserialize_public_key, verify_signature};
#[derive(thiserror::Error, Debug)]
pub enum VerificationError {
#[error("{0}")]
HeaderError(&'static str),
#[error("{0}")]
ParseError(&'static str),
#[error("invalid key ID")]
UrlError(#[from] url::ParseError),
#[error("actor error")]
ActorError,
#[error("invalid key")]
InvalidKey(#[from] rsa::pkcs8::Error),
#[error("invalid signature")]
InvalidSignature,
}
pub struct SignatureData {
pub actor_id: String,
pub message: String, // reconstructed message
pub signature: String, // base64-encoded signature
}
fn parse_http_signature(
request_method: &Method,
request_uri: &Uri,
request_headers: &HeaderMap,
) -> Result<SignatureData, VerificationError> {
let signature_header = request_headers.get("signature")
.ok_or(VerificationError::HeaderError("missing signature header"))?
.to_str()
.map_err(|_| VerificationError::HeaderError("invalid signature header"))?;
// TODO: support arbitrary parameter order
let signature_header_regexp_raw = concat!(
r#"keyId="(?P<key_id>.+)","#,
r#"headers="(?P<headers>.+)","#,
r#"signature="(?P<signature>.+)""#,
);
let signature_header_regexp = Regex::new(signature_header_regexp_raw).unwrap();
let signature_header_caps = signature_header_regexp
.captures(&signature_header)
.ok_or(VerificationError::HeaderError("invalid signature header"))?;
let key_id = signature_header_caps.name("key_id")
.ok_or(VerificationError::ParseError("keyId parameter is missing"))?
.as_str()
.to_owned();
let headers_parameter = signature_header_caps.name("headers")
.ok_or(VerificationError::ParseError("headers parameter is missing"))?
.as_str()
.to_owned();
let signature = signature_header_caps.name("signature")
.ok_or(VerificationError::ParseError("signature is missing"))?
.as_str()
.to_owned();
let mut message = format!(
"(request-target): {} {}",
request_method.as_str().to_lowercase(),
request_uri,
);
for header in headers_parameter.split(" ") {
if header == "(request-target)" {
continue;
}
let header_value = request_headers.get(header)
.ok_or(VerificationError::HeaderError("missing header"))?
.to_str()
.map_err(|_| VerificationError::HeaderError("invalid header value"))?;
let message_part = format!(
"\n{}: {}",
header,
header_value,
);
message.push_str(&message_part);
}
let key_url = url::Url::parse(&key_id)?;
let actor_id = &key_url[..url::Position::BeforeQuery];
let signature_data = SignatureData {
actor_id: actor_id.to_string(),
message,
signature,
};
Ok(signature_data)
}
pub async fn verify_http_signature(
config: &Config,
db_pool: &Pool,
request: &HttpRequest,
) -> Result<(), VerificationError> {
let signature_data = parse_http_signature(
request.method(),
request.uri(),
request.headers(),
)?;
let db_client = &**get_database_client(db_pool).await
.map_err(|_| VerificationError::ActorError)?;
let actor_profile = match get_profile_by_actor_id(db_client, &signature_data.actor_id).await {
Ok(profile) => profile,
Err(err) => match err {
DatabaseError::NotFound(_) => {
let profile_data = fetch_profile_by_actor_id(
&signature_data.actor_id,
&config.media_dir(),
).await.map_err(|err| {
log::error!("{}", err);
VerificationError::ActorError
})?;
let profile = create_profile(
db_client,
&profile_data,
).await.map_err(|_| VerificationError::ActorError)?;
profile
},
_ => {
return Err(VerificationError::ActorError);
},
},
};
let actor_value = actor_profile.actor_json.ok_or(VerificationError::ActorError)?;
let actor: Actor = serde_json::from_value(actor_value)
.map_err(|_| VerificationError::ActorError)?;
let public_key = deserialize_public_key(&actor.public_key.public_key_pem)?;
let is_valid_signature = verify_signature(
&public_key,
&signature_data.message,
&signature_data.signature,
).map_err(|_| VerificationError::InvalidSignature)?;
if !is_valid_signature {
return Err(VerificationError::InvalidSignature);
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use actix_web::http::{header, HeaderMap, HeaderName, HeaderValue, Uri};
use super::*;
#[test]
fn test_parse_signature() {
let request_method = Method::from_str("POST").unwrap();
let request_uri = "/user/123/inbox".parse::<Uri>().unwrap();
let mut request_headers = HeaderMap::new();
request_headers.insert(
header::HOST,
HeaderValue::from_static("example.com"),
);
let signature_header = concat!(
r#"keyId="https://myserver.org/actor#main-key","#,
r#"headers="(request-target) host","#,
r#"signature="test""#,
);
request_headers.insert(
HeaderName::from_static("signature"),
HeaderValue::from_static(signature_header),
);
let signature_data = parse_http_signature(
&request_method,
&request_uri,
&request_headers,
).unwrap();
assert_eq!(signature_data.actor_id, "https://myserver.org/actor");
assert_eq!(
signature_data.message,
"(request-target): post /user/123/inbox\nhost: example.com",
);
assert_eq!(signature_data.signature, "test");
}
}