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]
|
||||
perf = { level = "deny", priority = -1 }
|
||||
complexity = { level = "deny", priority = -1 }
|
||||
dbg_macro = "deny"
|
||||
dbg_macro = "deny"
|
||||
inefficient_to_string = "deny"
|
||||
items-after-statements = "deny"
|
||||
items-after-statements = "deny"
|
||||
implicit_clone = "deny"
|
||||
wildcard_imports = "deny"
|
||||
cast_lossless = "deny"
|
||||
|
@ -41,10 +41,11 @@ reqwest = { version = "0.11.27", features = ["json", "stream"] }
|
|||
reqwest-middleware = "0.2.5"
|
||||
tracing = "0.1.40"
|
||||
base64 = "0.22.1"
|
||||
openssl = "0.10.64"
|
||||
rand = "0.8.5"
|
||||
rsa = "0.9.6"
|
||||
once_cell = "1.19.0"
|
||||
http = "0.2.12"
|
||||
sha2 = "0.10.8"
|
||||
sha2 = { version = "0.10.8", features = ["oid"] }
|
||||
thiserror = "1.0.59"
|
||||
derive_builder = "0.20.0"
|
||||
itertools = "0.12.1"
|
||||
|
@ -61,14 +62,19 @@ bytes = "1.6.0"
|
|||
futures-core = { version = "0.3.30", default-features = false }
|
||||
pin-project-lite = "0.2.14"
|
||||
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 = [
|
||||
"sync",
|
||||
"rt",
|
||||
"rt-multi-thread",
|
||||
"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"
|
||||
moka = { version = "0.12.7", features = ["future"] }
|
||||
|
||||
|
@ -82,11 +88,10 @@ axum = { version = "0.6.20", features = [
|
|||
], default-features = false, optional = true }
|
||||
tower = { version = "0.4.13", 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]
|
||||
anyhow = "1.0.82"
|
||||
rand = "0.8.5"
|
||||
env_logger = "0.11.3"
|
||||
tower-http = { version = "0.5.2", features = ["map-request-body", "util"] }
|
||||
axum = { version = "0.6.20", features = [
|
||||
|
|
|
@ -15,12 +15,12 @@ use futures::StreamExt;
|
|||
use http::StatusCode;
|
||||
use httpdate::fmt_http_date;
|
||||
use itertools::Itertools;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use reqwest::{
|
||||
header::{HeaderMap, HeaderName, HeaderValue},
|
||||
Response,
|
||||
};
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
|
||||
use serde::Serialize;
|
||||
use std::{
|
||||
fmt::{Debug, Display},
|
||||
|
@ -37,7 +37,7 @@ pub struct SendActivityTask {
|
|||
pub(crate) activity_id: Url,
|
||||
pub(crate) activity: Bytes,
|
||||
pub(crate) inbox: Url,
|
||||
pub(crate) private_key: PKey<Private>,
|
||||
pub(crate) private_key: RsaPrivateKey,
|
||||
pub(crate) http_signature_compat: bool,
|
||||
}
|
||||
|
||||
|
@ -172,7 +172,7 @@ where
|
|||
pub(crate) async fn get_pkey_cached<ActorType>(
|
||||
data: &Data<impl Clone>,
|
||||
actor: &ActorType,
|
||||
) -> Result<PKey<Private>, Error>
|
||||
) -> Result<RsaPrivateKey, Error>
|
||||
where
|
||||
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
|
||||
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}"))
|
||||
})
|
||||
})
|
||||
.await
|
||||
.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
|
||||
.map_err(|e| Error::Other(format!("cloned error: {e}")))
|
||||
|
|
|
@ -24,8 +24,8 @@ use async_trait::async_trait;
|
|||
use derive_builder::Builder;
|
||||
use dyn_clone::{clone_trait_object, DynClone};
|
||||
use moka::future::Cache;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use rsa::{pkcs8::DecodePrivateKey, RsaPrivateKey};
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::{
|
||||
ops::Deref,
|
||||
|
@ -80,12 +80,12 @@ pub struct FederationConfig<T: Clone> {
|
|||
/// This can be used to implement secure mode federation.
|
||||
/// <https://docs.joinmastodon.org/spec/activitypub/#secure-mode>
|
||||
#[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(
|
||||
default = "Cache::builder().max_capacity(10000).build()",
|
||||
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
|
||||
/// present once constructed.
|
||||
#[builder(setter(skip))]
|
||||
|
@ -200,8 +200,8 @@ impl<T: Clone> FederationConfigBuilder<T> {
|
|||
.private_key_pem()
|
||||
.expect("actor does not have a private key to sign with");
|
||||
|
||||
let private_key = PKey::private_key_from_pem(private_key_pem.as_bytes())
|
||||
.expect("Could not decode PEM data");
|
||||
let private_key =
|
||||
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
|
||||
}
|
||||
|
|
21
src/error.rs
21
src/error.rs
|
@ -2,7 +2,10 @@
|
|||
|
||||
use crate::fetch::webfinger::WebFingerError;
|
||||
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 tokio::task::JoinError;
|
||||
use url::Url;
|
||||
|
@ -80,8 +83,20 @@ pub enum Error {
|
|||
Other(String),
|
||||
}
|
||||
|
||||
impl From<ErrorStack> for Error {
|
||||
fn from(value: ErrorStack) -> Self {
|
||||
impl From<RsaError> for Error {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,17 +20,17 @@ use http_signature_normalization_reqwest::{
|
|||
DefaultSpawner,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use openssl::{
|
||||
hash::MessageDigest,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
sign::{Signer, Verifier},
|
||||
};
|
||||
use reqwest::Request;
|
||||
use reqwest_middleware::RequestBuilder;
|
||||
use rsa::{
|
||||
pkcs8::{DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding},
|
||||
Pkcs1v15Sign,
|
||||
RsaPrivateKey,
|
||||
RsaPublicKey,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
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 url::Url;
|
||||
|
||||
|
@ -46,27 +46,23 @@ pub struct Keypair {
|
|||
impl Keypair {
|
||||
/// Helper method to turn this into an openssl private key
|
||||
#[cfg(test)]
|
||||
pub(crate) fn private_key(&self) -> Result<PKey<Private>, anyhow::Error> {
|
||||
Ok(PKey::private_key_from_pem(self.private_key.as_bytes())?)
|
||||
pub(crate) fn private_key(&self) -> Result<RsaPrivateKey, anyhow::Error> {
|
||||
use rsa::pkcs8::DecodePrivateKey;
|
||||
|
||||
Ok(RsaPrivateKey::from_pkcs8_pem(&self.private_key)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a random asymmetric keypair for ActivityPub HTTP signatures.
|
||||
pub fn generate_actor_keypair() -> Result<Keypair, std::io::Error> {
|
||||
let rsa = Rsa::generate(2048)?;
|
||||
let pkey = PKey::from_rsa(rsa)?;
|
||||
let public_key = pkey.public_key_to_pem()?;
|
||||
let private_key = pkey.private_key_to_pem_pkcs8()?;
|
||||
let key_to_string = |key| match String::from_utf8(key) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(e) => Err(std::io::Error::new(
|
||||
ErrorKind::Other,
|
||||
format!("Failed converting key to string: {}", e),
|
||||
)),
|
||||
};
|
||||
pub fn generate_actor_keypair() -> Result<Keypair, Error> {
|
||||
let mut rng = rand::thread_rng();
|
||||
let rsa = RsaPrivateKey::new(&mut rng, 2048)?;
|
||||
let pkey = RsaPublicKey::from(&rsa);
|
||||
let public_key = pkey.to_public_key_pem(LineEnding::default())?;
|
||||
let private_key = rsa.to_pkcs8_pem(LineEnding::default())?.to_string();
|
||||
Ok(Keypair {
|
||||
private_key: key_to_string(private_key)?,
|
||||
public_key: key_to_string(public_key)?,
|
||||
private_key,
|
||||
public_key,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -83,7 +79,7 @@ pub(crate) async fn sign_request(
|
|||
request_builder: RequestBuilder,
|
||||
actor_id: &Url,
|
||||
activity: Bytes,
|
||||
private_key: PKey<Private>,
|
||||
private_key: RsaPrivateKey,
|
||||
http_signature_compat: bool,
|
||||
) -> Result<Request, Error> {
|
||||
static CONFIG: Lazy<Config<DefaultSpawner>> =
|
||||
|
@ -106,10 +102,10 @@ pub(crate) async fn sign_request(
|
|||
Sha256::new(),
|
||||
activity,
|
||||
move |signing_string| {
|
||||
let mut signer = Signer::new(MessageDigest::sha256(), &private_key)?;
|
||||
signer.update(signing_string.as_bytes())?;
|
||||
|
||||
Ok(Base64.encode(signer.sign_to_vec()?)) as Result<_, Error>
|
||||
Ok(Base64.encode(private_key.sign(
|
||||
Pkcs1v15Sign::new::<Sha256>(),
|
||||
&Sha256::digest(signing_string.as_bytes()),
|
||||
)?)) as Result<_, Error>
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
@ -205,15 +201,19 @@ fn verify_signature_inner(
|
|||
"Verifying with key {}, message {}",
|
||||
&public_key, &signing_string
|
||||
);
|
||||
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())?;
|
||||
let public_key = RsaPublicKey::from_public_key_pem(public_key)?;
|
||||
|
||||
let base64_decoded = Base64
|
||||
.decode(signature)
|
||||
.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 {
|
||||
|
@ -284,6 +284,7 @@ pub mod test {
|
|||
use crate::activity_sending::generate_request_headers;
|
||||
use reqwest::Client;
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use rsa::{pkcs1::DecodeRsaPrivateKey, pkcs8::DecodePrivateKey};
|
||||
use std::str::FromStr;
|
||||
|
||||
static ACTOR_ID: Lazy<Url> = Lazy::new(|| Url::parse("https://example.com/u/alice").unwrap());
|
||||
|
@ -306,7 +307,7 @@ pub mod test {
|
|||
request_builder,
|
||||
&ACTOR_ID,
|
||||
"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
|
||||
// automatically from current time
|
||||
true,
|
||||
|
@ -342,7 +343,7 @@ pub mod test {
|
|||
request_builder,
|
||||
&ACTOR_ID,
|
||||
"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,
|
||||
)
|
||||
.await
|
||||
|
@ -378,13 +379,13 @@ pub mod test {
|
|||
}
|
||||
|
||||
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();
|
||||
let rsa = RsaPrivateKey::from_pkcs1_pem(PRIVATE_KEY).unwrap();
|
||||
let pkey = RsaPublicKey::from(&rsa);
|
||||
let public_key = pkey.to_public_key_pem(LineEnding::default()).unwrap();
|
||||
let private_key = rsa.to_pkcs8_pem(LineEnding::default()).unwrap().to_string();
|
||||
Keypair {
|
||||
private_key: String::from_utf8(private_key).unwrap(),
|
||||
public_key: String::from_utf8(public_key).unwrap(),
|
||||
private_key,
|
||||
public_key,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue