3 Commits

Author SHA1 Message Date
bab34b7c7f Add more test to validate JWTs
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2024-04-09 18:04:00 +02:00
0217d1c53d WIP
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-08 22:46:17 +02:00
ab7907d947 WIP 2024-04-08 22:19:28 +02:00
12 changed files with 607 additions and 2 deletions

View File

@@ -568,6 +568,12 @@ dependencies = [
"rustc-demangle", "rustc-demangle",
] ]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.20.0" version = "0.20.0"
@@ -586,6 +592,12 @@ version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
[[package]]
name = "base64ct"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "bincode" name = "bincode"
version = "2.0.0-rc.3" version = "2.0.0-rc.3"
@@ -792,6 +804,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.4.0" version = "0.4.0"
@@ -890,6 +908,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@@ -945,6 +975,17 @@ dependencies = [
"syn 2.0.58", "syn 2.0.58",
] ]
[[package]]
name = "der"
version = "0.7.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
dependencies = [
"const-oid",
"pem-rfc7468",
"zeroize",
]
[[package]] [[package]]
name = "deranged" name = "deranged"
version = "0.3.11" version = "0.3.11"
@@ -974,6 +1015,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer", "block-buffer",
"const-oid",
"crypto-common", "crypto-common",
"subtle", "subtle",
] ]
@@ -984,12 +1026,47 @@ version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.10.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a"
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"hkdf",
"pem-rfc7468",
"pkcs8",
"rand_core",
"sec1",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.33" version = "0.8.33"
@@ -1069,6 +1146,16 @@ dependencies = [
"simd-adler32", "simd-adler32",
] ]
[[package]]
name = "ff"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
dependencies = [
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.28" version = "1.0.28"
@@ -1215,6 +1302,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
"zeroize",
] ]
[[package]] [[package]]
@@ -1224,8 +1312,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"js-sys",
"libc", "libc",
"wasi", "wasi",
"wasm-bindgen",
] ]
[[package]] [[package]]
@@ -1254,6 +1344,17 @@ version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core",
"subtle",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.3.26" version = "0.3.26"
@@ -1596,6 +1697,21 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "language-tags" name = "language-tags"
version = "0.3.2" version = "0.3.2"
@@ -2007,6 +2123,18 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "p384"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@@ -2042,6 +2170,25 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" 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 = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.1" version = "2.3.1"
@@ -2080,6 +2227,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.30" version = "0.3.30"
@@ -2123,6 +2280,15 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.79" version = "1.0.79"
@@ -2367,6 +2533,16 @@ dependencies = [
"winreg", "winreg",
] ]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]] [[package]]
name = "rgb" name = "rgb"
version = "0.8.37" version = "0.8.37"
@@ -2376,6 +2552,21 @@ dependencies = [
"bytemuck", "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]] [[package]]
name = "rust-embed" name = "rust-embed"
version = "8.3.0" version = "8.3.0"
@@ -2484,6 +2675,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "security-framework" name = "security-framework"
version = "2.10.0" version = "2.10.0"
@@ -2605,6 +2810,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest",
"rand_core",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.7" version = "0.3.7"
@@ -2620,6 +2835,18 @@ dependencies = [
"quote", "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]] [[package]]
name = "slab" name = "slab"
version = "0.4.9" version = "0.4.9"
@@ -2654,6 +2881,16 @@ dependencies = [
"lock_api", "lock_api",
] ]
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]] [[package]]
name = "strsim" name = "strsim"
version = "0.10.0" version = "0.10.0"
@@ -3020,6 +3257,12 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.0" version = "2.5.0"
@@ -3132,11 +3375,13 @@ dependencies = [
"bytes", "bytes",
"clap", "clap",
"dotenvy", "dotenvy",
"elliptic-curve",
"env_logger", "env_logger",
"futures", "futures",
"futures-util", "futures-util",
"image", "image",
"ipnetwork", "ipnetwork",
"jsonwebtoken",
"lazy-regex", "lazy-regex",
"lazy_static", "lazy_static",
"light-openid", "light-openid",
@@ -3144,6 +3389,7 @@ dependencies = [
"mime_guess", "mime_guess",
"nix", "nix",
"num", "num",
"p384",
"quick-xml", "quick-xml",
"rand", "rand",
"reqwest", "reqwest",
@@ -3500,6 +3746,12 @@ dependencies = [
"syn 2.0.58", "syn 2.0.58",
] ]
[[package]]
name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.13.1" version = "0.13.1"

View File

@@ -45,3 +45,6 @@ rust-embed = { version = "8.3.0" }
mime_guess = "2.0.4" mime_guess = "2.0.4"
dotenvy = "0.15.7" dotenvy = "0.15.7"
nix = { version = "0.28.0", features = ["net"] } nix = { version = "0.28.0", features = ["net"] }
jsonwebtoken = "9.3.0"
elliptic-curve = { version = "0.13.8", features = ["pkcs8","pem" ] }
p384 = { version = "0.13.0", features = ["ecdsa", "pkcs8", "pem"] }

View 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))
}

View File

@@ -268,6 +268,10 @@ impl AppConfig {
self.definitions_path() self.definitions_path()
.join(format!("nwfilter-{}.json", name.0)) .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)] #[derive(Debug, Clone, serde::Serialize)]

View File

@@ -89,3 +89,18 @@ pub const NAT_MODE_ENV_VAR_NAME: &str = "NAT_MODE";
/// Nat hook file path /// Nat hook file path
pub const NAT_HOOK_PATH: &str = "/etc/libvirt/hooks/network"; 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;

View 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,
}))
}

View File

@@ -6,6 +6,7 @@ use std::error::Error;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::io::ErrorKind; use std::io::ErrorKind;
pub mod api_tokens_controller;
pub mod auth_controller; pub mod auth_controller;
pub mod iso_controller; pub mod iso_controller;
pub mod network_controller; pub mod network_controller;

View File

@@ -51,6 +51,8 @@ struct ServerConstraints {
nwfilter_comment_size: LenConstraints, nwfilter_comment_size: LenConstraints,
nwfilter_priority: SLenConstraints, nwfilter_priority: SLenConstraints,
nwfilter_selectors_count: LenConstraints, nwfilter_selectors_count: LenConstraints,
api_token_name_size: LenConstraints,
api_token_description_size: LenConstraints,
} }
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder { 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, max: 1000,
}, },
nwfilter_selectors_count: LenConstraints { min: 0, max: 1 }, 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,
},
}, },
}) })
} }

View File

@@ -1,4 +1,5 @@
pub mod actors; pub mod actors;
pub mod api_tokens;
pub mod app_config; pub mod app_config;
pub mod constants; pub mod constants;
pub mod controllers; pub mod controllers;

View File

@@ -22,8 +22,8 @@ use virtweb_backend::constants::{
MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME, MAX_INACTIVITY_DURATION, MAX_SESSION_DURATION, SESSION_COOKIE_NAME,
}; };
use virtweb_backend::controllers::{ use virtweb_backend::controllers::{
auth_controller, iso_controller, network_controller, nwfilter_controller, server_controller, api_tokens_controller, auth_controller, iso_controller, network_controller,
static_controller, vm_controller, nwfilter_controller, server_controller, static_controller, vm_controller,
}; };
use virtweb_backend::libvirt_client::LibVirtClient; use virtweb_backend::libvirt_client::LibVirtClient;
use virtweb_backend::middlewares::auth_middleware::AuthChecker; 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().disks_storage_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().nat_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().definitions_path()).unwrap();
files_utils::create_directory_if_missing(AppConfig::get().api_tokens_path()).unwrap();
let conn = Data::new(LibVirtClient( let conn = Data::new(LibVirtClient(
LibVirtActor::connect() LibVirtActor::connect()
@@ -276,6 +277,27 @@ async fn main() -> std::io::Result<()> {
"/api/nwfilter/{uid}", "/api/nwfilter/{uid}",
web::delete().to(nwfilter_controller::delete), 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 // Static assets
.route("/", web::get().to(static_controller::root_index)) .route("/", web::get().to(static_controller::root_index))
.route( .route(

View File

@@ -0,0 +1,140 @@
use elliptic_curve::pkcs8::EncodePublicKey;
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Validation};
use p384::ecdsa::{SigningKey, VerifyingKey};
use p384::pkcs8::{EncodePrivateKey, LineEnding};
use rand::rngs::OsRng;
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 signing_key = SigningKey::random(&mut OsRng);
let priv_pem = signing_key
.to_pkcs8_der()?
.to_pem("PRIVATE KEY", LineEnding::LF)?
.to_string();
let pub_key = VerifyingKey::from(signing_key);
let pub_pem = pub_key.to_public_key_pem(LineEnding::LF)?;
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,
}
impl Default for Claims {
fn default() -> Self {
Self {
sub: "my-sub".to_string(),
exp: time() + 100,
}
}
}
#[test]
fn jwt_encode_sign_verify_valid() {
let (pub_key, priv_key) = generate_key_pair().unwrap();
let claims = Claims::default();
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!");
let claims_out = validate_jwt(&pub_key, &jwt).expect("Failed to validate JWT!");
assert_eq!(claims, claims_out)
}
#[test]
fn jwt_encode_sign_verify_invalid_key() {
let (_pub_key, priv_key) = generate_key_pair().unwrap();
let (pub_key_2, _priv_key_2) = generate_key_pair().unwrap();
let claims = Claims::default();
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!");
validate_jwt::<Claims>(&pub_key_2, &jwt).expect_err("JWT should not have validated!");
}
#[test]
fn jwt_verify_random_string() {
let (pub_key, _priv_key) = generate_key_pair().unwrap();
validate_jwt::<Claims>(&pub_key, "random_string")
.expect_err("JWT should not have validated!");
}
#[test]
fn jwt_expired() {
let (pub_key, priv_key) = generate_key_pair().unwrap();
let claims = Claims {
exp: time() - 100,
..Default::default()
};
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!");
validate_jwt::<Claims>(&pub_key, &jwt).expect_err("JWT should not have validated!");
}
#[test]
fn jwt_invalid_signature() {
let (pub_key, priv_key) = generate_key_pair().unwrap();
let claims = Claims::default();
let jwt = sign_jwt(&priv_key, &claims).expect("Failed to sign JWT!");
validate_jwt::<Claims>(&pub_key, &format!("{jwt}bad"))
.expect_err("JWT should not have validated!");
}
}

View File

@@ -1,5 +1,6 @@
pub mod disks_utils; pub mod disks_utils;
pub mod files_utils; pub mod files_utils;
pub mod jwt_utils;
pub mod net_utils; pub mod net_utils;
pub mod rand_utils; pub mod rand_utils;
pub mod time_utils; pub mod time_utils;