WIP
This commit is contained in:
		
							
								
								
									
										63
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										63
									
								
								virtweb_backend/Cargo.lock
									
									
									
										generated
									
									
									
								
							@@ -1224,8 +1224,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "wasi",
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
@@ -1596,6 +1598,21 @@ dependencies = [
 | 
			
		||||
 "wasm-bindgen",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "jsonwebtoken"
 | 
			
		||||
version = "9.3.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "base64 0.21.7",
 | 
			
		||||
 "js-sys",
 | 
			
		||||
 "pem",
 | 
			
		||||
 "ring",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 "simple_asn1",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "language-tags"
 | 
			
		||||
version = "0.3.2"
 | 
			
		||||
@@ -2042,6 +2059,16 @@ version = "1.0.14"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "pem"
 | 
			
		||||
version = "3.0.4"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "base64 0.22.0",
 | 
			
		||||
 "serde",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "percent-encoding"
 | 
			
		||||
version = "2.3.1"
 | 
			
		||||
@@ -2376,6 +2403,21 @@ dependencies = [
 | 
			
		||||
 "bytemuck",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "ring"
 | 
			
		||||
version = "0.17.8"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "cc",
 | 
			
		||||
 "cfg-if",
 | 
			
		||||
 "getrandom",
 | 
			
		||||
 "libc",
 | 
			
		||||
 "spin",
 | 
			
		||||
 "untrusted",
 | 
			
		||||
 "windows-sys 0.52.0",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "rust-embed"
 | 
			
		||||
version = "8.3.0"
 | 
			
		||||
@@ -2620,6 +2662,18 @@ dependencies = [
 | 
			
		||||
 "quote",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "simple_asn1"
 | 
			
		||||
version = "0.6.2"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085"
 | 
			
		||||
dependencies = [
 | 
			
		||||
 "num-bigint",
 | 
			
		||||
 "num-traits",
 | 
			
		||||
 "thiserror",
 | 
			
		||||
 "time",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "slab"
 | 
			
		||||
version = "0.4.9"
 | 
			
		||||
@@ -3020,6 +3074,12 @@ dependencies = [
 | 
			
		||||
 "subtle",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "untrusted"
 | 
			
		||||
version = "0.9.0"
 | 
			
		||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
			
		||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 | 
			
		||||
 | 
			
		||||
[[package]]
 | 
			
		||||
name = "url"
 | 
			
		||||
version = "2.5.0"
 | 
			
		||||
@@ -3137,6 +3197,7 @@ dependencies = [
 | 
			
		||||
 "futures-util",
 | 
			
		||||
 "image",
 | 
			
		||||
 "ipnetwork",
 | 
			
		||||
 "jsonwebtoken",
 | 
			
		||||
 "lazy-regex",
 | 
			
		||||
 "lazy_static",
 | 
			
		||||
 "light-openid",
 | 
			
		||||
@@ -3144,9 +3205,11 @@ dependencies = [
 | 
			
		||||
 "mime_guess",
 | 
			
		||||
 "nix",
 | 
			
		||||
 "num",
 | 
			
		||||
 "pem",
 | 
			
		||||
 "quick-xml",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "reqwest",
 | 
			
		||||
 "ring",
 | 
			
		||||
 "rust-embed",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_json",
 | 
			
		||||
 
 | 
			
		||||
@@ -45,3 +45,6 @@ rust-embed = { version = "8.3.0" }
 | 
			
		||||
mime_guess = "2.0.4"
 | 
			
		||||
dotenvy = "0.15.7"
 | 
			
		||||
nix = { version = "0.28.0", features = ["net"] }
 | 
			
		||||
jsonwebtoken = "9.3.0"
 | 
			
		||||
ring = "0.17.8"
 | 
			
		||||
pem = "3.0.4"
 | 
			
		||||
							
								
								
									
										105
									
								
								virtweb_backend/src/api_tokens.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								virtweb_backend/src/api_tokens.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
//! # API tokens management
 | 
			
		||||
 | 
			
		||||
use crate::constants;
 | 
			
		||||
use crate::utils::jwt_utils;
 | 
			
		||||
use crate::utils::jwt_utils::{TokenPrivKey, TokenPubKey};
 | 
			
		||||
use crate::utils::time_utils::time;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Copy, Debug)]
 | 
			
		||||
pub struct TokenID(pub uuid::Uuid);
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct Token {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub id: TokenID,
 | 
			
		||||
    created: u64,
 | 
			
		||||
    updated: u64,
 | 
			
		||||
    pub pub_key: TokenPubKey,
 | 
			
		||||
    pub rights: Vec<TokenRights>,
 | 
			
		||||
    pub last_used: Option<u64>,
 | 
			
		||||
    pub ip_restriction: Option<ipnetwork::IpNetwork>,
 | 
			
		||||
    pub delete_after_inactivity: Option<u64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, Copy, Eq, PartialEq)]
 | 
			
		||||
pub enum TokenVerb {
 | 
			
		||||
    GET,
 | 
			
		||||
    POST,
 | 
			
		||||
    PUT,
 | 
			
		||||
    PATCH,
 | 
			
		||||
    DELETE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct TokenRights {
 | 
			
		||||
    verb: TokenVerb,
 | 
			
		||||
    uri: String,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Structure used to create a token
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
 | 
			
		||||
pub struct NewToken {
 | 
			
		||||
    pub name: String,
 | 
			
		||||
    pub description: String,
 | 
			
		||||
    pub rights: Vec<TokenRights>,
 | 
			
		||||
    pub ip_restriction: Option<ipnetwork::IpNetwork>,
 | 
			
		||||
    pub delete_after_inactivity: Option<u64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl NewToken {
 | 
			
		||||
    /// Check for error in token
 | 
			
		||||
    pub fn check_error(&self) -> Option<&'static str> {
 | 
			
		||||
        if self.name.len() < constants::API_TOKEN_NAME_MIN_LENGTH {
 | 
			
		||||
            return Some("Name is too short!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.name.len() > constants::API_TOKEN_NAME_MAX_LENGTH {
 | 
			
		||||
            return Some("Name is too long!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.description.len() < constants::API_TOKEN_DESCRIPTION_MIN_LENGTH {
 | 
			
		||||
            return Some("Description is too short!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if self.description.len() > constants::API_TOKEN_DESCRIPTION_MAX_LENGTH {
 | 
			
		||||
            return Some("Description is too long!");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        for r in &self.rights {
 | 
			
		||||
            if !r.uri.starts_with("/api/") {
 | 
			
		||||
                return Some("All API rights shall start with /api/");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if let Some(t) = self.delete_after_inactivity {
 | 
			
		||||
            if t < 3600 {
 | 
			
		||||
                return Some("API tokens shall be valid for at least 1 hour!");
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        None
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a new Token
 | 
			
		||||
pub async fn create(token: &NewToken) -> anyhow::Result<(Token, TokenPrivKey)> {
 | 
			
		||||
    let (pub_key, priv_key) = jwt_utils::generate_key_pair()?;
 | 
			
		||||
 | 
			
		||||
    let full_token = Token {
 | 
			
		||||
        name: token.name.to_string(),
 | 
			
		||||
        description: token.description.to_string(),
 | 
			
		||||
        id: TokenID(uuid::Uuid::new_v4()),
 | 
			
		||||
        created: time(),
 | 
			
		||||
        updated: time(),
 | 
			
		||||
        pub_key,
 | 
			
		||||
        rights: token.rights.clone(),
 | 
			
		||||
        last_used: Some(time()),
 | 
			
		||||
        ip_restriction: token.ip_restriction,
 | 
			
		||||
        delete_after_inactivity: token.delete_after_inactivity,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // TODO : save
 | 
			
		||||
 | 
			
		||||
    Ok((full_token, priv_key))
 | 
			
		||||
}
 | 
			
		||||
@@ -268,6 +268,10 @@ impl AppConfig {
 | 
			
		||||
        self.definitions_path()
 | 
			
		||||
            .join(format!("nwfilter-{}.json", name.0))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn api_tokens_path(&self) -> PathBuf {
 | 
			
		||||
        self.storage_path().join(constants::STORAGE_TOKENS_DIR)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Debug, Clone, serde::Serialize)]
 | 
			
		||||
 
 | 
			
		||||
@@ -89,3 +89,18 @@ pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE";
 | 
			
		||||
 | 
			
		||||
/// Nat hook file path
 | 
			
		||||
pub const NAT_HOOK_PATH: &str = "/etc/libvirt/hooks/network";
 | 
			
		||||
 | 
			
		||||
/// Directory where API tokens are stored, inside storage directory
 | 
			
		||||
pub const STORAGE_TOKENS_DIR: &str = "tokens";
 | 
			
		||||
 | 
			
		||||
/// API token name min length
 | 
			
		||||
pub const API_TOKEN_NAME_MIN_LENGTH: usize = 3;
 | 
			
		||||
 | 
			
		||||
/// API token name max length
 | 
			
		||||
pub const API_TOKEN_NAME_MAX_LENGTH: usize = 30;
 | 
			
		||||
 | 
			
		||||
/// API token description min length
 | 
			
		||||
pub const API_TOKEN_DESCRIPTION_MIN_LENGTH: usize = 5;
 | 
			
		||||
 | 
			
		||||
/// API token description max length
 | 
			
		||||
pub const API_TOKEN_DESCRIPTION_MAX_LENGTH: usize = 30;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								virtweb_backend/src/controllers/api_tokens_controller.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								virtweb_backend/src/controllers/api_tokens_controller.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
//! # API tokens management
 | 
			
		||||
 | 
			
		||||
use crate::api_tokens;
 | 
			
		||||
use crate::api_tokens::NewToken;
 | 
			
		||||
use crate::controllers::api_tokens_controller::rest_token::RestToken;
 | 
			
		||||
use crate::controllers::HttpResult;
 | 
			
		||||
use crate::utils::jwt_utils::TokenPrivKey;
 | 
			
		||||
use actix_web::{web, HttpResponse};
 | 
			
		||||
 | 
			
		||||
/// Create a special module for REST token to enforce usage of constructor function
 | 
			
		||||
mod rest_token {
 | 
			
		||||
    use crate::api_tokens::Token;
 | 
			
		||||
    use crate::utils::jwt_utils::TokenPubKey;
 | 
			
		||||
 | 
			
		||||
    #[derive(serde::Serialize)]
 | 
			
		||||
    pub struct RestToken {
 | 
			
		||||
        token: Token,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl RestToken {
 | 
			
		||||
        pub fn new(mut token: Token) -> Self {
 | 
			
		||||
            token.pub_key = TokenPubKey::None;
 | 
			
		||||
            Self { token }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize)]
 | 
			
		||||
struct CreateTokenResult {
 | 
			
		||||
    token: RestToken,
 | 
			
		||||
    priv_key: TokenPrivKey,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Create a new API token
 | 
			
		||||
pub async fn create(new_token: web::Json<NewToken>) -> HttpResult {
 | 
			
		||||
    if let Some(err) = new_token.check_error() {
 | 
			
		||||
        log::error!("Failed to validate new API token information! {err}");
 | 
			
		||||
        return Ok(HttpResponse::BadRequest().json(format!(
 | 
			
		||||
            "Failed to validate new API token information! {err}"
 | 
			
		||||
        )));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let (token, priv_key) = api_tokens::create(&new_token).await?;
 | 
			
		||||
 | 
			
		||||
    Ok(HttpResponse::Ok().json(CreateTokenResult {
 | 
			
		||||
        token: RestToken::new(token),
 | 
			
		||||
        priv_key,
 | 
			
		||||
    }))
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,7 @@ use std::error::Error;
 | 
			
		||||
use std::fmt::{Display, Formatter};
 | 
			
		||||
use std::io::ErrorKind;
 | 
			
		||||
 | 
			
		||||
pub mod api_tokens_controller;
 | 
			
		||||
pub mod auth_controller;
 | 
			
		||||
pub mod iso_controller;
 | 
			
		||||
pub mod network_controller;
 | 
			
		||||
 
 | 
			
		||||
@@ -51,6 +51,8 @@ struct ServerConstraints {
 | 
			
		||||
    nwfilter_comment_size: LenConstraints,
 | 
			
		||||
    nwfilter_priority: SLenConstraints,
 | 
			
		||||
    nwfilter_selectors_count: LenConstraints,
 | 
			
		||||
    api_token_name_size: LenConstraints,
 | 
			
		||||
    api_token_description_size: LenConstraints,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
			
		||||
@@ -98,6 +100,16 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
 | 
			
		||||
                max: 1000,
 | 
			
		||||
            },
 | 
			
		||||
            nwfilter_selectors_count: LenConstraints { min: 0, max: 1 },
 | 
			
		||||
 | 
			
		||||
            api_token_name_size: LenConstraints {
 | 
			
		||||
                min: constants::API_TOKEN_NAME_MIN_LENGTH,
 | 
			
		||||
                max: constants::API_TOKEN_NAME_MAX_LENGTH,
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            api_token_description_size: LenConstraints {
 | 
			
		||||
                min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
 | 
			
		||||
                max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
pub mod actors;
 | 
			
		||||
pub mod api_tokens;
 | 
			
		||||
pub mod app_config;
 | 
			
		||||
pub mod constants;
 | 
			
		||||
pub mod controllers;
 | 
			
		||||
 
 | 
			
		||||
@@ -22,8 +22,8 @@ use virtweb_backend::constants::{
 | 
			
		||||
    MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
 | 
			
		||||
};
 | 
			
		||||
use virtweb_backend::controllers::{
 | 
			
		||||
    auth_controller, iso_controller, network_controller, nwfilter_controller, server_controller,
 | 
			
		||||
    static_controller, vm_controller,
 | 
			
		||||
    api_tokens_controller, auth_controller, iso_controller, network_controller,
 | 
			
		||||
    nwfilter_controller, server_controller, static_controller, vm_controller,
 | 
			
		||||
};
 | 
			
		||||
use virtweb_backend::libvirt_client::LibVirtClient;
 | 
			
		||||
use virtweb_backend::middlewares::auth_middleware::AuthChecker;
 | 
			
		||||
@@ -50,6 +50,7 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().disks_storage_path()).unwrap();
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().nat_path()).unwrap();
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().definitions_path()).unwrap();
 | 
			
		||||
    files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap();
 | 
			
		||||
 | 
			
		||||
    let conn = Data::new(LibVirtClient(
 | 
			
		||||
        LibVirtActor::connect()
 | 
			
		||||
@@ -276,6 +277,27 @@ async fn main() -> std::io::Result<()> {
 | 
			
		||||
                "/api/nwfilter/{uid}",
 | 
			
		||||
                web::delete().to(nwfilter_controller::delete),
 | 
			
		||||
            )
 | 
			
		||||
            // API tokens controller
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/tokens/create",
 | 
			
		||||
                web::post().to(api_tokens_controller::create),
 | 
			
		||||
            )
 | 
			
		||||
            /* TODO .route(
 | 
			
		||||
                "/api/tokens/list",
 | 
			
		||||
                web::get().to(api_tokens_controller::list),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/tokens/{uid}",
 | 
			
		||||
                web::get().to(api_tokens_controller::get_single),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/tokens/{uid}",
 | 
			
		||||
                web::put().to(api_tokens_controller::update),
 | 
			
		||||
            )
 | 
			
		||||
            .route(
 | 
			
		||||
                "/api/tokens/{uid}",
 | 
			
		||||
                web::delete().to(api_tokens_controller::delete),
 | 
			
		||||
            )*/
 | 
			
		||||
            // Static assets
 | 
			
		||||
            .route("/", web::get().to(static_controller::root_index))
 | 
			
		||||
            .route(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										109
									
								
								virtweb_backend/src/utils/jwt_utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								virtweb_backend/src/utils/jwt_utils.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,109 @@
 | 
			
		||||
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation};
 | 
			
		||||
use ring::signature::{KeyPair, UnparsedPublicKey};
 | 
			
		||||
use serde::de::DeserializeOwned;
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
 | 
			
		||||
#[serde(tag = "alg")]
 | 
			
		||||
pub enum TokenPubKey {
 | 
			
		||||
    /// This variant DOES make crash the program. It MUST NOT be serialized.
 | 
			
		||||
    ///
 | 
			
		||||
    /// It is a hack to hide public key when getting the list of tokens
 | 
			
		||||
    None,
 | 
			
		||||
 | 
			
		||||
    /// ECDSA with SHA2-384 variant
 | 
			
		||||
    ES384 { r#pub: String },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
 | 
			
		||||
#[serde(tag = "alg")]
 | 
			
		||||
pub enum TokenPrivKey {
 | 
			
		||||
    ES384 { r#priv: String },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Generate a new token keypair
 | 
			
		||||
pub fn generate_key_pair() -> anyhow::Result<(TokenPubKey, TokenPrivKey)> {
 | 
			
		||||
    let doc = ring::signature::EcdsaKeyPair::generate_pkcs8(
 | 
			
		||||
        &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
 | 
			
		||||
        &ring::rand::SystemRandom::new(),
 | 
			
		||||
    )?;
 | 
			
		||||
 | 
			
		||||
    let priv_pem = pem::encode(&pem::Pem::new("PRIVATE KEY", doc.as_ref()));
 | 
			
		||||
 | 
			
		||||
    let pair = ring::signature::EcdsaKeyPair::from_pkcs8(
 | 
			
		||||
        &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING,
 | 
			
		||||
        doc.as_ref(),
 | 
			
		||||
        &ring::rand::SystemRandom::new(),
 | 
			
		||||
    )?;
 | 
			
		||||
    let pub_pem = pem::encode(&pem::Pem::new("PUBLIC KEY", pair.public_key().as_ref()));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    let pk = pair.public_key();
 | 
			
		||||
    let unp = UnparsedPublicKey::new(&ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, pk.as_ref());
 | 
			
		||||
 | 
			
		||||
    let decoding_key = DecodingKey::from_ec_pem(pub_pem.as_bytes()).expect("aie ai");
 | 
			
		||||
 | 
			
		||||
    Ok((
 | 
			
		||||
        TokenPubKey::ES384 { r#pub: pub_pem },
 | 
			
		||||
        TokenPrivKey::ES384 { r#priv: priv_pem },
 | 
			
		||||
    ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Sign JWT with a private key
 | 
			
		||||
pub fn sign_jwt<C: Serialize>(key: &TokenPrivKey, claims: &C) -> anyhow::Result<String> {
 | 
			
		||||
    match key {
 | 
			
		||||
        TokenPrivKey::ES384 { r#priv } => {
 | 
			
		||||
            let encoding_key = EncodingKey::from_ec_pem(r#priv.as_bytes())?;
 | 
			
		||||
 | 
			
		||||
            Ok(jsonwebtoken::encode(
 | 
			
		||||
                &jsonwebtoken::Header::new(Algorithm::ES384),
 | 
			
		||||
                &claims,
 | 
			
		||||
                &encoding_key,
 | 
			
		||||
            )?)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Validate a given JWT
 | 
			
		||||
pub fn validate_jwt<E: DeserializeOwned>(key: &TokenPubKey, token: &str) -> anyhow::Result<E> {
 | 
			
		||||
    match key {
 | 
			
		||||
        TokenPubKey::ES384 { r#pub } => {
 | 
			
		||||
            let decoding_key = DecodingKey::from_ec_pem(r#pub.as_bytes())?;
 | 
			
		||||
 | 
			
		||||
            let validation = Validation::new(Algorithm::ES384);
 | 
			
		||||
            Ok(jsonwebtoken::decode::<E>(token, &decoding_key, &validation)?.claims)
 | 
			
		||||
        }
 | 
			
		||||
        TokenPubKey::None => {
 | 
			
		||||
            panic!("A public key is required!")
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
    use crate::utils::jwt_utils::{generate_key_pair, sign_jwt, validate_jwt};
 | 
			
		||||
    use crate::utils::time_utils::time;
 | 
			
		||||
    use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
    #[derive(Debug, Serialize, Deserialize, Eq, PartialEq)]
 | 
			
		||||
    pub struct Claims {
 | 
			
		||||
        sub: String,
 | 
			
		||||
        exp: u64,
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn jwt_encode_sign_verify_valid() {
 | 
			
		||||
        let (pub_key, priv_key) = generate_key_pair().unwrap();
 | 
			
		||||
        let claims = Claims {
 | 
			
		||||
            sub: "my-sub".to_string(),
 | 
			
		||||
            exp: time() + 100,
 | 
			
		||||
        };
 | 
			
		||||
        let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!");
 | 
			
		||||
 | 
			
		||||
        println!("pub {pub_key:?}");
 | 
			
		||||
        println!("priv {priv_key:?}");
 | 
			
		||||
        let claims_out = validate_jwt(&pub_key, &jwt).expect("Failed to validate JWT!");
 | 
			
		||||
 | 
			
		||||
        assert_eq!(claims, claims_out)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
pub mod disks_utils;
 | 
			
		||||
pub mod files_utils;
 | 
			
		||||
pub mod jwt_utils;
 | 
			
		||||
pub mod net_utils;
 | 
			
		||||
pub mod rand_utils;
 | 
			
		||||
pub mod time_utils;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user