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}