From 8f47daa2e21b26c8cda6e1241f1b62fff7d60278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=97=8D+85CD?= <50108258+kwaa@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:20:35 +0800 Subject: [PATCH] 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 --- Cargo.toml | 21 +++++++---- src/activity_sending.rs | 10 ++--- src/config.rs | 10 ++--- src/error.rs | 21 +++++++++-- src/http_signatures.rs | 81 +++++++++++++++++++++-------------------- 5 files changed, 82 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 522a84a..b16df17 100644 --- a/Cargo.toml +++ b/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 = [ diff --git a/src/activity_sending.rs b/src/activity_sending.rs index 4af8439..f9023ce 100644 --- a/src/activity_sending.rs +++ b/src/activity_sending.rs @@ -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, + pub(crate) private_key: RsaPrivateKey, pub(crate) http_signature_compat: bool, } @@ -172,7 +172,7 @@ where pub(crate) async fn get_pkey_cached( data: &Data, actor: &ActorType, -) -> Result, Error> +) -> Result 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::, Error>::Ok(pkey) + std::result::Result::::Ok(pkey) }) .await .map_err(|e| Error::Other(format!("cloned error: {e}"))) diff --git a/src/config.rs b/src/config.rs index f5ccfff..2015750 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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 { /// This can be used to implement secure mode federation. /// #[builder(default = "None", setter(custom))] - pub(crate) signed_fetch_actor: Option)>>, + pub(crate) signed_fetch_actor: Option>, #[builder( default = "Cache::builder().max_capacity(10000).build()", setter(custom) )] - pub(crate) actor_pkey_cache: Cache>, + pub(crate) actor_pkey_cache: Cache, /// Queue for sending outgoing activities. Only optional to make builder work, its always /// present once constructed. #[builder(setter(skip))] @@ -200,8 +200,8 @@ impl FederationConfigBuilder { .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 } diff --git a/src/error.rs b/src/error.rs index d2f7c87..1866e48 100644 --- a/src/error.rs +++ b/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 for Error { - fn from(value: ErrorStack) -> Self { +impl From for Error { + fn from(value: RsaError) -> Self { + Error::Other(value.to_string()) + } +} + +impl From for Error { + fn from(value: Pkcs8Error) -> Self { + Error::Other(value.to_string()) + } +} + +impl From for Error { + fn from(value: SpkiError) -> Self { Error::Other(value.to_string()) } } diff --git a/src/http_signatures.rs b/src/http_signatures.rs index 1f4e15b..aa526f9 100644 --- a/src/http_signatures.rs +++ b/src/http_signatures.rs @@ -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, anyhow::Error> { - Ok(PKey::private_key_from_pem(self.private_key.as_bytes())?) + pub(crate) fn private_key(&self) -> Result { + 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 { - 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 { + 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_key: RsaPrivateKey, http_signature_compat: bool, ) -> Result { static CONFIG: Lazy> = @@ -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::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::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 = 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, } }