actix_middleware_ed25519_authentication/
lib.rs

1//! Actix-web middleware for ed25519 signature validation of incoming requests.
2//!
3//! Provides a middleware that can be used to validate the signature of
4//! incoming requests. Offering these features:
5//! - Signature validation via public key
6//! - Customizable header names for signature and timestamp
7//! - Authentication status is available in the request extensions.
8//! - Optional automatic rejection of invalid requests
9//!
10//! # Example
11//!
12//! ```rust
13//! use actix_middleware_ed25519_authentication::AuthenticatorBuilder;
14//! use actix_web::{web, App, HttpResponse, HttpServer};
15//!
16//! HttpServer::new(move || {
17//!         App::new()
18//!             .wrap(
19//!                 AuthenticatorBuilder::new()
20//!                 .public_key(&public_key)
21//!                 .signature_header("X-Signature-Ed25519")
22//!                 .timestamp_header("X-Signature-Timestamp")
23//!                 .reject()
24//!                 .build()
25//!             )    
26//!             .route("/", web::post().to(HttpResponse::Ok))
27//!  })
28//! .bind(("127.0.0.1", 3000))?
29//! .run()
30//! .await
31//!```  
32//!
33use actix_http::h1::Payload;
34use actix_web::{
35    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
36    error::ErrorUnauthorized,
37    http::header::HeaderValue,
38    web::BytesMut,
39    Error, HttpMessage,
40};
41use ed25519_dalek::{PublicKey, Signature, SignatureError, Verifier};
42use futures_util::{future::LocalBoxFuture, FutureExt, StreamExt};
43use std::{future::Ready, pin::Pin, rc::Rc};
44
45#[derive(Default)]
46/// `AuthenticatorBuilder` is a [builder](https://rust-unofficial.github.io/patterns/patterns/creational/builder.html) struct that holds the public key, signature header, timestamp header,
47/// and a boolean value that indicates whether or not to reject requests.
48///
49/// Properties:
50///
51/// * `public_key`: The public key that will be used to verify the signature.
52/// **For a successful build of the authenticator, a public key will be required**.
53/// * `signature_header`: The name of the header that contains the signature.
54/// * `timestamp_header`: The name of the header that contains the timestamp.
55/// * `reject`: If true, the middleware will reject the request if it is not signed.
56///  If false, the middleware will allow the request to continue, adding [`AuthenticationInfo`] to the request extensions.
57pub struct AuthenticatorBuilder {
58    public_key: Option<String>,
59    signature_header: Option<String>,
60    timestamp_header: Option<String>,
61    reject: bool,
62}
63impl AuthenticatorBuilder {
64    /// Creates a new `AuthenticatorBuilder` with default values.
65    /// **Without a public key, the builder will panic on build.**
66    pub fn new() -> Self {
67        Self::default()
68    }
69    /// Sets the public key that will be used to verify the signature.
70    /// **Required.**
71    ///
72    /// # Arguments
73    /// * `public_key`: The public key that will be used to verify the signature. Must be a valid ed25519 public key for successful validation. Must be a hex-encoded string.
74    pub fn public_key(self, public_key: &str) -> Self {
75        Self {
76            public_key: Some(public_key.into()),
77            ..self
78        }
79    }
80    /// Sets the name of the header that contains the signature.
81    /// If not set, the default value is `X-Signature-Ed25519`.
82    /// *optional*
83    ///
84    /// # Arguments
85    /// * `header`: The name of the header that contains the signature.
86    pub fn signature_header(self, header: &str) -> Self {
87        Self {
88            signature_header: Some(header.into()),
89            ..self
90        }
91    }
92    /// Sets the name of the header that contains the timestamp.
93    /// If not set, the default value is `X-Signature-Timestamp`.
94    /// *optional*
95    ///
96    /// # Arguments
97    /// * `header`: The name of the header that contains the timestamp.
98    pub fn timestamp_header(self, header: &str) -> Self {
99        Self {
100            timestamp_header: Some(header.into()),
101            ..self
102        }
103    }
104    /// Sets whether or not to reject requests that are not signed.
105    /// If not set, the default value is `false`.
106    /// *optional*
107    pub fn reject(self) -> Self {
108        Self {
109            reject: true,
110            ..self
111        }
112    }
113    /// Converts the builder into the service factory [`Ed25519Authenticator`], as expected
114    /// by actix-web's [`wrap`](actix_web::App::wrap) function.
115    /// # Panics
116    /// If the builder is missing a public key, this function will panic.
117    ///
118    /// If the public key is not a valid ed25519 public key provided as a hex string, this function will panic.
119    pub fn build(self) -> Ed25519Authenticator {
120        let data: MiddlewareData = self.into();
121        data.into()
122    }
123}
124
125/// Ed25519Authenticator is a middleware factory that generates [`Ed25519AuthenticatorMiddleware`],
126/// which verifies the signature of incoming request.
127/// It is created through the [`AuthenticatorBuilder`] and consumed by actix-web's [`wrap`](actix_web::App::wrap) function.
128///
129/// It is a [transform](https://docs.rs/actix-web/4.1.0/actix_web/dev/trait.Transform.html) service factory.
130pub struct Ed25519Authenticator {
131    data: MiddlewareData,
132}
133
134impl<S, B> Transform<S, ServiceRequest> for Ed25519Authenticator
135where
136    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
137    S::Future: 'static,
138    B: 'static,
139{
140    type Response = ServiceResponse<B>;
141    type Error = Error;
142    type InitError = ();
143    type Transform = Ed25519AuthenticatorMiddleware<S>;
144    type Future = Ready<Result<Self::Transform, Self::InitError>>;
145
146    fn new_transform(&self, service: S) -> Self::Future {
147        std::future::ready(Ok(Ed25519AuthenticatorMiddleware {
148            service: Rc::new(service),
149            data: Rc::new(self.data.clone()),
150        }))
151    }
152}
153
154impl From<MiddlewareData> for Ed25519Authenticator {
155    fn from(data: MiddlewareData) -> Self {
156        Self { data }
157    }
158}
159
160/// MiddlewareData is a struct that holds the public key, signature header name, timestamp header name, and a boolean value that indicates whether or not to reject requests.
161/// When used with the [`authenticate_request`](fn.authenticate_request.html) function, the rejection boolean is ignored.
162#[derive(Clone, Debug)]
163pub struct MiddlewareData {
164    public_key: String,
165    signature_header: String,
166    timestamp_header: String,
167    reject: bool,
168}
169
170impl From<AuthenticatorBuilder> for MiddlewareData {
171    fn from(builder: AuthenticatorBuilder) -> Self {
172        Self {
173            public_key: builder.public_key.unwrap(),
174            signature_header: builder
175                .signature_header
176                .unwrap_or_else(|| "X-Signature-Ed25519".into()),
177            timestamp_header: builder
178                .timestamp_header
179                .unwrap_or_else(|| "X-Signature-Timestamp".into()),
180            reject: builder.reject,
181        }
182    }
183}
184
185impl MiddlewareData {
186    /// Creates a new `MiddlewareData` with default headers. Intended to be used with the [`authenticate_request`](fn.authenticate_request.html) function.
187    pub fn new(public_key: &str) -> Self {
188        Self {
189            public_key: public_key.into(),
190            signature_header: "X-Signature-Ed25519".into(),
191            timestamp_header: "X-Signature-Timestamp".into(),
192            reject: false,
193        }
194    }
195}
196
197/// AuthenticationInfo is a struct that holds information about the authentication of a request. This struct is added to the request extensions.
198#[derive(Debug)]
199pub struct AuthenticationInfo {
200    pub authenticated: bool,
201}
202
203/// Ed25519AuthenticatorMiddleware is a middleware that verifies the signature of incoming request.
204/// It is generated by the [`Ed25519Authenticator`] middleware factory and not intended to be used directly.
205///
206/// It is a [service](https://docs.rs/actix-web/4.1.0/actix_web/dev/trait.Service.html) middleware.
207pub struct Ed25519AuthenticatorMiddleware<S> {
208    service: Rc<S>,
209    data: Rc<MiddlewareData>,
210}
211
212impl<S, B> Service<ServiceRequest> for Ed25519AuthenticatorMiddleware<S>
213where
214    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
215    S::Future: 'static,
216    B: 'static,
217{
218    type Response = ServiceResponse<B>;
219    type Error = Error;
220    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
221
222    forward_ready!(service);
223
224    /// # Panics
225    /// If reject is set to false, and the signature is invalid, this function will panic. This is unimplemented behavior.
226    fn call(
227        &self,
228        mut req: ServiceRequest,
229    ) -> Pin<
230        Box<
231            (dyn futures_util::Future<Output = Result<ServiceResponse<B>, actix_web::Error>>
232                 + 'static),
233        >,
234    > {
235        let data = self.data.clone();
236        let srv = self.service.clone();
237
238        async move {
239            let verify = authenticate_request(&mut req, &data).await;
240
241            match (verify, data.reject) {
242                (Err(_), true) => Err(ErrorUnauthorized("Unauthorized")),
243                (Err(_), false) => {
244                    req.extensions_mut().insert(AuthenticationInfo {
245                        authenticated: false,
246                    });
247                    let fut = srv.call(req);
248                    let res = fut.await?;
249                    Ok(res)
250                }
251                (Ok(_), _) => {
252                    req.extensions_mut().insert(AuthenticationInfo {
253                        authenticated: true,
254                    });
255                    let fut = srv.call(req);
256                    let res = fut.await?;
257                    Ok(res)
258                }
259            }
260        }
261        .boxed_local()
262    }
263}
264
265/// authenticate_request is a function that verifies the signature of an incoming request.
266/// Intended to allow for manual handling of authentication, or for use with other middleware.
267///
268/// # Arguments
269/// * `req` - mutable reference to the incoming request
270/// * `data` - reference to the [`MiddlewareData`] struct
271///
272/// Note: This function does not add the [`AuthenticationInfo`] struct to the request extensions.
273/// Additionally, this function does not reject requests if the signature is invalid.
274/// Instead, it returns a [`Result`] with an [`Error`] if the signature is invalid.
275pub async fn authenticate_request(
276    req: &mut ServiceRequest,
277    data: &MiddlewareData,
278) -> Result<(), SignatureError> {
279    let (http_request, body) = req.parts_mut();
280
281    let default_header = HeaderValue::from_static("");
282
283    let public_key = PublicKey::from_bytes(&hex::decode(&data.public_key).unwrap_or_else(|_| {
284        println!("Couldn't decode public key!");
285        Vec::<u8>::new()
286    }))
287    .unwrap();
288
289    let timestamp = http_request
290        .headers()
291        .get(data.timestamp_header.clone())
292        .unwrap_or(&default_header);
293
294    let signature = {
295        let header = http_request
296            .headers()
297            .get(data.signature_header.clone())
298            .unwrap_or(&default_header);
299        let decoded_header = hex::decode(header).unwrap();
300
301        let mut sig_arr: [u8; 64] = [0; 64];
302        for (i, byte) in decoded_header.into_iter().enumerate() {
303            sig_arr[i] = byte;
304        }
305        Signature::from_bytes(&sig_arr).unwrap()
306    };
307
308    let mut payload = BytesMut::new();
309
310    while let Some(item) = body.next().await {
311        if let Ok(b) = item {
312            payload.extend_from_slice(&b)
313        }
314    }
315
316    let content = timestamp
317        .as_bytes()
318        .iter()
319        .chain(&payload)
320        .cloned()
321        .collect::<Vec<u8>>();
322
323    let (_, mut new_payload) = Payload::create(true);
324    new_payload.unread_data(payload.into());
325
326    match public_key.verify(&content, &signature) {
327        Err(e) => Err(e),
328        Ok(()) => {
329            req.set_payload(new_payload.into());
330            Ok(())
331        }
332    }
333}