Skip to content

Commit 70fb3e6

Browse files
jplockclaude
andauthored
[perf] Optimize enclave with async I/O, parallel decryption, and HashMap (#259)
* [perf] Optimize enclave with async I/O, parallel decryption, and HashMap - Switch from BTreeMap to HashMap for O(1) field lookups - Add rayon for parallel HPKE field decryption across multiple cores - Convert to async I/O using tokio and tokio-vsock for concurrent client handling (spawn task per connection) - Add async protocol functions (send_message_async, recv_message_async) - Pre-allocate HashMap with capacity hints to reduce allocations - Use Mutex for thread-safe error collection during parallel decryption Performance impact: - Field decryption scales with CPU cores for multi-field requests - Concurrent request handling eliminates head-of-line blocking - HashMap provides constant-time field access vs O(log n) for BTreeMap The code maintains no-panic guarantees with proper error handling throughout all new code paths. * [chore] Remove unused tokio features (net, sync) Minimize tokio dependency surface area by removing features not used: - net: tokio-vsock provides its own vsock networking - sync: No tokio sync primitives used in the codebase Retained features: - rt-multi-thread: Required for async runtime and tokio::spawn - io-util: Required for AsyncReadExt/AsyncWriteExt traits - macros: Required for #[tokio::main] * [perf] Remove async/tokio, keep rayon and HashMap optimizations Reverts the async/tokio approach in favor of simpler synchronous code: - Removes tokio and tokio-vsock dependencies - Uses std::thread for concurrent client handling - Keeps rayon for parallel field decryption - Keeps HashMap for O(1) lookups - Reduces external dependency surface area The enclave typically handles a single parent connection at a time, making async overhead unnecessary. Rayon still provides parallelization for CPU-bound field decryption operations. * [security] Address security review findings Critical fixes: - Add connection limiting (MAX_CONCURRENT_CONNECTIONS=32) to prevent DoS - Create SecureHpkePrivateKey wrapper with ZeroizeOnDrop for key material - Remove expect() calls in functions.rs, use proper error handling High severity fixes: - Fix mutex poisoning: log critical error instead of silently ignoring - Sanitize error messages in logs to prevent sensitive data leakage - Gate detailed error logging behind debug builds only The enclave now: - Limits concurrent connections to prevent resource exhaustion - Properly zeroizes HPKE private key material on drop - Never panics in production code paths - Logs critical conditions (mutex poisoning) for debugging --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3c696e6 commit 70fb3e6

8 files changed

Lines changed: 356 additions & 138 deletions

File tree

Cargo.lock

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enclave/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ aws-lc-rs = { version = "=1.15.2", default-features = false }
2626
cel-interpreter = { version = "=0.10.0", default-features = false, features = ["json", "chrono"] }
2727
chrono = { version = "=0.4.42", default-features = false, features = ["now"] }
2828
data-encoding = { version = "=2.9.0", default-features = false, features = ["alloc"] }
29+
rayon = { version = "=1.10.0", default-features = false }
2930
serde = { version = "=1.0.228", default-features = false, features = ["derive"] }
3031
serde_json = { version = "=1.0.145", default-features = false }
3132
rustls = { version = "=0.23.35", default-features = false, features = ["aws_lc_rs", "prefer-post-quantum"] }

enclave/src/constants.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33

44
pub const ENCLAVE_PORT: u32 = 5050;
55

6+
/// Maximum concurrent connections to prevent resource exhaustion DoS attacks.
7+
/// Each connection spawns a thread (~8KB stack minimum), so this limits memory usage.
8+
/// With 32 connections and 10MB max message size, worst case is ~320MB memory.
9+
pub const MAX_CONCURRENT_CONNECTIONS: usize = 32;
10+
611
/// Maximum allowed message size (10 MB) to prevent memory exhaustion DoS attacks
712
pub const MAX_MESSAGE_SIZE: u64 = 10 * 1024 * 1024;
813

enclave/src/expressions.rs

Lines changed: 80 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: MIT-0
33

4-
use std::collections::BTreeMap;
4+
use std::collections::HashMap;
55

66
use anyhow::{Result, anyhow, bail};
77
use cel_interpreter::Value as celValue;
@@ -12,9 +12,9 @@ use crate::constants::MAX_EXPRESSION_LENGTH;
1212
use crate::functions;
1313

1414
pub fn execute_expressions(
15-
fields: &BTreeMap<String, Value>,
16-
expressions: &BTreeMap<String, String>,
17-
) -> Result<BTreeMap<String, Value>> {
15+
fields: &HashMap<String, Value>,
16+
expressions: &HashMap<String, String>,
17+
) -> Result<HashMap<String, Value>> {
1818
if expressions.is_empty() {
1919
return Ok(fields.clone());
2020
}
@@ -51,7 +51,8 @@ pub fn execute_expressions(
5151
context.add_function("date", functions::date);
5252
context.add_function("age", functions::age);
5353

54-
let mut transformed: BTreeMap<String, Value> = BTreeMap::new();
54+
let mut transformed: HashMap<String, Value> =
55+
HashMap::with_capacity(fields.len() + expressions.len());
5556

5657
for (field, decrypted_value) in fields {
5758
context
@@ -92,7 +93,7 @@ pub fn execute_expressions(
9293
mod tests {
9394
use super::*;
9495
use proptest::prelude::*;
95-
use std::collections::BTreeMap;
96+
use std::collections::HashMap;
9697

9798
// **Feature: enclave-improvements, Property 5: Expression failure fallback**
9899
// **Validates: Requirements 8.2**
@@ -111,9 +112,9 @@ mod tests {
111112
/// Simulates the fallback behavior from main.rs:
112113
/// When execute_expressions returns Err, return the original fields unchanged.
113114
fn execute_with_fallback(
114-
fields: &BTreeMap<String, Value>,
115-
expressions: &BTreeMap<String, String>,
116-
) -> BTreeMap<String, Value> {
115+
fields: &HashMap<String, Value>,
116+
expressions: &HashMap<String, String>,
117+
) -> HashMap<String, Value> {
117118
match execute_expressions(fields, expressions) {
118119
Ok(result) => result,
119120
Err(_) => fields.clone(),
@@ -133,7 +134,7 @@ mod tests {
133134
invalid_expr_type in 0usize..3
134135
) {
135136
// Create original fields
136-
let mut fields: BTreeMap<String, Value> = BTreeMap::new();
137+
let mut fields: HashMap<String, Value> = HashMap::new();
137138
fields.insert(field_name.clone(), Value::String(field_value.clone()));
138139

139140
// Create an invalid expression that will fail to execute gracefully
@@ -144,7 +145,7 @@ mod tests {
144145
_ => "undefined_var.to_uppercase()".to_string(),
145146
};
146147

147-
let mut expressions: BTreeMap<String, String> = BTreeMap::new();
148+
let mut expressions: HashMap<String, String> = HashMap::new();
148149
expressions.insert("result".to_string(), invalid_expression);
149150

150151
// Execute with fallback (simulating main.rs behavior)
@@ -173,7 +174,7 @@ mod tests {
173174
use std::hash::{Hash, Hasher};
174175

175176
// Generate deterministic field names and values based on seed
176-
let mut fields: BTreeMap<String, Value> = BTreeMap::new();
177+
let mut fields: HashMap<String, Value> = HashMap::new();
177178
for i in 0..num_fields {
178179
let mut hasher = DefaultHasher::new();
179180
(field_seed, i).hash(&mut hasher);
@@ -184,7 +185,7 @@ mod tests {
184185
}
185186

186187
// Create an expression that references an undefined variable
187-
let mut expressions: BTreeMap<String, String> = BTreeMap::new();
188+
let mut expressions: HashMap<String, String> = HashMap::new();
188189
expressions.insert("computed".to_string(), "undefined_var.to_uppercase()".to_string());
189190

190191
// Execute with fallback
@@ -212,10 +213,10 @@ mod tests {
212213
field_name in "[a-z][a-z0-9_]{0,10}",
213214
field_value in "[a-zA-Z0-9 ]{1,20}"
214215
) {
215-
let mut fields: BTreeMap<String, Value> = BTreeMap::new();
216+
let mut fields: HashMap<String, Value> = HashMap::new();
216217
fields.insert(field_name.clone(), Value::String(field_value.clone()));
217218

218-
let expressions: BTreeMap<String, String> = BTreeMap::new();
219+
let expressions: HashMap<String, String> = HashMap::new();
219220

220221
let result = execute_expressions(&fields, &expressions).unwrap();
221222

@@ -233,11 +234,11 @@ mod tests {
233234
// Generate lowercase string to test to_uppercase
234235
field_value in "[a-z]{1,10}"
235236
) {
236-
let mut fields: BTreeMap<String, Value> = BTreeMap::new();
237+
let mut fields: HashMap<String, Value> = HashMap::new();
237238
fields.insert(field_name.clone(), Value::String(field_value.clone()));
238239

239240
// Create expression to uppercase the field
240-
let mut expressions: BTreeMap<String, String> = BTreeMap::new();
241+
let mut expressions: HashMap<String, String> = HashMap::new();
241242
expressions.insert(field_name.clone(), format!("{}.to_uppercase()", field_name));
242243

243244
let result = execute_expressions(&fields, &expressions).unwrap();
@@ -253,67 +254,65 @@ mod tests {
253254

254255
#[test]
255256
fn test_skip_expressions() {
256-
let expressions = BTreeMap::new();
257+
let expressions = HashMap::new();
257258

258-
let expected: BTreeMap<String, Value> =
259-
BTreeMap::from([("first_name".to_string(), "Bob".into())]);
259+
let expected: HashMap<String, Value> =
260+
HashMap::from([("first_name".to_string(), "Bob".into())]);
260261

261262
let actual = execute_expressions(&expected, &expressions).unwrap();
262263
assert_eq!(actual, expected);
263264
}
264265

265266
#[test]
266267
fn test_execute_transforms() {
267-
let expressions: BTreeMap<String, String> = BTreeMap::from([(
268+
let expressions: HashMap<String, String> = HashMap::from([(
268269
"first_name".to_string(),
269270
"first_name.to_uppercase()".to_string(),
270271
)]);
271272

272-
let fields: BTreeMap<String, Value> =
273-
BTreeMap::from([("first_name".to_string(), "Bob".into())]);
273+
let fields: HashMap<String, Value> =
274+
HashMap::from([("first_name".to_string(), "Bob".into())]);
274275

275-
let expected: BTreeMap<String, Value> =
276-
BTreeMap::from([("first_name".to_string(), "BOB".into())]);
276+
let expected: HashMap<String, Value> =
277+
HashMap::from([("first_name".to_string(), "BOB".into())]);
277278

278279
let actual = execute_expressions(&fields, &expressions).unwrap();
279280
assert_eq!(actual, expected);
280281
}
281282

282283
#[test]
283284
fn test_base64() {
284-
let expressions: BTreeMap<String, String> = BTreeMap::from([(
285+
let expressions: HashMap<String, String> = HashMap::from([(
285286
"first_name".into(),
286287
"first_name.base64_encode().base64_decode()".into(),
287288
)]);
288289

289-
let fields: BTreeMap<String, Value> = BTreeMap::from([("first_name".into(), "Bob".into())]);
290+
let fields: HashMap<String, Value> = HashMap::from([("first_name".into(), "Bob".into())]);
290291

291-
let expected: BTreeMap<String, Value> =
292-
BTreeMap::from([("first_name".into(), "Bob".into())]);
292+
let expected: HashMap<String, Value> = HashMap::from([("first_name".into(), "Bob".into())]);
293293

294294
let actual = execute_expressions(&fields, &expressions).unwrap();
295295
assert_eq!(actual, expected);
296296
}
297297

298298
#[test]
299299
fn test_hex() {
300-
let expressions: BTreeMap<String, String> = BTreeMap::from([(
300+
let expressions: HashMap<String, String> = HashMap::from([(
301301
"first_name".into(),
302302
"first_name.hex_encode().hex_decode()".into(),
303303
)]);
304304

305-
let fields: BTreeMap<String, Value> = BTreeMap::from([("first_name".into(), "Bob".into())]);
305+
let fields: HashMap<String, Value> = HashMap::from([("first_name".into(), "Bob".into())]);
306306

307-
let expected: BTreeMap<String, Value> =
308-
BTreeMap::from([("first_name".into(), "Bob".into())]);
307+
let expected: HashMap<String, Value> = HashMap::from([("first_name".into(), "Bob".into())]);
309308

310309
let actual = execute_expressions(&fields, &expressions).unwrap();
311310
assert_eq!(actual, expected);
312311
}
313312

314313
#[test]
315314
fn test_functions() {
316-
let expressions: BTreeMap<String, String> = BTreeMap::from([
315+
let expressions: HashMap<String, String> = HashMap::from([
317316
("is_empty".into(), "''.is_empty() == true".into()),
318317
("to_lowercase".into(), "'Bob'.to_lowercase()".into()),
319318
("to_uppercase".into(), "'Bob'.to_uppercase()".into()),
@@ -327,43 +326,63 @@ mod tests {
327326
("date".into(), "date('1979-04-05')".into()),
328327
]);
329328

330-
let fields = BTreeMap::default();
331-
let expected: BTreeMap<String, Value> =
332-
BTreeMap::from([
333-
("is_empty".into(), true.into()),
334-
("to_lowercase".into(), "bob".into()),
335-
("to_uppercase".into(), "BOB".into()),
336-
("sha256".into(), "cd9fb1e148ccd8442e5aa74904cc73bf6fb54d1d54d333bd596aa9bb4bb4e961".into()),
337-
("sha384".into(), "b7808c5991933fa578a7d41a177b013f2f745a2c4fac90d1e8631a1ce21918dc5fee092a290a6443e47649989ec9871f".into()),
338-
("sha512".into(), "0c3e99453b4ae505617a3c9b6ce73fc3cd13ddc3b2e2237459710a57f8ec6d26d056db144ff7c71b00ed4e4c39716e9e2099c8076e604423dd74554d4db1e649".into()),
339-
("hex_encode".into(), "426f62".into()),
340-
("hex_decode".into(), "Bob".into()),
341-
("base64_encode".into(), "Qm9i".into()),
342-
("base64_decode".into(), "Bob".into()),
343-
("date".into(), "1979-04-05T00:00:00+00:00".into()),
344-
]);
345-
329+
let fields = HashMap::default();
330+
// Note: Using Vec for comparison since HashMap ordering is non-deterministic
346331
let actual = execute_expressions(&fields, &expressions).unwrap();
347-
assert_eq!(actual, expected);
332+
333+
assert_eq!(actual.get("is_empty"), Some(&Value::Bool(true)));
334+
assert_eq!(
335+
actual.get("to_lowercase"),
336+
Some(&Value::String("bob".into()))
337+
);
338+
assert_eq!(
339+
actual.get("to_uppercase"),
340+
Some(&Value::String("BOB".into()))
341+
);
342+
assert_eq!(
343+
actual.get("sha256"),
344+
Some(&Value::String(
345+
"cd9fb1e148ccd8442e5aa74904cc73bf6fb54d1d54d333bd596aa9bb4bb4e961".into()
346+
))
347+
);
348+
assert_eq!(actual.get("sha384"), Some(&Value::String("b7808c5991933fa578a7d41a177b013f2f745a2c4fac90d1e8631a1ce21918dc5fee092a290a6443e47649989ec9871f".into())));
349+
assert_eq!(actual.get("sha512"), Some(&Value::String("0c3e99453b4ae505617a3c9b6ce73fc3cd13ddc3b2e2237459710a57f8ec6d26d056db144ff7c71b00ed4e4c39716e9e2099c8076e604423dd74554d4db1e649".into())));
350+
assert_eq!(
351+
actual.get("hex_encode"),
352+
Some(&Value::String("426f62".into()))
353+
);
354+
assert_eq!(actual.get("hex_decode"), Some(&Value::String("Bob".into())));
355+
assert_eq!(
356+
actual.get("base64_encode"),
357+
Some(&Value::String("Qm9i".into()))
358+
);
359+
assert_eq!(
360+
actual.get("base64_decode"),
361+
Some(&Value::String("Bob".into()))
362+
);
363+
assert_eq!(
364+
actual.get("date"),
365+
Some(&Value::String("1979-04-05T00:00:00+00:00".into()))
366+
);
348367
}
349368

350369
#[test]
351370
fn test_complex() {
352-
let expressions: BTreeMap<String, String> =
353-
BTreeMap::from([("age".into(), "date(birth_date).age()".into())]);
371+
let expressions: HashMap<String, String> =
372+
HashMap::from([("age".into(), "date(birth_date).age()".into())]);
354373

355-
let fields: BTreeMap<String, Value> = BTreeMap::from([
374+
let fields: HashMap<String, Value> = HashMap::from([
356375
("first_name".into(), "Bob".into()),
357376
("birth_date".into(), "1979-01-01".into()),
358377
]);
359378

360-
let expected: BTreeMap<String, Value> = BTreeMap::from([
361-
("first_name".into(), "Bob".into()),
362-
("birth_date".into(), "1979-01-01".into()),
363-
("age".into(), 46.into()),
364-
]);
365-
366379
let actual = execute_expressions(&fields, &expressions).unwrap();
367-
assert_eq!(actual, expected);
380+
381+
assert_eq!(actual.get("first_name"), Some(&Value::String("Bob".into())));
382+
assert_eq!(
383+
actual.get("birth_date"),
384+
Some(&Value::String("1979-01-01".into()))
385+
);
386+
assert_eq!(actual.get("age"), Some(&Value::Number(46.into())));
368387
}
369388
}

0 commit comments

Comments
 (0)