refactor!: use rsa
instead of openssl
(#116)
* refactor!: use `rsa` instead of `openssl` * fix: format code * fix: format code * fix: lint code * fix: format code
This commit is contained in:
parent
08af457453
commit
8f47daa2e2
5 changed files with 82 additions and 61 deletions
21
Cargo.toml
21
Cargo.toml
|
@ -21,9 +21,9 @@ deprecated = "deny"
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
perf = { level = "deny", priority = -1 }
|
perf = { level = "deny", priority = -1 }
|
||||||
complexity = { level = "deny", priority = -1 }
|
complexity = { level = "deny", priority = -1 }
|
||||||
dbg_macro = "deny"
|
dbg_macro = "deny"
|
||||||
inefficient_to_string = "deny"
|
inefficient_to_string = "deny"
|
||||||
items-after-statements = "deny"
|
items-after-statements = "deny"
|
||||||
implicit_clone = "deny"
|
implicit_clone = "deny"
|
||||||
wildcard_imports = "deny"
|
wildcard_imports = "deny"
|
||||||
cast_lossless = "deny"
|
cast_lossless = "deny"
|
||||||
|
@ -41,10 +41,11 @@ reqwest = { version = "0.11.27", features = ["json", "stream"] }
|
||||||
reqwest-middleware = "0.2.5"
|
reqwest-middleware = "0.2.5"
|
||||||
tracing = "0.1.40"
|
tracing = "0.1.40"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
openssl = "0.10.64"
|
rand = "0.8.5"
|
||||||
|
rsa = "0.9.6"
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
http = "0.2.12"
|
http = "0.2.12"
|
||||||
sha2 = "0.10.8"
|
sha2 = { version = "0.10.8", features = ["oid"] }
|
||||||
thiserror = "1.0.59"
|
thiserror = "1.0.59"
|
||||||
derive_builder = "0.20.0"
|
derive_builder = "0.20.0"
|
||||||
itertools = "0.12.1"
|
itertools = "0.12.1"
|
||||||
|
@ -61,14 +62,19 @@ bytes = "1.6.0"
|
||||||
futures-core = { version = "0.3.30", default-features = false }
|
futures-core = { version = "0.3.30", default-features = false }
|
||||||
pin-project-lite = "0.2.14"
|
pin-project-lite = "0.2.14"
|
||||||
activitystreams-kinds = "0.3.0"
|
activitystreams-kinds = "0.3.0"
|
||||||
regex = { version = "1.10.5", default-features = false, features = ["std", "unicode"] }
|
regex = { version = "1.10.5", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"unicode",
|
||||||
|
] }
|
||||||
tokio = { version = "1.37.0", features = [
|
tokio = { version = "1.37.0", features = [
|
||||||
"sync",
|
"sync",
|
||||||
"rt",
|
"rt",
|
||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"time",
|
"time",
|
||||||
] }
|
] }
|
||||||
diesel = { version = "2.1.6", features = ["postgres"], default-features = false, optional = true }
|
diesel = { version = "2.1.6", features = [
|
||||||
|
"postgres",
|
||||||
|
], default-features = false, optional = true }
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
moka = { version = "0.12.7", features = ["future"] }
|
moka = { version = "0.12.7", features = ["future"] }
|
||||||
|
|
||||||
|
@ -82,11 +88,10 @@ axum = { version = "0.6.20", features = [
|
||||||
], default-features = false, optional = true }
|
], 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 }
|
||||||
http-body-util = {version = "0.1.1", optional = true }
|
http-body-util = { version = "0.1.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1.0.82"
|
anyhow = "1.0.82"
|
||||||
rand = "0.8.5"
|
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
tower-http = { version = "0.5.2", features = ["map-request-body", "util"] }
|
tower-http = { version = "0.5.2", features = ["map-request-body", "util"] }
|
||||||
axum = { version = "0.6.20", features = [
|
axum = { version = "0.6.20", features = [
|
||||||
|
|
|
@ -15,12 +15,12 @@ use futures::StreamExt;
|
||||||
use http::StatusCode;
|
use http::StatusCode;
|
||||||
use httpdate::fmt_http_date;
|
use httpdate::fmt_http_date;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use openssl::pkey::{PKey, Private};
|
|
||||||
use reqwest::{
|
use reqwest::{
|
||||||
header::{HeaderMap, HeaderName, HeaderValue},
|
header::{HeaderMap, HeaderName, HeaderValue},
|
||||||
Response,
|
Response,
|
||||||
};
|
};
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::{
|
use std::{
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
|
@ -37,7 +37,7 @@ pub struct SendActivityTask {
|
||||||
pub(crate) activity_id: Url,
|
pub(crate) activity_id: Url,
|
||||||
pub(crate) activity: Bytes,
|
pub(crate) activity: Bytes,
|
||||||
pub(crate) inbox: Url,
|
pub(crate) inbox: Url,
|
||||||
pub(crate) private_key: PKey<Private>,
|
pub(crate) private_key: RsaPrivateKey,
|
||||||
pub(crate) http_signature_compat: bool,
|
pub(crate) http_signature_compat: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ where
|
||||||
pub(crate) async fn get_pkey_cached<ActorType>(
|
pub(crate) async fn get_pkey_cached<ActorType>(
|
||||||
data: &Data<impl Clone>,
|
data: &Data<impl Clone>,
|
||||||
actor: &ActorType,
|
actor: &ActorType,
|
||||||
) -> Result<PKey<Private>, Error>
|
) -> Result<RsaPrivateKey, Error>
|
||||||
where
|
where
|
||||||
ActorType: Actor,
|
ActorType: Actor,
|
||||||
{
|
{
|
||||||
|
@ -189,13 +189,13 @@ where
|
||||||
|
|
||||||
// This is a mostly expensive blocking call, we don't want to tie up other tasks while this is happening
|
// This is a mostly expensive blocking call, we don't want to tie up other tasks while this is happening
|
||||||
let pkey = tokio::task::spawn_blocking(move || {
|
let pkey = tokio::task::spawn_blocking(move || {
|
||||||
PKey::private_key_from_pem(private_key_pem.as_bytes()).map_err(|err| {
|
RsaPrivateKey::from_pkcs8_pem(&private_key_pem).map_err(|err| {
|
||||||
Error::Other(format!("Could not create private key from PEM data:{err}"))
|
Error::Other(format!("Could not create private key from PEM data:{err}"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|err| Error::Other(format!("Error joining: {err}")))??;
|
.map_err(|err| Error::Other(format!("Error joining: {err}")))??;
|
||||||
std::result::Result::<PKey<Private>, Error>::Ok(pkey)
|
std::result::Result::<RsaPrivateKey, Error>::Ok(pkey)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| Error::Other(format!("cloned error: {e}")))
|
.map_err(|e| Error::Other(format!("cloned error: {e}")))
|
||||||
|
|
|
@ -24,8 +24,8 @@ use async_trait::async_trait;
|
||||||
use derive_builder::Builder;
|
use derive_builder::Builder;
|
||||||
use dyn_clone::{clone_trait_object, DynClone};
|
use dyn_clone::{clone_trait_object, DynClone};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
use openssl::pkey::{PKey, Private};
|
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::{
|
use std::{
|
||||||
ops::Deref,
|
ops::Deref,
|
||||||
|
@ -80,12 +80,12 @@ pub struct FederationConfig<T: Clone> {
|
||||||
/// This can be used to implement secure mode federation.
|
/// This can be used to implement secure mode federation.
|
||||||
/// <https://docs.joinmastodon.org/spec/activitypub/#secure-mode>
|
/// <https://docs.joinmastodon.org/spec/activitypub/#secure-mode>
|
||||||
#[builder(default = "None", setter(custom))]
|
#[builder(default = "None", setter(custom))]
|
||||||
pub(crate) signed_fetch_actor: Option<Arc<(Url, PKey<Private>)>>,
|
pub(crate) signed_fetch_actor: Option<Arc<(Url, RsaPrivateKey)>>,
|
||||||
#[builder(
|
#[builder(
|
||||||
default = "Cache::builder().max_capacity(10000).build()",
|
default = "Cache::builder().max_capacity(10000).build()",
|
||||||
setter(custom)
|
setter(custom)
|
||||||
)]
|
)]
|
||||||
pub(crate) actor_pkey_cache: Cache<Url, PKey<Private>>,
|
pub(crate) actor_pkey_cache: Cache<Url, RsaPrivateKey>,
|
||||||
/// Queue for sending outgoing activities. Only optional to make builder work, its always
|
/// Queue for sending outgoing activities. Only optional to make builder work, its always
|
||||||
/// present once constructed.
|
/// present once constructed.
|
||||||
#[builder(setter(skip))]
|
#[builder(setter(skip))]
|
||||||
|
@ -200,8 +200,8 @@ impl<T: Clone> FederationConfigBuilder<T> {
|
||||||
.private_key_pem()
|
.private_key_pem()
|
||||||
.expect("actor does not have a private key to sign with");
|
.expect("actor does not have a private key to sign with");
|
||||||
|
|
||||||
let private_key = PKey::private_key_from_pem(private_key_pem.as_bytes())
|
let private_key =
|
||||||
.expect("Could not decode PEM data");
|
RsaPrivateKey::from_pkcs8_pem(&private_key_pem).expect("Could not decode PEM data");
|
||||||
self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key))));
|
self.signed_fetch_actor = Some(Some(Arc::new((actor.id(), private_key))));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
21
src/error.rs
21
src/error.rs
|
@ -2,7 +2,10 @@
|
||||||
|
|
||||||
use crate::fetch::webfinger::WebFingerError;
|
use crate::fetch::webfinger::WebFingerError;
|
||||||
use http_signature_normalization_reqwest::SignError;
|
use http_signature_normalization_reqwest::SignError;
|
||||||
use openssl::error::ErrorStack;
|
use rsa::{
|
||||||
|
errors::Error as RsaError,
|
||||||
|
pkcs8::{spki::Error as SpkiError, Error as Pkcs8Error},
|
||||||
|
};
|
||||||
use std::string::FromUtf8Error;
|
use std::string::FromUtf8Error;
|
||||||
use tokio::task::JoinError;
|
use tokio::task::JoinError;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
@ -80,8 +83,20 @@ pub enum Error {
|
||||||
Other(String),
|
Other(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ErrorStack> for Error {
|
impl From<RsaError> for Error {
|
||||||
fn from(value: ErrorStack) -> Self {
|
fn from(value: RsaError) -> Self {
|
||||||
|
Error::Other(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Pkcs8Error> for Error {
|
||||||
|
fn from(value: Pkcs8Error) -> Self {
|
||||||
|
Error::Other(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SpkiError> for Error {
|
||||||
|
fn from(value: SpkiError) -> Self {
|
||||||
Error::Other(value.to_string())
|
Error::Other(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,17 +20,17 @@ use http_signature_normalization_reqwest::{
|
||||||
DefaultSpawner,
|
DefaultSpawner,
|
||||||
};
|
};
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use openssl::{
|
|
||||||
hash::MessageDigest,
|
|
||||||
pkey::{PKey, Private},
|
|
||||||
rsa::Rsa,
|
|
||||||
sign::{Signer, Verifier},
|
|
||||||
};
|
|
||||||
use reqwest::Request;
|
use reqwest::Request;
|
||||||
use reqwest_middleware::RequestBuilder;
|
use reqwest_middleware::RequestBuilder;
|
||||||
|
use rsa::{
|
||||||
|
pkcs8::{DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding},
|
||||||
|
Pkcs1v15Sign,
|
||||||
|
RsaPrivateKey,
|
||||||
|
RsaPublicKey,
|
||||||
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::{collections::BTreeMap, fmt::Debug, io::ErrorKind, time::Duration};
|
use std::{collections::BTreeMap, fmt::Debug, time::Duration};
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -46,27 +46,23 @@ pub struct Keypair {
|
||||||
impl Keypair {
|
impl Keypair {
|
||||||
/// Helper method to turn this into an openssl private key
|
/// Helper method to turn this into an openssl private key
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn private_key(&self) -> Result<PKey<Private>, anyhow::Error> {
|
pub(crate) fn private_key(&self) -> Result<RsaPrivateKey, anyhow::Error> {
|
||||||
Ok(PKey::private_key_from_pem(self.private_key.as_bytes())?)
|
use rsa::pkcs8::DecodePrivateKey;
|
||||||
|
|
||||||
|
Ok(RsaPrivateKey::from_pkcs8_pem(&self.private_key)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a random asymmetric keypair for ActivityPub HTTP signatures.
|
/// Generate a random asymmetric keypair for ActivityPub HTTP signatures.
|
||||||
pub fn generate_actor_keypair() -> Result<Keypair, std::io::Error> {
|
pub fn generate_actor_keypair() -> Result<Keypair, Error> {
|
||||||
let rsa = Rsa::generate(2048)?;
|
let mut rng = rand::thread_rng();
|
||||||
let pkey = PKey::from_rsa(rsa)?;
|
let rsa = RsaPrivateKey::new(&mut rng, 2048)?;
|
||||||
let public_key = pkey.public_key_to_pem()?;
|
let pkey = RsaPublicKey::from(&rsa);
|
||||||
let private_key = pkey.private_key_to_pem_pkcs8()?;
|
let public_key = pkey.to_public_key_pem(LineEnding::default())?;
|
||||||
let key_to_string = |key| match String::from_utf8(key) {
|
let private_key = rsa.to_pkcs8_pem(LineEnding::default())?.to_string();
|
||||||
Ok(s) => Ok(s),
|
|
||||||
Err(e) => Err(std::io::Error::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
format!("Failed converting key to string: {}", e),
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
Ok(Keypair {
|
Ok(Keypair {
|
||||||
private_key: key_to_string(private_key)?,
|
private_key,
|
||||||
public_key: key_to_string(public_key)?,
|
public_key,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +79,7 @@ pub(crate) async fn sign_request(
|
||||||
request_builder: RequestBuilder,
|
request_builder: RequestBuilder,
|
||||||
actor_id: &Url,
|
actor_id: &Url,
|
||||||
activity: Bytes,
|
activity: Bytes,
|
||||||
private_key: PKey<Private>,
|
private_key: RsaPrivateKey,
|
||||||
http_signature_compat: bool,
|
http_signature_compat: bool,
|
||||||
) -> Result<Request, Error> {
|
) -> Result<Request, Error> {
|
||||||
static CONFIG: Lazy<Config<DefaultSpawner>> =
|
static CONFIG: Lazy<Config<DefaultSpawner>> =
|
||||||
|
@ -106,10 +102,10 @@ pub(crate) async fn sign_request(
|
||||||
Sha256::new(),
|
Sha256::new(),
|
||||||
activity,
|
activity,
|
||||||
move |signing_string| {
|
move |signing_string| {
|
||||||
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
|
Ok(Base64.encode(private_key.sign(
|
||||||
signer.update(signing_string.as_bytes())?;
|
Pkcs1v15Sign::new::<Sha256>(),
|
||||||
|
&Sha256::digest(signing_string.as_bytes()),
|
||||||
Ok(Base64.encode(signer.sign_to_vec()?)) as Result<_, Error>
|
)?)) as Result<_, Error>
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -205,15 +201,19 @@ fn verify_signature_inner(
|
||||||
"Verifying with key {}, message {}",
|
"Verifying with key {}, message {}",
|
||||||
&public_key, &signing_string
|
&public_key, &signing_string
|
||||||
);
|
);
|
||||||
let public_key = PKey::public_key_from_pem(public_key.as_bytes())?;
|
let public_key = RsaPublicKey::from_public_key_pem(public_key)?;
|
||||||
let mut verifier = Verifier::new(MessageDigest::sha256(), &public_key)?;
|
|
||||||
verifier.update(signing_string.as_bytes())?;
|
|
||||||
|
|
||||||
let base64_decoded = Base64
|
let base64_decoded = Base64
|
||||||
.decode(signature)
|
.decode(signature)
|
||||||
.map_err(|err| Error::Other(err.to_string()))?;
|
.map_err(|err| Error::Other(err.to_string()))?;
|
||||||
|
|
||||||
Ok(verifier.verify(&base64_decoded)?)
|
Ok(public_key
|
||||||
|
.verify(
|
||||||
|
Pkcs1v15Sign::new::<Sha256>(),
|
||||||
|
&Sha256::digest(signing_string.as_bytes()),
|
||||||
|
&base64_decoded,
|
||||||
|
)
|
||||||
|
.is_ok())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if verified {
|
if verified {
|
||||||
|
@ -284,6 +284,7 @@ pub mod test {
|
||||||
use crate::activity_sending::generate_request_headers;
|
use crate::activity_sending::generate_request_headers;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
|
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::DecodePrivateKey};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
|
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
|
||||||
|
@ -306,7 +307,7 @@ pub mod test {
|
||||||
request_builder,
|
request_builder,
|
||||||
&ACTOR_ID,
|
&ACTOR_ID,
|
||||||
"my activity".into(),
|
"my activity".into(),
|
||||||
PKey::private_key_from_pem(test_keypair().private_key.as_bytes()).unwrap(),
|
RsaPrivateKey::from_pkcs8_pem(&test_keypair().private_key).unwrap(),
|
||||||
// set this to prevent created/expires headers to be generated and inserted
|
// set this to prevent created/expires headers to be generated and inserted
|
||||||
// automatically from current time
|
// automatically from current time
|
||||||
true,
|
true,
|
||||||
|
@ -342,7 +343,7 @@ pub mod test {
|
||||||
request_builder,
|
request_builder,
|
||||||
&ACTOR_ID,
|
&ACTOR_ID,
|
||||||
"my activity".to_string().into(),
|
"my activity".to_string().into(),
|
||||||
PKey::private_key_from_pem(test_keypair().private_key.as_bytes()).unwrap(),
|
RsaPrivateKey::from_pkcs8_pem(&test_keypair().private_key).unwrap(),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -378,13 +379,13 @@ pub mod test {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn test_keypair() -> Keypair {
|
pub fn test_keypair() -> Keypair {
|
||||||
let rsa = Rsa::private_key_from_pem(PRIVATE_KEY.as_bytes()).unwrap();
|
let rsa = RsaPrivateKey::from_pkcs1_pem(PRIVATE_KEY).unwrap();
|
||||||
let pkey = PKey::from_rsa(rsa).unwrap();
|
let pkey = RsaPublicKey::from(&rsa);
|
||||||
let private_key = pkey.private_key_to_pem_pkcs8().unwrap();
|
let public_key = pkey.to_public_key_pem(LineEnding::default()).unwrap();
|
||||||
let public_key = pkey.public_key_to_pem().unwrap();
|
let private_key = rsa.to_pkcs8_pem(LineEnding::default()).unwrap().to_string();
|
||||||
Keypair {
|
Keypair {
|
||||||
private_key: String::from_utf8(private_key).unwrap(),
|
private_key,
|
||||||
public_key: String::from_utf8(public_key).unwrap(),
|
public_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue