Skip to content

Commit fa6a43c

Browse files
refactor: Transform CLI to modular architecture
- Refactor 842-line main.rs into clean 35-line entry point - Organize commands into focused modules by category: * info.rs - Information and listing commands * token.rs - Token management operations * keys.rs - Key management operations * crypto.rs - Sign/verify/encrypt/decrypt operations * symmetric.rs - Symmetric key operations * key_wrap.rs - Key wrapping/unwrapping * mac.rs - HMAC and CMAC operations * util.rs - Utility commands (benchmark, audit, troubleshooting) * analyze.rs - Observability log analysis - Add commands/common.rs for shared utilities (PIN handling, config) - Implement commands/mod.rs as main dispatcher with routing logic - All 55 integration tests pass - no functionality lost - Update documentation with CLI_ARCHITECTURE.md - Improve maintainability, testability, and extensibility Breaking: None (full backward compatibility maintained) Features: Modular command architecture for easier maintenance
1 parent fca99e4 commit fa6a43c

16 files changed

Lines changed: 1316 additions & 830 deletions

File tree

.github/copilot-instructions.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,13 @@ Keep this pattern; observability layers should complement, not replace.
192192
---
193193

194194
## CLI Conventions
195-
- Use `clap` derive macros; keep subcommands in `cli.rs`, dispatch in `main.rs`.
196-
- Keep operations in `pkcs11/*` modules.
197-
- Add tests to `test.sh` when introducing core ops.
195+
- **Modular Architecture**: Commands are organized into focused handlers in `src/commands/`
196+
- **Entry Point**: `main.rs` is now a clean 35-line file with configuration and dispatch
197+
- **Command Handlers**: Each category (info, token, keys, crypto, etc.) has its own module
198+
- **Shared Utilities**: Common functionality (PIN handling, config) in `commands/common.rs`
199+
- Use `clap` derive macros; keep subcommands in `cli.rs`, dispatch via `commands::dispatch_command`
200+
- Keep core operations in `rust-hsm-core/src/pkcs11/*` modules
201+
- Add tests to `test.sh` when introducing core ops
198202

199203
---
200204

@@ -398,12 +402,24 @@ let key_handle = session.find_objects(&template)?.first().copied()
398402
## Adding New Commands
399403

400404
1. Add variant to `Commands` enum in [cli.rs](../crates/rust-hsm-cli/src/cli.rs)
401-
2. Implement function in appropriate `pkcs11/*.rs` module
402-
3. Add export to [pkcs11/keys/mod.rs](../crates/rust-hsm-cli/src/pkcs11/keys/mod.rs) if needed
403-
4. Add match arm in [main.rs](../crates/rust-hsm-cli/src/main.rs) to dispatch to your function
404-
5. Add test case to [test.sh](../test.sh) for SoftHSM2
405-
6. Add test case to [testKryoptic.sh](../testKryoptic.sh) for Kryoptic
406-
7. Update command documentation in [docs/commands/](../docs/commands/)
405+
2. Implement function in appropriate `rust-hsm-core/src/pkcs11/*.rs` module
406+
3. Add handler to appropriate `commands/*.rs` module (or create new category)
407+
4. Update dispatcher in [commands/mod.rs](../crates/rust-hsm-cli/src/commands/mod.rs)
408+
5. Use shared utilities from [commands/common.rs](../crates/rust-hsm-cli/src/commands/common.rs) for PIN handling and config
409+
6. Add test case to [test.sh](../test.sh) for SoftHSM2
410+
7. Add test case to [testKryoptic.sh](../testKryoptic.sh) for Kryoptic
411+
8. Update command documentation in [docs/commands/](../docs/commands/)
412+
413+
**Command Handler Categories:**
414+
- `info.rs` - Information and listing commands
415+
- `token.rs` - Token management operations
416+
- `keys.rs` - Key management operations
417+
- `crypto.rs` - Cryptographic operations (sign, verify, encrypt, decrypt)
418+
- `symmetric.rs` - Symmetric key operations
419+
- `key_wrap.rs` - Key wrapping/unwrapping
420+
- `mac.rs` - HMAC and CMAC operations
421+
- `util.rs` - Utility commands (benchmark, audit, troubleshooting)
422+
- `analyze.rs` - Observability log analysis
407423

408424
---
409425

README.md

1.42 KB

Project Structure

rust-hsm/
  README.md
  .github/
    copilot-instructions.md  # AI agent guidance
  docker/
    Dockerfile               # Single container with SoftHSM2 + Rust CLI
    softhsm2.conf           # SoftHSM configuration
    entrypoint.sh           # Container startup script
  compose.yaml              # Docker Compose configuration
  crates/
    rust-hsm-cli/
      Cargo.toml
      src/
        main.rs             # CLI entry point
        pkcs11/
          mod.rs            # Module exports
          errors.rs         # Custom error types
          info.rs           # Module info command
          slots.rs          # Slot listing
          token.rs          # Token management
          keys.rs           # Key operations (gen, sign, verify)
          objects.rs        # Object listing
rust-hsm/
  README.md
  .github/
    copilot-instructions.md  # AI agent guidance
  docker/
    Dockerfile               # Single container with SoftHSM2 + Rust CLI
    softhsm2.conf           # SoftHSM configuration
    entrypoint.sh           # Container startup script
  compose.yaml              # Docker Compose configuration
  crates/
    rust-hsm-cli/           # Modular CLI with organized command handlers
      Cargo.toml
      src/
        main.rs             # Clean 35-line entry point
        cli.rs              # Command definitions
        config.rs           # Configuration handling
        commands/           # Modular command handlers
          mod.rs            # Main dispatcher
          common.rs         # Shared utilities (PIN, config)
          info.rs           # Information commands
          token.rs          # Token management
          keys.rs           # Key management
          crypto.rs         # Sign/verify/encrypt/decrypt
          symmetric.rs      # Symmetric operations
          key_wrap.rs       # Key wrapping
          mac.rs            # HMAC/CMAC operations
          util.rs           # Benchmark/audit/troubleshooting
          analyze.rs        # Log analysis
    rust-hsm-core/          # Core PKCS#11 operations
      src/
        pkcs11/             # PKCS#11 functionality
    observe-core/           # Observability event schema
    observe-cryptoki/       # Cryptoki observability wrapper
    rust-hsm-analyze/       # Log analysis and statistics

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
//! Log analysis command
2+
3+
use anyhow::Result;
4+
5+
pub fn handle_analyze(log_file: String, format: String) -> Result<()> {
6+
use rust_hsm_analyze::{parse_observe_json, Analyzer};
7+
8+
// Parse the log file
9+
let events = parse_observe_json(&log_file)?;
10+
11+
if events.is_empty() {
12+
println!("No events found in log file");
13+
return Ok(());
14+
}
15+
16+
// Analyze the events
17+
let analyzer = Analyzer::new(events);
18+
let analysis = analyzer.analyze();
19+
20+
// Output results
21+
if format == "json" {
22+
println!("{}", serde_json::to_string_pretty(&analysis)?);
23+
} else {
24+
println!("\n=== PKCS#11 Session Analysis ===\n");
25+
println!("Total Operations: {}", analysis.total_ops);
26+
println!("Success Rate: {:.2}%", analysis.success_rate);
27+
println!("Error Count: {}", analysis.error_count);
28+
29+
println!("\n--- Overall Timing ---");
30+
println!("Total Duration: {:.2}ms", analysis.duration_stats.total_ms);
31+
println!("Average: {:.2}ms", analysis.duration_stats.avg_ms);
32+
println!("Min: {:.2}ms", analysis.duration_stats.min_ms);
33+
println!("Max: {:.2}ms", analysis.duration_stats.max_ms);
34+
println!("P50: {:.2}ms", analysis.duration_stats.p50_ms);
35+
println!("P95: {:.2}ms", analysis.duration_stats.p95_ms);
36+
println!("P99: {:.2}ms", analysis.duration_stats.p99_ms);
37+
38+
if !analysis.by_function.is_empty() {
39+
println!("\n--- Per-Function Statistics ---");
40+
for (func, stats) in &analysis.by_function {
41+
println!("\n{}", func);
42+
println!(" Calls: {}", stats.count);
43+
let success_rate = if stats.count > 0 {
44+
(stats.success_count as f64 / stats.count as f64) * 100.0
45+
} else {
46+
0.0
47+
};
48+
let error_rate = if stats.count > 0 {
49+
(stats.error_count as f64 / stats.count as f64) * 100.0
50+
} else {
51+
0.0
52+
};
53+
println!(" Success: {} ({:.1}%)", stats.success_count, success_rate);
54+
println!(" Errors: {} ({:.1}%)", stats.error_count, error_rate);
55+
println!(" Avg Duration: {:.2}ms", stats.avg_duration_ms);
56+
}
57+
}
58+
59+
if !analysis.errors.is_empty() {
60+
println!("\n--- Errors ---");
61+
for error in &analysis.errors {
62+
println!("\n{} → {} ({})", error.func, error.rv_name, error.rv);
63+
println!(" Count: {}", error.count);
64+
println!(" First seen: {}", error.timestamp);
65+
}
66+
}
67+
}
68+
69+
Ok(())
70+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//! Common utilities and structures for command handling
2+
3+
use anyhow::Result;
4+
use std::env;
5+
use std::io::{self, BufRead};
6+
7+
use crate::config::Config;
8+
9+
/// Shared context for all command handlers
10+
pub struct CommandContext {
11+
pub config: Config,
12+
pub module_path: String,
13+
}
14+
15+
impl CommandContext {
16+
pub fn new(config: Config) -> Result<Self> {
17+
let module_path =
18+
env::var("PKCS11_MODULE").unwrap_or_else(|_| config.get_pkcs11_module().to_string());
19+
20+
tracing::info!("Using PKCS#11 module: {}", module_path);
21+
22+
Ok(CommandContext {
23+
config,
24+
module_path,
25+
})
26+
}
27+
28+
/// Get token label, using CLI value or config default
29+
pub fn token_label(&self, cli_value: Option<String>) -> Result<String> {
30+
self.config
31+
.token_label(cli_value.as_deref())
32+
.ok_or_else(|| anyhow::anyhow!("Token label required"))
33+
}
34+
}
35+
36+
/// Read a PIN from stdin, trimming whitespace
37+
pub fn read_pin_from_stdin() -> Result<String> {
38+
let stdin = io::stdin();
39+
let mut line = String::new();
40+
stdin.lock().read_line(&mut line)?;
41+
Ok(line.trim().to_string())
42+
}
43+
44+
/// Common PIN handling logic
45+
pub fn get_pin(pin: Option<String>, use_stdin: bool, error_msg: &str) -> Result<String> {
46+
if use_stdin {
47+
read_pin_from_stdin()
48+
} else {
49+
pin.ok_or_else(|| anyhow::anyhow!("{}", error_msg))
50+
}
51+
}
52+
53+
/// Get user PIN with consistent error handling
54+
pub fn get_user_pin(pin: Option<String>, use_stdin: bool) -> Result<String> {
55+
get_pin(pin, use_stdin, "User PIN required")
56+
}
57+
58+
/// Get SO PIN with consistent error handling
59+
pub fn get_so_pin(pin: Option<String>, use_stdin: bool) -> Result<String> {
60+
get_pin(pin, use_stdin, "SO PIN required")
61+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//! Cryptographic operation commands
2+
3+
use anyhow::Result;
4+
5+
use super::common::{get_user_pin, CommandContext};
6+
7+
pub fn handle_sign(
8+
ctx: CommandContext,
9+
label: Option<String>,
10+
user_pin: Option<String>,
11+
key_label: String,
12+
input: String,
13+
output: String,
14+
json: bool,
15+
pin_stdin: bool,
16+
) -> Result<()> {
17+
let token_label = ctx.token_label(label)?;
18+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
19+
20+
rust_hsm_core::keys::sign(
21+
&ctx.module_path,
22+
&token_label,
23+
&user_pin_value,
24+
&key_label,
25+
&input,
26+
&output,
27+
json,
28+
ctx.config.observe_enabled,
29+
&ctx.config.observe_log_file,
30+
)
31+
}
32+
33+
pub fn handle_verify(
34+
ctx: CommandContext,
35+
label: Option<String>,
36+
user_pin: Option<String>,
37+
key_label: String,
38+
input: String,
39+
signature: String,
40+
json: bool,
41+
pin_stdin: bool,
42+
) -> Result<()> {
43+
let token_label = ctx.token_label(label)?;
44+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
45+
46+
rust_hsm_core::keys::verify(
47+
&ctx.module_path,
48+
&token_label,
49+
&user_pin_value,
50+
&key_label,
51+
&input,
52+
&signature,
53+
json,
54+
)
55+
}
56+
57+
pub fn handle_encrypt(
58+
ctx: CommandContext,
59+
label: Option<String>,
60+
user_pin: Option<String>,
61+
key_label: String,
62+
input: String,
63+
output: String,
64+
pin_stdin: bool,
65+
) -> Result<()> {
66+
let token_label = ctx.token_label(label)?;
67+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
68+
69+
rust_hsm_core::keys::encrypt(
70+
&ctx.module_path,
71+
&token_label,
72+
&user_pin_value,
73+
&key_label,
74+
&input,
75+
&output,
76+
)
77+
}
78+
79+
pub fn handle_decrypt(
80+
ctx: CommandContext,
81+
label: Option<String>,
82+
user_pin: Option<String>,
83+
key_label: String,
84+
input: String,
85+
output: String,
86+
pin_stdin: bool,
87+
) -> Result<()> {
88+
let token_label = ctx.token_label(label)?;
89+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
90+
91+
rust_hsm_core::keys::decrypt(
92+
&ctx.module_path,
93+
&token_label,
94+
&user_pin_value,
95+
&key_label,
96+
&input,
97+
&output,
98+
)
99+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//! Information and listing commands
2+
3+
use anyhow::Result;
4+
5+
use super::common::CommandContext;
6+
7+
pub fn handle_info(ctx: CommandContext, json: bool) -> Result<()> {
8+
rust_hsm_core::info::display_info(&ctx.module_path, json)
9+
}
10+
11+
pub fn handle_list_slots(ctx: CommandContext, json: bool) -> Result<()> {
12+
rust_hsm_core::slots::list_slots(&ctx.module_path, json)
13+
}
14+
15+
pub fn handle_list_mechanisms(
16+
ctx: CommandContext,
17+
slot: Option<u64>,
18+
detailed: bool,
19+
json: bool,
20+
) -> Result<()> {
21+
rust_hsm_core::info::list_mechanisms(&ctx.module_path, slot, detailed, json)
22+
}
23+
24+
pub fn handle_list_objects(
25+
ctx: CommandContext,
26+
label: Option<String>,
27+
user_pin: Option<String>,
28+
pin_stdin: bool,
29+
detailed: bool,
30+
json: bool,
31+
) -> Result<()> {
32+
use super::common::get_user_pin;
33+
34+
let token_label = ctx.token_label(label)?;
35+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
36+
37+
rust_hsm_core::objects::list_objects(
38+
&ctx.module_path,
39+
&token_label,
40+
&user_pin_value,
41+
detailed,
42+
json,
43+
)
44+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//! Key wrapping operation commands
2+
3+
use anyhow::Result;
4+
5+
use super::common::{get_user_pin, CommandContext};
6+
7+
pub fn handle_wrap_key(
8+
ctx: CommandContext,
9+
label: Option<String>,
10+
user_pin: Option<String>,
11+
key_label: String,
12+
wrapping_key_label: String,
13+
output: String,
14+
pin_stdin: bool,
15+
) -> Result<()> {
16+
let token_label = ctx.token_label(label)?;
17+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
18+
19+
rust_hsm_core::keys::wrap_key(
20+
&ctx.module_path,
21+
&token_label,
22+
&user_pin_value,
23+
&key_label,
24+
&wrapping_key_label,
25+
&output,
26+
)
27+
}
28+
29+
pub fn handle_unwrap_key(
30+
ctx: CommandContext,
31+
label: Option<String>,
32+
user_pin: Option<String>,
33+
key_label: String,
34+
wrapping_key_label: String,
35+
input: String,
36+
key_type: String,
37+
pin_stdin: bool,
38+
) -> Result<()> {
39+
let token_label = ctx.token_label(label)?;
40+
let user_pin_value = get_user_pin(user_pin, pin_stdin)?;
41+
42+
rust_hsm_core::keys::unwrap_key(
43+
&ctx.module_path,
44+
&token_label,
45+
&user_pin_value,
46+
&key_label,
47+
&wrapping_key_label,
48+
&input,
49+
&key_type,
50+
)
51+
}

0 commit comments

Comments
 (0)