Add signature tests, update dependencies, remove Cargo.lock from git
This commit is contained in:
parent
6b4f798f76
commit
c56f526914
6 changed files with 189 additions and 2306 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,2 +1,3 @@
|
|||
/target
|
||||
/.idea
|
||||
/Cargo.lock
|
||||
|
|
2263
Cargo.lock
generated
2263
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
52
Cargo.toml
52
Cargo.toml
|
@ -9,41 +9,41 @@ repository = "https://github.com/LemmyNet/activitypub-federation-rust"
|
|||
documentation = "https://docs.rs/activitypub_federation/"
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4.23", features = ["clock"], default-features = false }
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
async-trait = "0.1.58"
|
||||
chrono = { version = "0.4.24", features = ["clock"], default-features = false }
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
async-trait = "0.1.68"
|
||||
url = { version = "2.3.1", features = ["serde"] }
|
||||
serde_json = { version = "1.0.87", features = ["preserve_order"] }
|
||||
anyhow = "1.0.66"
|
||||
reqwest = { version = "0.11.12", features = ["json", "stream"] }
|
||||
reqwest-middleware = "0.2.0"
|
||||
serde_json = { version = "1.0.95", features = ["preserve_order"] }
|
||||
anyhow = "1.0.70"
|
||||
reqwest = { version = "0.11.16", features = ["json", "stream"] }
|
||||
reqwest-middleware = "0.2.1"
|
||||
tracing = "0.1.37"
|
||||
base64 = "0.13.1"
|
||||
openssl = "0.10.42"
|
||||
once_cell = "1.16.0"
|
||||
http = "0.2.8"
|
||||
base64 = "0.21.0"
|
||||
openssl = "0.10.48"
|
||||
once_cell = "1.17.1"
|
||||
http = "0.2.9"
|
||||
sha2 = "0.10.6"
|
||||
background-jobs = "0.13.0"
|
||||
thiserror = "1.0.37"
|
||||
thiserror = "1.0.40"
|
||||
derive_builder = "0.12.0"
|
||||
itertools = "0.10.5"
|
||||
dyn-clone = "1.0.9"
|
||||
dyn-clone = "1.0.11"
|
||||
enum_delegate = "0.2.0"
|
||||
httpdate = "1.0.2"
|
||||
http-signature-normalization-reqwest = { version = "0.7.1", default-features = false, features = ["sha-2", "middleware"] }
|
||||
http-signature-normalization = "0.6.0"
|
||||
actix-rt = "2.7.0"
|
||||
bytes = "1.3.0"
|
||||
futures-core = { version = "0.3.25", default-features = false }
|
||||
http-signature-normalization-reqwest = { version = "0.8.0", default-features = false, features = ["sha-2", "middleware"] }
|
||||
http-signature-normalization = "0.7.0"
|
||||
actix-rt = "2.8.0"
|
||||
bytes = "1.4.0"
|
||||
futures-core = { version = "0.3.27", default-features = false }
|
||||
pin-project-lite = "0.2.9"
|
||||
activitystreams-kinds = "0.2.1"
|
||||
regex = { version = "1.7.1", default-features = false, features = ["std"] }
|
||||
activitystreams-kinds = "0.3.0"
|
||||
regex = { version = "1.7.3", default-features = false, features = ["std"] }
|
||||
|
||||
# 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 = { 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 }
|
||||
hyper = { version = "0.14", optional = true }
|
||||
displaydoc = "0.2.3"
|
||||
|
@ -55,10 +55,10 @@ axum = ["dep:axum", "dep:tower", "dep:hyper"]
|
|||
|
||||
[dev-dependencies]
|
||||
rand = "0.8.5"
|
||||
env_logger = "0.9.3"
|
||||
tower-http = { version = "0.3", features = ["map-request-body", "util"] }
|
||||
axum = { version = "0.6.0", features = ["http1", "tokio", "query"], default-features = false }
|
||||
axum-macros = "0.3.4"
|
||||
env_logger = "0.10.0"
|
||||
tower-http = { version = "0.4.0", features = ["map-request-body", "util"] }
|
||||
axum = { version = "0.6.12", features = ["http1", "tokio", "query"], default-features = false }
|
||||
axum-macros = "0.3.7"
|
||||
|
||||
[profile.dev]
|
||||
strip = "symbols"
|
||||
|
|
|
@ -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();
|
||||
if let Some(port) = inbox_url.port() {
|
||||
host = format!("{}:{}", host, port);
|
||||
|
|
|
@ -55,6 +55,7 @@ where
|
|||
mod test {
|
||||
use super::*;
|
||||
use crate::{
|
||||
activity_queue::generate_request_headers,
|
||||
config::FederationConfig,
|
||||
http_signatures::sign_request,
|
||||
traits::tests::{DbConnection, DbUser, Follow, DB_USER_KEYPAIR},
|
||||
|
@ -62,6 +63,7 @@ mod test {
|
|||
use actix_web::test::TestRequest;
|
||||
use reqwest::Client;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use url::Url;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_receive_activity() {
|
||||
|
@ -109,8 +111,11 @@ mod test {
|
|||
}
|
||||
|
||||
async fn setup_receive_test() -> (String, TestRequest, FederationConfig<DbConnection>) {
|
||||
let request_builder =
|
||||
ClientWithMiddleware::from(Client::default()).post("https://example.com/inbox");
|
||||
let inbox = "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 {
|
||||
actor: ObjectId::parse("http://localhost:123").unwrap(),
|
||||
object: ObjectId::parse("http://localhost:124").unwrap(),
|
||||
|
|
|
@ -9,9 +9,10 @@ use crate::{
|
|||
error::{Error, Error::ActivitySignatureInvalid},
|
||||
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_signature_normalization_reqwest::prelude::{Config, SignExt};
|
||||
use once_cell::sync::{Lazy, OnceCell};
|
||||
use once_cell::sync::Lazy;
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::PKey,
|
||||
|
@ -25,8 +26,6 @@ use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind};
|
|||
use tracing::debug;
|
||||
use url::Url;
|
||||
|
||||
static HTTP_SIG_CONFIG: OnceCell<Config> = OnceCell::new();
|
||||
|
||||
/// A private/public key pair used for HTTP signatures
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Keypair {
|
||||
|
@ -64,15 +63,14 @@ pub(crate) async fn sign_request(
|
|||
private_key: String,
|
||||
http_signature_compat: bool,
|
||||
) -> 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 sig_conf = HTTP_SIG_CONFIG.get_or_init(|| {
|
||||
let c = Config::new();
|
||||
if http_signature_compat {
|
||||
c.mastodon_compat()
|
||||
} else {
|
||||
c
|
||||
}
|
||||
});
|
||||
let sig_conf = match http_signature_compat {
|
||||
false => CONFIG.clone(),
|
||||
true => CONFIG_COMPAT.clone(),
|
||||
};
|
||||
request_builder
|
||||
.signature_with_digest(
|
||||
sig_conf.clone(),
|
||||
|
@ -84,7 +82,7 @@ pub(crate) async fn sign_request(
|
|||
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
|
||||
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
|
||||
|
@ -122,7 +120,7 @@ where
|
|||
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
|
||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
|
||||
verifier.update(signing_string.as_bytes())?;
|
||||
Ok(verifier.verify(&base64::decode(signature)?)?)
|
||||
Ok(verifier.verify(&Base64.decode(signature)?)?)
|
||||
})
|
||||
.map_err(Error::other)?;
|
||||
|
||||
|
@ -179,10 +177,152 @@ pub(crate) fn verify_inbox_hash(
|
|||
|
||||
for part in digest {
|
||||
hasher.update(body);
|
||||
if base64::encode(hasher.finalize_reset()) != part.digest {
|
||||
if Base64.encode(hasher.finalize_reset()) != part.digest {
|
||||
return Err(Error::ActivityBodyDigestInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue