Add signature tests, update dependencies, remove Cargo.lock from git

This commit is contained in:
Felix Ableitner 2023-03-30 21:22:38 +02:00
parent 6b4f798f76
commit c56f526914
6 changed files with 189 additions and 2306 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target /target
/.idea /.idea
/Cargo.lock

2263
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,41 +9,41 @@ repository = "https://github.com/LemmyNet/activitypub-federation-rust"
documentation = "https://docs.rs/activitypub_federation/" documentation = "https://docs.rs/activitypub_federation/"
[dependencies] [dependencies]
chrono = { version = "0.4.23", features = ["clock"], default-features = false } chrono = { version = "0.4.24", features = ["clock"], default-features = false }
serde = { version = "1.0.147", features = ["derive"] } serde = { version = "1.0.159", features = ["derive"] }
async-trait = "0.1.58" async-trait = "0.1.68"
url = { version = "2.3.1", features = ["serde"] } url = { version = "2.3.1", features = ["serde"] }
serde_json = { version = "1.0.87", features = ["preserve_order"] } serde_json = { version = "1.0.95", features = ["preserve_order"] }
anyhow = "1.0.66" anyhow = "1.0.70"
reqwest = { version = "0.11.12", features = ["json", "stream"] } reqwest = { version = "0.11.16", features = ["json", "stream"] }
reqwest-middleware = "0.2.0" reqwest-middleware = "0.2.1"
tracing = "0.1.37" tracing = "0.1.37"
base64 = "0.13.1" base64 = "0.21.0"
openssl = "0.10.42" openssl = "0.10.48"
once_cell = "1.16.0" once_cell = "1.17.1"
http = "0.2.8" http = "0.2.9"
sha2 = "0.10.6" sha2 = "0.10.6"
background-jobs = "0.13.0" background-jobs = "0.13.0"
thiserror = "1.0.37" thiserror = "1.0.40"
derive_builder = "0.12.0" derive_builder = "0.12.0"
itertools = "0.10.5" itertools = "0.10.5"
dyn-clone = "1.0.9" dyn-clone = "1.0.11"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"
httpdate = "1.0.2" httpdate = "1.0.2"
http-signature-normalization-reqwest = { version = "0.7.1", default-features = false, features = ["sha-2", "middleware"] } http-signature-normalization-reqwest = { version = "0.8.0", default-features = false, features = ["sha-2", "middleware"] }
http-signature-normalization = "0.6.0" http-signature-normalization = "0.7.0"
actix-rt = "2.7.0" actix-rt = "2.8.0"
bytes = "1.3.0" bytes = "1.4.0"
futures-core = { version = "0.3.25", default-features = false } futures-core = { version = "0.3.27", default-features = false }
pin-project-lite = "0.2.9" pin-project-lite = "0.2.9"
activitystreams-kinds = "0.2.1" activitystreams-kinds = "0.3.0"
regex = { version = "1.7.1", default-features = false, features = ["std"] } regex = { version = "1.7.3", default-features = false, features = ["std"] }
# Actix-web # Actix-web
actix-web = { version = "4.2.1", default-features = false, optional = true } actix-web = { version = "4.3.1", default-features = false, optional = true }
# Axum # Axum
axum = { version = "0.6.0", features = ["json", "headers"], default-features = false, optional = true } axum = { version = "0.6.12", features = ["json", "headers"], default-features = false, optional = true }
tower = { version = "0.4.13", optional = true } tower = { version = "0.4.13", optional = true }
hyper = { version = "0.14", optional = true } hyper = { version = "0.14", optional = true }
displaydoc = "0.2.3" displaydoc = "0.2.3"
@ -55,10 +55,10 @@ axum = ["dep:axum", "dep:tower", "dep:hyper"]
[dev-dependencies] [dev-dependencies]
rand = "0.8.5" rand = "0.8.5"
env_logger = "0.9.3" env_logger = "0.10.0"
tower-http = { version = "0.3", features = ["map-request-body", "util"] } tower-http = { version = "0.4.0", features = ["map-request-body", "util"] }
axum = { version = "0.6.0", features = ["http1", "tokio", "query"], default-features = false } axum = { version = "0.6.12", features = ["http1", "tokio", "query"], default-features = false }
axum-macros = "0.3.4" axum-macros = "0.3.7"
[profile.dev] [profile.dev]
strip = "symbols" strip = "symbols"

View file

@ -194,7 +194,7 @@ async fn do_send(
} }
} }
fn generate_request_headers(inbox_url: &Url) -> HeaderMap { pub(crate) fn generate_request_headers(inbox_url: &Url) -> HeaderMap {
let mut host = inbox_url.domain().expect("read inbox domain").to_string(); let mut host = inbox_url.domain().expect("read inbox domain").to_string();
if let Some(port) = inbox_url.port() { if let Some(port) = inbox_url.port() {
host = format!("{}:{}", host, port); host = format!("{}:{}", host, port);

View file

@ -55,6 +55,7 @@ where
mod test { mod test {
use super::*; use super::*;
use crate::{ use crate::{
activity_queue::generate_request_headers,
config::FederationConfig, config::FederationConfig,
http_signatures::sign_request, http_signatures::sign_request,
traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR}, traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR},
@ -62,6 +63,7 @@ mod test {
use actix_web::test::TestRequest; use actix_web::test::TestRequest;
use reqwest::Client; use reqwest::Client;
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use url::Url;
#[actix_rt::test] #[actix_rt::test]
async fn test_receive_activity() { async fn test_receive_activity() {
@ -109,8 +111,11 @@ mod test {
} }
async fn setup_receive_test() -> (String, TestRequest, FederationConfig<DbConnection>) { async fn setup_receive_test() -> (String, TestRequest, FederationConfig<DbConnection>) {
let request_builder = let inbox = "https://example.com/inbox";
ClientWithMiddleware::from(Client::default()).post("https://example.com/inbox"); let headers = generate_request_headers(&Url::parse(inbox).unwrap());
let request_builder = ClientWithMiddleware::from(Client::default())
.post(inbox)
.headers(headers);
let activity = Follow { let activity = Follow {
actor: ObjectId::parse("http://localhost:123").unwrap(), actor: ObjectId::parse("http://localhost:123").unwrap(),
object: ObjectId::parse("http://localhost:124").unwrap(), object: ObjectId::parse("http://localhost:124").unwrap(),

View file

@ -9,9 +9,10 @@ use crate::{
error::{Error, Error::ActivitySignatureInvalid}, error::{Error, Error::ActivitySignatureInvalid},
protocol::public_key::main_key_id, protocol::public_key::main_key_id,
}; };
use base64::{engine::general_purpose::STANDARD as Base64, Engine};
use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri}; use http::{header::HeaderName, uri::PathAndQuery, HeaderValue, Method, Uri};
use http_signature_normalization_reqwest::prelude::{Config, SignExt}; use http_signature_normalization_reqwest::prelude::{Config, SignExt};
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::Lazy;
use openssl::{ use openssl::{
hash::MessageDigest, hash::MessageDigest,
pkey::PKey, pkey::PKey,
@ -25,8 +26,6 @@ use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind};
use tracing::debug; use tracing::debug;
use url::Url; use url::Url;
static HTTP_SIG_CONFIG: OnceCell<Config> = OnceCell::new();
/// A private/public key pair used for HTTP signatures /// A private/public key pair used for HTTP signatures
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Keypair { pub struct Keypair {
@ -64,15 +63,14 @@ pub(crate) async fn sign_request(
private_key: String, private_key: String,
http_signature_compat: bool, http_signature_compat: bool,
) -> Result<Request, anyhow::Error> { ) -> Result<Request, anyhow::Error> {
static CONFIG: Lazy<Config> = Lazy::new(Config::new);
static CONFIG_COMPAT: Lazy<Config> = Lazy::new(|| Config::new().mastodon_compat());
let key_id = main_key_id(&actor_id); let key_id = main_key_id(&actor_id);
let sig_conf = HTTP_SIG_CONFIG.get_or_init(|| { let sig_conf = match http_signature_compat {
let c = Config::new(); false => CONFIG.clone(),
if http_signature_compat { true => CONFIG_COMPAT.clone(),
c.mastodon_compat() };
} else {
c
}
});
request_builder request_builder
.signature_with_digest( .signature_with_digest(
sig_conf.clone(), sig_conf.clone(),
@ -84,7 +82,7 @@ pub(crate) async fn sign_request(
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?; let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
signer.update(signing_string.as_bytes())?; signer.update(signing_string.as_bytes())?;
Ok(base64::encode(signer.sign_to_vec()?)) as Result<_, anyhow::Error> Ok(Base64.encode(signer.sign_to_vec()?)) as Result<_, anyhow::Error>
}, },
) )
.await .await
@ -122,7 +120,7 @@ where
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?; let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?; let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
verifier.update(signing_string.as_bytes())?; verifier.update(signing_string.as_bytes())?;
Ok(verifier.verify(&base64::decode(signature)?)?) Ok(verifier.verify(&Base64.decode(signature)?)?)
}) })
.map_err(Error::other)?; .map_err(Error::other)?;
@ -179,10 +177,152 @@ pub(crate) fn verify_inbox_hash(
for part in digest { for part in digest {
hasher.update(body); hasher.update(body);
if base64::encode(hasher.finalize_reset()) != part.digest { if Base64.encode(hasher.finalize_reset()) != part.digest {
return Err(Error::ActivityBodyDigestInvalid); return Err(Error::ActivityBodyDigestInvalid);
} }
} }
Ok(()) Ok(())
} }
#[cfg(test)]
pub mod test {
use super::*;
use crate::activity_queue::generate_request_headers;
use reqwest::Client;
use reqwest_middleware::ClientWithMiddleware;
use std::str::FromStr;
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
static INBOX_URL: Lazy<Url> =
Lazy::new(|| Url::parse("https://example.com/u/alice/inbox").unwrap());
#[actix_rt::test]
async fn test_sign() {
let mut headers = generate_request_headers(&INBOX_URL);
// use hardcoded date in order to test against hardcoded signature
headers.insert(
"date",
HeaderValue::from_str("Tue, 28 Mar 2023 21:03:44 GMT").unwrap(),
);
let request_builder = ClientWithMiddleware::from(Client::new())
.post(INBOX_URL.to_string())
.headers(headers);
let request = sign_request(
request_builder,
ACTOR_ID.clone(),
"my activity".to_string(),
test_keypair().private_key,
// set this to prevent created/expires headers to be generated and inserted
// automatically from current time
true,
)
.await
.unwrap();
let signature = request
.headers()
.get("signature")
.unwrap()
.to_str()
.unwrap();
let expected_signature = concat!(
"keyId=\"https://example.com/u/alice#main-key\",",
"algorithm=\"hs2019\",",
"headers=\"(request-target) content-type date digest host\",",
"signature=\"BpZhHNqzd6d6jhWOxyJ0jXwWWxiKMNK7i3mrr/5mVFnH7fUpicwqw8cSYVr",
"cwWjt0I07HW7rZFUfIdSgCoOEdvxtrccF/hTrwYgm8O6SQRHl1UfFtDR6e9EpfPieVmTjo0",
"QVfyzLLa41rmnz/yFqqer/v0kcdED51/dGe8NCGPBbhgK6C4oh7r+XHsQZMIhh38BcfZVWN",
"YaMqgyhFxu2f34IKnOEk6NjSaNtO+PzQUhbksTvH0Vvi6R0dtQINJFdONVBl4AwDC1INeF5",
"uhQo/SaKHfP3UitUHdM5Pbn+LhZYDB9AaQAW5ZGD43Aw15ecwsnKi4HcjV8nBw4zehlvaQ==\""
);
assert_eq!(signature, expected_signature);
}
#[actix_rt::test]
async fn test_verify() {
let headers = generate_request_headers(&INBOX_URL);
let request_builder = ClientWithMiddleware::from(Client::new())
.post(INBOX_URL.to_string())
.headers(headers);
let request = sign_request(
request_builder,
ACTOR_ID.clone(),
"my activity".to_string(),
test_keypair().private_key,
false,
)
.await
.unwrap();
let valid = verify_signature(
request.headers(),
request.method(),
&Uri::from_str(request.url().as_str()).unwrap(),
&test_keypair().public_key,
);
println!("{:?}", &valid);
assert!(valid.is_ok());
}
#[test]
fn test_verify_inbox_hash_valid() {
let digest_header =
HeaderValue::from_static("SHA-256=lzFT+G7C2hdI5j8M+FuJg1tC+O6AGMVJhooTCKGfbKM=");
let body = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
let valid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
println!("{:?}", &valid);
assert!(valid.is_ok());
}
#[test]
fn test_verify_inbox_hash_not_valid() {
let digest_header =
HeaderValue::from_static("SHA-256=Z9h7DJfYWjffXw2XftmWCnpEaK/yqOHKvzCIzIaqgbU=");
let body = "lorem ipsum";
let invalid = verify_inbox_hash(Some(&digest_header), body.as_bytes());
assert_eq!(invalid, Err(Error::ActivityBodyDigestInvalid));
}
pub fn test_keypair() -> Keypair {
let rsa = Rsa::private_key_from_pem(PRIVATE_KEY.as_bytes()).unwrap();
let pkey = PKey::from_rsa(rsa).unwrap();
let private_key = pkey.private_key_to_pem_pkcs8().unwrap();
let public_key = pkey.public_key_to_pem().unwrap();
Keypair {
private_key: String::from_utf8(private_key).unwrap(),
public_key: String::from_utf8(public_key).unwrap(),
}
}
/// Hardcoded private key so that signature doesn't change across runs
const PRIVATE_KEY: &str = concat!(
"-----BEGIN RSA PRIVATE KEY-----\n",
"MIIEogIBAAKCAQEA2kZpsvWYrwM9zMQiDwo4k6/VfpK2aDTeVe9ZkcvDrrWfqt72\n",
"QSjjtXLa8sxJlEn+/zbnZ1lG3AO/WsKs2jiOycNQHBS1ITnSZKEpdKnAoLUn4k16\n",
"YivRmALyLedOfIrvMtQzH8a+kOQ71u2Wa3H9jpkCT5W9OneEBa3VjQp49kcrF3tm\n",
"mrEUhfai5GJM4xrdr587y7exkBF4wObepta9opSeuBkPV4QXZPfgmjwW+oOTheVH\n",
"6L7yjzvjW92j4/T6XKAcu0kn/aQhR8SiGtPBMyOlcW4S2eDHWf1RlqbNGb5L9Qam\n",
"fb0WAymx0ANLUDQyXAu5zViMrd4g8mgdkg7C1wIDAQABAoIBAAHAT0Uvsguz0Frq\n",
"0Li8+A4I4U/RQeqW6f9XtHWpl3NSYuqOPJZY2DxypHRB1Iex13x/gBHH/8jwgShR\n",
"2x/3ev9kmsLu6f+CcdniCFQdFiRaVh/IFI0Ve7cz5tkcoiuSB2NDNcaYFwIdYqfr\n",
"Ytz2OCn2hLQHKB9M9pLMSnDsPmMAOveY11XfhkECrWlh1bx9YPyJScnNKTblB3M+\n",
"GhYL3xzuCxPCC9nUfqz7Y8FnZTCmePOwcRflJDTLFs6Bqkv1PZOZWzI+7akaJxfI\n",
"SOSw3VkGegsdoGVgHobqT2tqL8vuKM1bs47PFwWjVCGEoOvcC/Ha1+INemWbh7VA\n",
"Xa/jvxkCgYEA/+AxeMCLCmH/F696W3RpPdFL25wSYQr1auV2xRfmsT+hhpSp3yz/\n",
"ypkazS9TbnSCm18up+jE9rJ1c9VIZrgcTeKzPURzE68RR8uOsa9o9kaUzfyvRAzb\n",
"fmQXMvv2rmm9U7srhjpvKo1BcHpQIQYToKt0TOv7soSEY2jGNvaK6i0CgYEA2mGL\n",
"sL36WoHF3x2DZNvknLJGjxPSMmdjjfflFRqxKeP+Sf54C4QH/1hxHe/yl/KMBTfa\n",
"woBl05SrwTnQ7bOeR8VTmzP53JfkECT5I9h/g8vT8dkz5WQXWNDgy61Imq/UmWwm\n",
"DHElGrkF31oy5w6+aZ58Sa5bXhBDYpkUP9+pV5MCgYAW5BCo89i8gg3XKZyxp9Vu\n",
"cVXu/KRsSBWyjXq1oTDDNKUXrB8SVy0/C7lpF83H+OZiTf6XiOxuAYMebLtAbUIi\n",
"+Z/9YC1HWocaPCy02rNyLNhNIUjwtpHAWeX1arMj4VPNtNXs+TdOwDpVfKvEeI2y\n",
"9wO9ifMHgnFxj0MEUcQVtQKBgHg2Mhs8uM+RmEbVjDq9AP9w835XPuIYH6lKyIPx\n",
"iYyxwI0i0xojt/NL0BjWuQgDsCg/MuDWpTbvJAzdsrDmqz5+1SMeXXCc/CIW+D5P\n",
"MwJt9WGwWuzvSBrQAK6d2NWt7K335on6zp4DM8RbdqHSb+bcIza8D/ebpDxmX8s5\n",
"Z5KZAoGAX8u+63w1uy1FLhf48SqmjOqkAjdUZCWEmaim69koAOdTIBSSDOnAqzGu\n",
"wIVdLLzI6xTgbYmfErCwpU2v8MfUWr0BDzjQ9G6c5rhcS1BkfxbeAsC42XaVIgCk\n",
"2sMNMqi6f96jbp4IQI70BpecsnBAUa+VoT57bZRvy0lW26w9tYI=\n",
"-----END RSA PRIVATE KEY-----\n"
);
}