diff options
-rw-r--r-- | Cargo.lock | 57 | ||||
-rw-r--r-- | contrib/bruno/users/follow.bru | 29 | ||||
-rw-r--r-- | crates/sellershut/Cargo.toml | 6 | ||||
-rw-r--r-- | crates/sellershut/src/server/activities/follow.rs | 5 | ||||
-rw-r--r-- | crates/sellershut/src/server/middleware/sign_request.rs | 49 | ||||
-rw-r--r-- | crates/sellershut/src/server/middleware/sign_request/signature.rs | 59 |
6 files changed, 190 insertions, 15 deletions
@@ -739,6 +739,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] name = "form_urlencoded" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1546,6 +1561,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" [[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2059,17 +2112,19 @@ dependencies = [ "anyhow", "async-trait", "axum", + "base64", "clap", "config", "enum_delegate", "futures-util", - "hmac", "nanoid", + "openssl", "serde", "serde_json", "sha2", "sqlx", "stack-up", + "time", "tokio", "tower", "tower-http", diff --git a/contrib/bruno/users/follow.bru b/contrib/bruno/users/follow.bru new file mode 100644 index 0000000..89cf4e1 --- /dev/null +++ b/contrib/bruno/users/follow.bru @@ -0,0 +1,29 @@ +meta { + name: follow + type: http + seq: 3 +} + +post { + url: {{HUT_HOSTNAME}}/users/sellershut/inbox + body: none + auth: inherit +} + +headers { + Content-Type: application/activity+json +} + +script:pre-request { + const { nanoid } = require("nanoid") + + const hostname = bru.getEnvVar("HUT_HOSTNAME"); + + req.setBody({ + "@context": "https://www.w3.org/ns/activitystreams", + "id": `${hostname}/activity/follow/${nanoid()}`, + "type": "Follow", + "actor": `http://localhost/users/sellershut`, + "object": `http://localhost/users/sellershut` + }) +} diff --git a/crates/sellershut/Cargo.toml b/crates/sellershut/Cargo.toml index 43948f8..7501da5 100644 --- a/crates/sellershut/Cargo.toml +++ b/crates/sellershut/Cargo.toml @@ -12,19 +12,21 @@ activitypub_federation = { version = "0.7.0-beta.5", default-features = false, f anyhow.workspace = true async-trait.workspace = true axum = { workspace = true, features = ["macros"] } +base64 = "0.22.1" clap = { workspace = true, features = ["derive"] } config = { workspace = true, features = ["toml"] } enum_delegate = "0.2.0" futures-util.workspace = true -hmac = "0.12.1" nanoid.workspace = true +openssl = "0.10.73" serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2 = "0.10.9" sqlx = { workspace = true, features = ["macros", "migrate", "runtime-tokio", "time", "tls-rustls", "uuid"] } +time = { version = "0.3.41", default-features = false, features = ["parsing"] } tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } tower = { workspace = true, features = ["util"] } -tower-http = { workspace = true, features = ["trace"] } +tower-http = { workspace = true, features = ["map-request-body", "trace", "util"] } tracing.workspace = true url.workspace = true uuid = { workspace = true, features = ["v7"] } diff --git a/crates/sellershut/src/server/activities/follow.rs b/crates/sellershut/src/server/activities/follow.rs index a004c6b..f0df0d9 100644 --- a/crates/sellershut/src/server/activities/follow.rs +++ b/crates/sellershut/src/server/activities/follow.rs @@ -119,8 +119,8 @@ mod tests { config.application.port ); let actor = format!( - "http://localhost:{}/users/{}", - config.application.port, hut_config.instance_name + "http://localhost/users/{}", + hut_config.instance_name ); let app = server::router(state); @@ -138,6 +138,7 @@ mod tests { .oneshot( Request::builder() .method("POST") + .header("Content-Type", "application/activity+json") .uri("/users/sellershut/inbox") .body(Body::from(body)) .unwrap(), diff --git a/crates/sellershut/src/server/middleware/sign_request.rs b/crates/sellershut/src/server/middleware/sign_request.rs index a8f2f3a..889984f 100644 --- a/crates/sellershut/src/server/middleware/sign_request.rs +++ b/crates/sellershut/src/server/middleware/sign_request.rs @@ -1,14 +1,20 @@ +mod signature; + use activitypub_federation::config::FederationConfig; -use axum::{body::Body, extract::Request, response::Response}; +use axum::{ + body::Body, + extract::Request, + http::HeaderValue, + response::Response, +}; use futures_util::future::BoxFuture; -use hmac::{Hmac, Mac}; -use sha2::{Sha256, digest::KeyInit}; -use std::task::{Context, Poll}; +use std::{ + task::{Context, Poll}, +}; use tower::{Layer, Service}; -use crate::state::AppHandle; +use crate::{server::middleware::sign_request::signature::Signature, state::AppHandle}; -type HmacSha256 = Hmac<Sha256>; #[derive(Clone)] pub struct SignRequestLayer { @@ -42,7 +48,7 @@ pub struct SignRequestMiddleware<S> { impl<S> Service<Request> for SignRequestMiddleware<S> where - S: Service<Request, Response = Response> + Send + 'static, + S: Service<Request, Response = Response> + Clone + Send + 'static, S::Future: Send + 'static, { type Response = S::Response; @@ -55,10 +61,33 @@ where } fn call(&mut self, request: Request) -> Self::Future { - let future = self.inner.call(request); + let mut inner = self.inner.clone(); + let (parts, body) = request.into_parts(); + Box::pin(async move { - let response: Response = future.await?; - Ok(response) + let bytes = axum::body::to_bytes(body, usize::MAX).await.unwrap(); + + let signature = Signature::create( + "" + .as_bytes(), + bytes, + ) + .unwrap(); + + let mut new_request = Request::from_parts(parts, Body::from(signature.body)); + + let head = new_request.headers_mut(); + let header = format!( + "keyId=\"http://localhost/users/sellershut#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest\",signature=\"{}\"", + signature.signature, + ); + println!("{header}"); + head.insert("Host", HeaderValue::from_str(&signature.host).unwrap()); + head.insert("Date", HeaderValue::from_str(&signature.date).unwrap()); + head.insert("Digest", HeaderValue::from_str(&signature.digest).unwrap()); + head.insert("Signature", HeaderValue::from_str(&header).unwrap()); + + inner.call(new_request).await }) } } diff --git a/crates/sellershut/src/server/middleware/sign_request/signature.rs b/crates/sellershut/src/server/middleware/sign_request/signature.rs new file mode 100644 index 0000000..18e62c0 --- /dev/null +++ b/crates/sellershut/src/server/middleware/sign_request/signature.rs @@ -0,0 +1,59 @@ +use axum::body::Bytes; +use base64::{Engine, engine::general_purpose}; +use openssl::{pkey::PKey, rsa::Rsa, sign::Signer}; +use sha2::{Digest, Sha256}; +use sqlx::types::time::OffsetDateTime; +use time::format_description::well_known::Rfc2822; + +use crate::error::AppError; + +/// Signature state for verifying a request +pub struct Signature { + pub host: String, + pub date: String, + pub digest: String, + pub signature: String, + pub body: Bytes +} + +impl Signature { + pub fn create(key: &[u8], body: Bytes) -> Result<Self, AppError> { + let mut hasher = Sha256::new(); + hasher.update(&body); + + let result = hasher.finalize(); + + let digest_hash = general_purpose::STANDARD.encode(result); + let digest = format!("SHA-256={digest_hash}"); + + let inbox_path = "/users/sellershut/inbox"; + let now = OffsetDateTime::now_utc().format(&Rfc2822)?; + + let signing_string = [ + format!("(request-target): post {inbox_path}"), + "host: localhost:2210".to_owned(), + format!("date: {now}"), + format!("digest: {digest}"), + ] + .join("\n"); + + let rsa = Rsa::private_key_from_pem(key)?; + let pkey = PKey::from_rsa(rsa)?; + + // Create signer with SHA-256 + let mut signer = Signer::new(openssl::hash::MessageDigest::sha256(), &pkey)?; + signer.update(signing_string.as_bytes())?; + + // Sign and encode as base64 + let signature = signer.sign_to_vec()?; + let result = general_purpose::STANDARD.encode(signature); + + Ok(Self{ + host: "localhost:2210".to_string(), + date: now, + digest, + signature: result, + body, + }) + } +} |