Skip to content

Commit 3d85a35

Browse files
authored
Merge pull request #34 from nuclearcat/unified-secret
jwt: Add unified_secret handling
2 parents e48e08c + a500809 commit 3d85a35

7 files changed

Lines changed: 94 additions & 59 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ edition = "2021"
66
[dependencies]
77
async-trait = "0.1"
88
axum = { version = "0.7.9", features = ["tracing", "multipart", "macros"] }
9-
axum-server = { version = "0.8.0", features = ["rustls", "rustls-pemfile", "tls-rustls"] }
9+
axum-server = { version = "0.8.0", features = ["rustls", "tls-rustls"] }
1010
azure_blob_uploader = "0.1.4"
1111
azure_storage = "0.21.0"
1212
azure_storage_blobs = "0.21.0"
@@ -19,14 +19,12 @@ futures = "0.3.31"
1919
futures-util = "0.3.31"
2020
headers = "0.4.0"
2121
hex = "0.4.3"
22-
hmac = "0.13.0"
2322
http-body-util = "0.1.2"
24-
jwt = "0.16.0"
23+
jsonwebtoken = "10"
2524
rand = "0.8.5"
2625
reqwest = { version = "0.12.9", features = ["blocking"] }
2726
rustls = "0.23.20"
2827
serde = { version = "1.0.216", features = ["derive"] }
29-
sha2 = "0.11.0"
3028
sysinfo = { version = "0.35.2", features = ["serde"] }
3129
tempfile = "3.14.0"
3230
tokio = { version = "1.42.0", features = ["rt", "rt-multi-thread", "macros"] }
@@ -39,8 +37,6 @@ tracing-subscriber = "0.3.19"
3937

4038
[dev-dependencies]
4139
reqwest = { version = "0.12.9", features = ["blocking", "multipart"] }
42-
hmac = "0.13.0"
43-
jwt = "0.16.0"
44-
sha2 = "0.11.0"
40+
jsonwebtoken = "10"
4541
tokio = { version = "1.42.0", features = ["rt", "rt-multi-thread", "macros", "time", "process"] }
4642
tempfile = "3.14.0"

src/azure.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ mod tests {
106106
}
107107
}
108108

109+
#[allow(dead_code)]
109110
fn calculate_checksum(filename: &String, data: &[u8]) {
110111
let hash = sha2_512::default().update(data).finalize();
111112
let digest = hash.digest();
@@ -217,6 +218,7 @@ async fn write_file_to_blob_streaming(
217218

218219
/// Write file to Azure blob storage (legacy version using Vec<u8>)
219220
/// TBD: Rework, do not keep whole file as Vec<u8> in memory!!!
221+
#[allow(dead_code)]
220222
async fn write_file_to_blob(
221223
filename: String,
222224
data: Vec<u8>,
@@ -378,7 +380,6 @@ async fn get_file_from_blob(filename: String) -> ReceivedFile {
378380
let blob_client = ClientBuilder::new(storage_account, storage_credential)
379381
.blob_client(storage_container, storage_blob);
380382
let blob_url_res = blob_client.url();
381-
let mut blob_url = "".to_string();
382383
let mut received_file = ReceivedFile {
383384
original_filename: "".to_string(),
384385
cached_filename: "".to_string(),
@@ -387,16 +388,13 @@ async fn get_file_from_blob(filename: String) -> ReceivedFile {
387388
};
388389
received_file.original_filename = filename.clone();
389390

390-
match blob_url_res {
391-
Ok(url) => {
392-
//println!("Blob URL: {}", url);
393-
blob_url = url.to_string();
394-
}
391+
let mut blob_url = match blob_url_res {
392+
Ok(url) => url.to_string(),
395393
Err(e) => {
396394
eprintln!("Error getting blob URL: {:?}", e);
397395
return received_file;
398396
}
399-
}
397+
};
400398
// append SAS token to blob URL
401399
blob_url.push_str(storage_sastoken);
402400
// we generate a hash of the filename to use as cache filename
@@ -527,6 +525,7 @@ async fn get_file_from_blob(filename: String) -> ReceivedFile {
527525

528526
// Implement set tags for Azure blob storage
529527
// tags are in format "key=value"
528+
#[allow(dead_code)]
530529
async fn azure_set_filename_tags(
531530
filename: String,
532531
user_tags: Vec<(String, String)>,
@@ -571,7 +570,8 @@ async fn azure_set_filename_tags(
571570
}
572571
}
573572

574-
async fn azure_list_files(directory: String) -> Vec<String> {
573+
#[allow(dead_code)]
574+
async fn azure_list_files(_directory: String) -> Vec<String> {
575575
let azure_cfg = Arc::new(get_azure_credentials("azure"));
576576
let storage_account = azure_cfg.account.as_str();
577577
let storage_key = azure_cfg.key.clone();

src/local.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ fn get_metadata_file_path(filename: &str) -> PathBuf {
100100
}
101101

102102
/// Calculate SHA-512 checksum of file data
103+
#[allow(dead_code)]
103104
fn calculate_checksum(filename: &str, data: &[u8]) {
104105
let hash = sha2_512::default().update(data).finalize();
105106
let digest = hash.digest();
@@ -157,6 +158,7 @@ async fn write_file_to_local_streaming(
157158
}
158159

159160
/// Write file to local storage (legacy version using Vec<u8>)
161+
#[allow(dead_code)]
160162
fn write_file_to_local(
161163
filename: String,
162164
data: Vec<u8>,
@@ -298,6 +300,7 @@ fn list_files_in_local(directory: String) -> Vec<String> {
298300
}
299301

300302
/// Set tags for local storage (stored in metadata)
303+
#[allow(dead_code)]
301304
fn set_tags_for_local_file(
302305
filename: String,
303306
user_tags: Vec<(String, String)>,

src/main.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ impl<'a> tokio::io::AsyncRead for FieldStream<'a> {
190190
}
191191
}
192192

193+
#[allow(dead_code)]
193194
#[async_trait]
194195
trait Driver: Send + Sync {
195196
async fn write_file(
@@ -1149,6 +1150,7 @@ async fn driver_get_file(filepath: String) -> ReceivedFile {
11491150
driver.get_file(filepath).await
11501151
}
11511152

1153+
#[allow(dead_code)]
11521154
async fn write_file_driver(
11531155
filename: String,
11541156
data: Vec<u8>,

src/storcaching.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{debug_log, get_config_content};
1+
use crate::get_config_content;
22
use serde::Deserialize;
33
use std::cmp::Ordering;
44
use std::collections::{BinaryHeap, HashSet};
@@ -325,15 +325,14 @@ pub async fn cache_loop(cache_dir: &str) {
325325
let cleanup_chunk_size = config.cleanup_chunk_size.max(1);
326326
let mut deleted_entries_counter: u64 = 0;
327327
let mut reclaimed_bytes_counter: u64 = 0;
328-
let mut cached_file_count: usize = 0;
329328
let mut next_log = Instant::now();
330329

331330
loop {
332331
let (limit_outcome, file_count) =
333332
enforce_cache_file_limit(cache_dir, cleanup_chunk_size).await;
334333
deleted_entries_counter += limit_outcome.deleted_entries;
335334
reclaimed_bytes_counter += limit_outcome.reclaimed_bytes;
336-
cached_file_count = file_count;
335+
let mut cached_file_count = file_count;
337336

338337
let mut free_space = freediskspace_percent(cache_dir).await;
339338
if free_space < DISK_SPACE_LOW_PERCENT {

src/storjwt.rs

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,62 @@
1-
use crate::{debug_log, get_config_content};
2-
use hmac::{Hmac, Mac};
3-
use jwt::{Header, SignWithKey, Token, VerifyWithKey};
4-
use sha2::Sha256;
1+
use crate::get_config_content;
2+
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
3+
use serde::{Deserialize, Serialize};
54
use std::collections::BTreeMap;
65
use toml::value::Table;
7-
pub fn verify_jwt_token(token_str: &str) -> Result<BTreeMap<String, String>, jwt::Error> {
8-
// config.toml, jwt_secret parameter
6+
7+
#[derive(Debug, Serialize, Deserialize)]
8+
struct Claims {
9+
email: String,
10+
}
11+
12+
fn verify_with_key_str(
13+
token_str: &str,
14+
key_str: &str,
15+
) -> Result<BTreeMap<String, String>, jsonwebtoken::errors::Error> {
16+
let key = DecodingKey::from_secret(key_str.as_bytes());
17+
let mut validation = Validation::default();
18+
validation.required_spec_claims.clear();
19+
validation.validate_exp = false;
20+
let token_data = decode::<Claims>(token_str, &key, &validation)?;
21+
let mut claims = BTreeMap::new();
22+
claims.insert("email".to_string(), token_data.claims.email);
23+
Ok(claims)
24+
}
25+
26+
pub fn verify_jwt_token(
27+
token_str: &str,
28+
) -> Result<BTreeMap<String, String>, jsonwebtoken::errors::Error> {
929
let toml_cfg = get_config_content();
1030
let parsed_toml = toml_cfg.parse::<Table>().unwrap();
11-
let key_str = parsed_toml["jwt_secret"].as_str().unwrap();
12-
let key: Hmac<Sha256> = Hmac::new_from_slice(key_str.as_bytes())?;
13-
let verify_result = token_str.verify_with_key(&key);
14-
let token: Token<Header, BTreeMap<String, String>, _> = match verify_result {
15-
Ok(token) => token,
16-
Err(e) => {
17-
eprintln!("JWT verification error: {:?}", e);
18-
return Err(e);
19-
}
20-
};
21-
//let header = token.header();
22-
let claims = token.claims();
23-
let email = claims.get("email");
24-
match email {
25-
Some(email) => {
26-
debug_log!("email: {}", email);
31+
32+
// If only unified_secret is configured, it serves as jwt_secret as well.
33+
// Try jwt_secret first, then fall through to unified_secret.
34+
if let Some(key_str) = parsed_toml.get("jwt_secret").and_then(|v| v.as_str()) {
35+
match verify_with_key_str(token_str, key_str) {
36+
Ok(claims) => {
37+
debug_log!("email: {}", claims["email"]);
38+
return Ok(claims);
39+
}
40+
Err(e) => {
41+
debug_log!("JWT verification with jwt_secret failed: {:?}", e);
42+
}
2743
}
28-
None => {
29-
debug_log!("email not found");
30-
return Err(jwt::Error::InvalidSignature);
44+
}
45+
46+
if let Some(unified) = parsed_toml.get("unified_secret").and_then(|v| v.as_str()) {
47+
match verify_with_key_str(token_str, unified) {
48+
Ok(claims) => {
49+
debug_log!("email (unified_secret): {}", claims["email"]);
50+
return Ok(claims);
51+
}
52+
Err(e) => {
53+
eprintln!("JWT verification with unified_secret also failed: {:?}", e);
54+
return Err(e);
55+
}
3156
}
3257
}
33-
Ok(claims.clone())
58+
59+
Err(jsonwebtoken::errors::ErrorKind::InvalidSignature.into())
3460
}
3561

3662
pub fn generate_jwt_secret() {
@@ -45,13 +71,18 @@ pub fn generate_jwt_secret() {
4571
debug_log!("jwt_secret=\"{}\"", secret);
4672
}
4773

48-
pub fn generate_jwt_token(email: &str) -> Result<String, jwt::Error> {
74+
pub fn generate_jwt_token(email: &str) -> Result<String, jsonwebtoken::errors::Error> {
4975
let toml_cfg = get_config_content();
5076
let parsed_toml = toml_cfg.parse::<Table>().unwrap();
51-
let key_str = parsed_toml["jwt_secret"].as_str().unwrap();
52-
let key: Hmac<Sha256> = Hmac::new_from_slice(key_str.as_bytes())?;
53-
let mut claims = BTreeMap::new();
54-
claims.insert("email".to_string(), email.to_string());
55-
let token_str = claims.sign_with_key(&key)?;
56-
Ok(token_str)
77+
// For token generation, prefer jwt_secret, fall back to unified_secret
78+
let key_str = parsed_toml
79+
.get("jwt_secret")
80+
.or_else(|| parsed_toml.get("unified_secret"))
81+
.and_then(|v| v.as_str())
82+
.expect("config must define jwt_secret or unified_secret");
83+
let key = EncodingKey::from_secret(key_str.as_bytes());
84+
let claims = Claims {
85+
email: email.to_string(),
86+
};
87+
encode(&Header::default(), &claims, &key)
5788
}

tests/e2e.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
// End-to-end tests for kernelci-storage using the local storage driver.
55
// These tests start the actual server binary and exercise the HTTP API.
66

7-
use hmac::{Hmac, Mac};
8-
use jwt::SignWithKey;
7+
use jsonwebtoken::{encode, EncodingKey, Header};
98
use reqwest::blocking::multipart;
10-
use sha2::Sha256;
11-
use std::collections::BTreeMap;
9+
use serde::Serialize;
1210
use std::net::TcpListener;
1311
use std::path::PathBuf;
1412
use std::process::{Child, Command};
@@ -18,6 +16,11 @@ const JWT_SECRET: &str = "test-secret-for-e2e-testing";
1816
const TEST_EMAIL: &str = "test@kernelci.org";
1917
const RESTRICTED_EMAIL: &str = "restricted@kernelci.org";
2018

19+
#[derive(Serialize)]
20+
struct Claims {
21+
email: String,
22+
}
23+
2124
/// Find an available TCP port
2225
fn get_free_port() -> u16 {
2326
let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind to ephemeral port");
@@ -26,10 +29,11 @@ fn get_free_port() -> u16 {
2629

2730
/// Generate a JWT token for the given email using the test secret
2831
fn generate_token(email: &str) -> String {
29-
let key: Hmac<Sha256> = Hmac::new_from_slice(JWT_SECRET.as_bytes()).unwrap();
30-
let mut claims = BTreeMap::new();
31-
claims.insert("email".to_string(), email.to_string());
32-
claims.sign_with_key(&key).unwrap()
32+
let key = EncodingKey::from_secret(JWT_SECRET.as_bytes());
33+
let claims = Claims {
34+
email: email.to_string(),
35+
};
36+
encode(&Header::default(), &claims, &key).unwrap()
3337
}
3438

3539
/// Test server handle - kills the server process on drop

0 commit comments

Comments
 (0)