diff --git a/CLAUDE.md b/CLAUDE.md index c27195e..fe6b9f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,7 +2,6 @@ ## Build & Test Commands - Build & run: `cargo build && cargo run` -- Run server: `cargo run server` - Run tests: `cargo test` (or `cargo test -- --nocapture` for verbose output) - Run single test: `cargo test test_name -- --nocapture` - Docker test: `./test_docker.sh` (includes vault initialization) @@ -10,15 +9,6 @@ - Lint: `cargo clippy -- -D warnings` - Format: `cargo fmt --all` -## CLI Commands -- Start server: `cargo run server [--vault-addr URL] [--api-port PORT]` -- Login: `cargo run login --username USER --password PASS [--api-url URL]` -- Upload document: `cargo run upload --name NAME --file PATH [--api-url URL]` -- Sign document: `cargo run sign --document-id ID --username USER --token TOKEN [--api-url URL]` -- Verify document: `cargo run verify --document-id ID [--api-url URL]` -- List documents: `cargo run list [--api-url URL]` -- Get document details: `cargo run get --document-id ID [--api-url URL]` - ## Code Style Guidelines - **Formatting**: Follow rustfmt conventions (run `cargo fmt` before committing) - **Imports**: Group by crate (stdlib → external → internal) diff --git a/Cargo.toml b/Cargo.toml index 1b492d1..3c7dfe2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -reqwest = { version = "0.11.18", features = ["json", "multipart"] } +reqwest = { version = "0.11.18", features = ["json"] } tokio = { version = "1.28.0", features = ["full"] } serde = { version = "1.0.160", features = ["derive"] } serde_json = "1.0.96" @@ -15,4 +15,3 @@ sha2 = "0.10.6" base64 = "0.21.0" tracing = "0.1.37" tracing-subscriber = { version = "0.3.17", features = ["env-filter"] } -clap = { version = "4.4.6", features = ["derive", "env"] } diff --git a/Dockerfile b/Dockerfile index 3d5123f..8bf3e59 100644 --- a/Dockerfile +++ b/Dockerfile @@ -31,7 +31,5 @@ RUN apt-get update && apt-get install -y vault WORKDIR /usr/local/bin COPY --from=builder /usr/src/vault-hier/target/release/vault-hier . - -# Set the entrypoint to directly run the Rust binary with the server command by default +# Set the entrypoint to directly run the Rust binary ENTRYPOINT ["/usr/local/bin/vault-hier"] -CMD ["server"] diff --git a/README.md b/README.md index 5fab328..5d5f23e 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ This project implements a hierarchical document signing system using HashiCorp V - **Hierarchical Signing**: Requires 3 of 5 signatures to validate a document, with at least 1 signature from each department - **Department Structure**: Two departments (Legal and Finance) with 5 users each - **Document API**: Upload, sign, and verify documents through a RESTful API -- **CLI Client**: Interact with the system through command-line interface - **Vault Integration**: Leverages HashiCorp Vault's Transit engine for cryptographic operations ## System Architecture @@ -51,46 +50,7 @@ The system consists of: - Legal department: legal1/legal1pass through legal5/legal5pass - Finance department: finance1/finance1pass through finance5/finance5pass -### CLI Commands - -The project includes a command-line interface to interact with the API: - -1. **Start the Server**: - ```bash - cargo run server [--vault-addr URL] [--api-port PORT] - ``` - -2. **Login**: - ```bash - cargo run login --username USER --password PASS [--api-url URL] - ``` - -3. **Upload Document**: - ```bash - cargo run upload --name NAME --file PATH [--api-url URL] - ``` - -4. **Sign Document**: - ```bash - cargo run sign --document-id ID --username USER --token TOKEN [--api-url URL] - ``` - -5. **Verify Document**: - ```bash - cargo run verify --document-id ID [--api-url URL] - ``` - -6. **List Documents**: - ```bash - cargo run list [--api-url URL] - ``` - -7. **Get Document Details**: - ```bash - cargo run get --document-id ID [--api-url URL] - ``` - -### API Usage Examples (curl) +### API Usage Examples 1. **Login**: ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 63ad0ec..b62ab73 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,8 +39,6 @@ services: deploy: restart_policy: condition: none - # Run with 'server' command - command: server --vault-addr http://vault:8200 --api-port 3000 volumes: vault-data: diff --git a/src/main.rs b/src/main.rs index d57cd89..2efb99b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,343 +1,40 @@ use anyhow::Result; -use clap::{Parser, Subcommand}; -use std::path::PathBuf; +use std::env; use tracing::{info}; use tracing_subscriber::{fmt, EnvFilter}; // Import our library use vault_hier::{start_api, initialize_vault}; -#[derive(Parser)] -#[command(author, version, about = "Hierarchical Document Signing with HashiCorp Vault")] -struct Cli { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand)] -enum Commands { - /// Start the vault-hier server - Server { - /// Address of the Vault server - #[arg(long, env = "VAULT_ADDR", default_value = "http://127.0.0.1:8200")] - vault_addr: String, - - /// Port for the API server - #[arg(long, env = "API_PORT", default_value_t = 3000)] - api_port: u16, - }, - - /// Login to get an authentication token - Login { - /// Username for authentication - #[arg(short, long)] - username: String, - - /// Password for authentication - #[arg(short, long)] - password: String, - - /// API server address - #[arg(long, default_value = "http://localhost:3000")] - api_url: String, - }, - - /// Upload a document - Upload { - /// Name of the document - #[arg(short, long)] - name: String, - - /// Path to the document file - #[arg(short, long)] - file: PathBuf, - - /// API server address - #[arg(long, default_value = "http://localhost:3000")] - api_url: String, - }, - - /// Sign a document - Sign { - /// Document ID to sign - #[arg(short, long)] - document_id: String, - - /// Username for signing - #[arg(short, long)] - username: String, - - /// Authentication token - #[arg(short, long)] - token: String, - - /// API server address - #[arg(long, default_value = "http://localhost:3000")] - api_url: String, - }, - - /// Verify a document's signatures - Verify { - /// Document ID to verify - #[arg(short, long)] - document_id: String, - - /// API server address - #[arg(long, default_value = "http://localhost:3000")] - api_url: String, - }, - - /// List all documents - List { - /// API server address - #[arg(long, default_value = "http://localhost:3000")] - api_url: String, - }, - - /// Get document details - Get { - /// Document ID to retrieve - #[arg(short, long)] - document_id: String, - - /// API server address - #[arg(long, default_value = "http://localhost:3000")] - api_url: String, - }, -} - #[tokio::main] async fn main() -> Result<()> { // Initialize tracing fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive("vault_hier=trace".parse()?)) + .with_env_filter(EnvFilter::from_default_env().add_directive("vault_hier=info".parse()?)) .with_target(false) .init(); - let cli = Cli::parse(); + // Get Vault address from env var or use default + let vault_addr = env::var("VAULT_ADDR").unwrap_or_else(|_| "http://127.0.0.1:8200".to_string()); - match cli.command { - Commands::Server { vault_addr, api_port } => { - run_server(&vault_addr, api_port).await?; - }, - Commands::Login { username, password, api_url } => { - login(&username, &password, &api_url).await?; - }, - Commands::Upload { name, file, api_url } => { - upload_document(&name, file, &api_url).await?; - }, - Commands::Sign { document_id, username, token, api_url } => { - sign_document(&document_id, &username, &token, &api_url).await?; - }, - Commands::Verify { document_id, api_url } => { - verify_document(&document_id, &api_url).await?; - }, - Commands::List { api_url } => { - list_documents(&api_url).await?; - }, - Commands::Get { document_id, api_url } => { - get_document(&document_id, &api_url).await?; - }, - } + // Get API port from env var or use default + let api_port = env::var("API_PORT") + .unwrap_or_else(|_| "3000".to_string()) + .parse::() + .unwrap_or(3000); - Ok(()) -} - -async fn run_server(vault_addr: &str, api_port: u16) -> Result<()> { info!("Vault address: {}", vault_addr); info!("Connecting to Vault at: {}", vault_addr); // Initialize and unseal Vault, get the root token - let root_token = initialize_vault(vault_addr).await?; + let root_token = initialize_vault(&vault_addr).await?; info!("Starting hierarchical document signing API..."); // Start the hierarchical signing API - start_api(vault_addr, &root_token, api_port).await?; + start_api(&vault_addr, &root_token, api_port).await?; info!("API server shutdown. Exiting."); Ok(()) } - -async fn login(username: &str, password: &str, api_url: &str) -> Result<()> { - info!("Logging in as user: {}", username); - - let client = reqwest::Client::new(); - let response = client - .post(&format!("{}/api/login", api_url)) - .json(&serde_json::json!({ - "username": username, - "password": password, - })) - .send() - .await?; - - if response.status().is_success() { - let data: serde_json::Value = response.json().await?; - println!("Login successful!"); - println!("Token: {}", data["token"]); - println!("Department: {}", data["department"]); - } else { - let error_text = response.text().await?; - println!("Login failed: {}", error_text); - } - - Ok(()) -} - -async fn upload_document(name: &str, file_path: PathBuf, api_url: &str) -> Result<()> { - info!("Uploading document: {}", name); - - let file_content = tokio::fs::read(&file_path).await?; - let file_name = file_path.file_name().unwrap().to_string_lossy(); - - let form = reqwest::multipart::Form::new() - .text("name", name.to_string()) - .part("file", reqwest::multipart::Part::bytes(file_content) - .file_name(file_name.to_string())); - - let client = reqwest::Client::new(); - let response = client - .post(&format!("{}/api/documents", api_url)) - .multipart(form) - .send() - .await?; - - if response.status().is_success() { - let data: serde_json::Value = response.json().await?; - println!("Document uploaded successfully!"); - println!("Document ID: {}", data["id"]); - } else { - let error_text = response.text().await?; - println!("Upload failed: {}", error_text); - } - - Ok(()) -} - -async fn sign_document(document_id: &str, username: &str, token: &str, api_url: &str) -> Result<()> { - info!("Signing document: {}", document_id); - - let client = reqwest::Client::new(); - let response = client - .post(&format!("{}/api/documents/{}/sign", api_url, document_id)) - .json(&serde_json::json!({ - "username": username, - "token": token, - })) - .send() - .await?; - - if response.status().is_success() { - let data: serde_json::Value = response.json().await?; - println!("Document signed successfully!"); - println!("Signature ID: {}", data["signature_id"]); - } else { - let error_text = response.text().await?; - println!("Signing failed: {}", error_text); - } - - Ok(()) -} - -async fn verify_document(document_id: &str, api_url: &str) -> Result<()> { - info!("Verifying document: {}", document_id); - - let client = reqwest::Client::new(); - let response = client - .get(&format!("{}/api/documents/{}/verify", api_url, document_id)) - .send() - .await?; - - if response.status().is_success() { - let data: serde_json::Value = response.json().await?; - println!("Verification result:"); - println!(" Valid: {}", data["valid"]); - println!(" Total signatures: {}", data["signature_count"]); - println!(" Departments represented: {}", data["departments_represented"]); - - if let Some(signatures) = data["signatures"].as_array() { - println!("\nSignatures:"); - for (i, sig) in signatures.iter().enumerate() { - println!(" {}. User: {}, Department: {}, Time: {}", - i+1, - sig["username"], - sig["department"], - sig["timestamp"]); - } - } - } else { - let error_text = response.text().await?; - println!("Verification failed: {}", error_text); - } - - Ok(()) -} - -async fn list_documents(api_url: &str) -> Result<()> { - info!("Listing all documents"); - - let client = reqwest::Client::new(); - let response = client - .get(&format!("{}/api/documents", api_url)) - .send() - .await?; - - if response.status().is_success() { - let data: Vec = response.json().await?; - println!("Documents:"); - - for (i, doc) in data.iter().enumerate() { - println!(" {}. ID: {}, Name: {}", i+1, doc["id"], doc["name"]); - } - - if data.is_empty() { - println!(" No documents found."); - } - } else { - let error_text = response.text().await?; - println!("Failed to list documents: {}", error_text); - } - - Ok(()) -} - -async fn get_document(document_id: &str, api_url: &str) -> Result<()> { - info!("Getting document: {}", document_id); - - let client = reqwest::Client::new(); - let response = client - .get(&format!("{}/api/documents/{}", api_url, document_id)) - .send() - .await?; - - if response.status().is_success() { - let doc: serde_json::Value = response.json().await?; - println!("Document details:"); - println!(" ID: {}", doc["id"]); - println!(" Name: {}", doc["name"]); - println!(" Hash: {}", doc["hash"]); - println!(" Created: {}", doc["created_at"]); - - if let Some(signatures) = doc["signatures"].as_array() { - println!("\nSignatures:"); - for (i, sig) in signatures.iter().enumerate() { - println!(" {}. User: {}, Department: {}, Time: {}", - i+1, - sig["username"], - sig["department"], - sig["timestamp"]); - } - - if signatures.is_empty() { - println!(" No signatures yet."); - } - } - } else { - let error_text = response.text().await?; - println!("Failed to get document: {}", error_text); - } - - Ok(()) -} diff --git a/src/vault_setup.rs b/src/vault_setup.rs index 872840e..771b3cf 100644 --- a/src/vault_setup.rs +++ b/src/vault_setup.rs @@ -442,9 +442,6 @@ impl VaultClient { Department::Finance => "finance", }; - // Get the username from the policy name (remove "-policy" suffix) - let username = policy_name.trim_end_matches("-policy"); - // Policy content with specific paths for the department let policy = format!(r#" # Allow reading document metadata @@ -452,8 +449,8 @@ impl VaultClient { capabilities = ["read"] }} - # Allow signing with user's key - use explicit username instead of identity.entity.name - path "transit/sign/{}" {{ + # Allow signing with user's key + path "transit/sign/{{{{identity.entity.name}}}}" {{ capabilities = ["update"] }} @@ -466,7 +463,7 @@ impl VaultClient { path "documents/data/dept/{}/signatures/*" {{ capabilities = ["create", "read", "update"] }} - "#, username, dept_name); + "#, dept_name); let url = format!("{}/v1/sys/policies/acl/{}", self.addr, policy_name); let payload = json!({ diff --git a/test_local.sh b/test_local.sh index 71d82fa..071f85e 100755 --- a/test_local.sh +++ b/test_local.sh @@ -16,56 +16,6 @@ else VAULT_PID_FILE="./vault.pid" fi -# Use a fixed port for the API to make debugging easier -API_PORT=3456 -API_URL="http://localhost:$API_PORT" - -# Error handling function -handle_error() { - echo "Error encountered, showing logs:" - if [ -f "./api_server.log" ]; then - echo "=== API Server Log ===" - cat ./api_server.log - echo "======================" - fi - - if [ -f "./vault_server.log" ]; then - echo "=== Vault Server Log ===" - tail -n 100 ./vault_server.log - echo "=======================" - fi - - # Call cleanup - cleanup - exit 1 -} - -# Function to cleanup on exit -cleanup() { - echo "Cleaning up resources..." - if [ -n "$SERVER_PID" ]; then - echo "Stopping server process ($SERVER_PID)..." - kill -9 $SERVER_PID 2>/dev/null || true - fi - if [ -f "$VAULT_PID_FILE" ]; then - VAULT_PID=$(cat "$VAULT_PID_FILE") - echo "Stopping vault process ($VAULT_PID)..." - kill -9 $VAULT_PID 2>/dev/null || true - rm -f "$VAULT_PID_FILE" - fi - killall vault-hier - rm -f test_document.txt - rm -rf /tmp/vault-test - # We'll keep the logs for inspection - # rm -f ./vault_server.log - # rm -f ./api_server.log - echo "Cleanup complete." -} - -# Set trap for errors and interrupts -trap handle_error ERR -trap cleanup EXIT - # Check if Vault is installed if ! command -v vault &> /dev/null; then echo "Vault is not installed. Please install it first." @@ -135,125 +85,35 @@ for i in {1..10}; do sleep 2 done -# Build and run the Rust application with the server command -echo "Building and running the vault-hier server..." -echo "Using API port: $API_PORT" -cargo build && cargo run server --vault-addr "$VAULT_ADDR" --api-port $API_PORT > ./api_server.log 2>&1 & -SERVER_PID=$! -echo "Server started with PID $SERVER_PID" - -# Wait for the server to start -echo "Waiting for the server to start..." -sleep 10 # Increased wait time to ensure server is ready - -# Test the server with some client operations -echo "Testing the client operations..." - -# Create a sample file for testing -echo "Creating a sample file for testing..." -echo "This is a test document" > test_document.txt - -# Test login with legal1 user -echo "Testing login with legal1 user..." -LOGIN_OUTPUT=$(cargo run login --username legal1 --password legal1pass --api-url "$API_URL") -echo "$LOGIN_OUTPUT" -LEGAL_TOKEN=$(echo "$LOGIN_OUTPUT" | grep "Token:" | awk '{print $2}' | tr -d '"') - -if [ -z "$LEGAL_TOKEN" ]; then - echo "Login failed for legal1. Could not get token." - handle_error -fi - -echo "Login successful for legal1, got token: ${LEGAL_TOKEN:0:8}..." - -# Test upload document -echo "Testing document upload..." -UPLOAD_OUTPUT=$(cargo run upload --name "Test Document" --file test_document.txt --api-url "$API_URL") -echo "$UPLOAD_OUTPUT" -DOC_ID=$(echo "$UPLOAD_OUTPUT" | grep "Document ID:" | awk '{print $3}' | tr -d '"') - -if [ -z "$DOC_ID" ]; then - echo "Upload failed. Could not get document ID." - handle_error -fi - -echo "Upload successful, got document ID: $DOC_ID" - -# Test using direct curl with the legal token -echo "Testing document signing with legal token via curl..." -echo "Using token: $LEGAL_TOKEN" -SIGN_OUTPUT=$(curl -s -X POST "$API_URL/api/documents/$DOC_ID/sign" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"legal1\",\"token\":\"$LEGAL_TOKEN\"}") -echo "$SIGN_OUTPUT" - -if echo "$SIGN_OUTPUT" | grep -q "signatures"; then - echo "Document signed successfully" -else - echo "Signing failed with curl. Trying with finance user..." - - # Try with finance user - echo "Testing login with finance1 user..." - LOGIN_OUTPUT=$(cargo run login --username finance1 --password finance1pass --api-url "$API_URL") - echo "$LOGIN_OUTPUT" - FINANCE_TOKEN=$(echo "$LOGIN_OUTPUT" | grep "Token:" | awk '{print $2}' | tr -d '"') - - if [ -z "$FINANCE_TOKEN" ]; then - echo "Login failed for finance1. Could not get token." - handle_error - fi - - echo "Login successful for finance1, got token: ${FINANCE_TOKEN:0:8}..." - - echo "Testing document signing with finance token via curl..." - SIGN_OUTPUT=$(curl -s -X POST "$API_URL/api/documents/$DOC_ID/sign" \ - -H "Content-Type: application/json" \ - -d "{\"username\":\"finance1\",\"token\":\"$FINANCE_TOKEN\"}") - echo "$SIGN_OUTPUT" - - if ! echo "$SIGN_OUTPUT" | grep -q "signatures"; then - echo "Signing failed with both legal and finance users. Skipping rest of test." - handle_error - fi -fi - -# Test verification -echo "Testing document verification..." -VERIFY_OUTPUT=$(cargo run verify --document-id "$DOC_ID" --api-url "$API_URL") -echo "$VERIFY_OUTPUT" - -if echo "$VERIFY_OUTPUT" | grep -q "Verification result"; then - echo "Verification successful" -else - echo "Verification failed" - handle_error -fi - -# Test getting document details -echo "Testing get document details..." -GET_OUTPUT=$(cargo run get --document-id "$DOC_ID" --api-url "$API_URL") -echo "$GET_OUTPUT" - -if echo "$GET_OUTPUT" | grep -q "Document details"; then - echo "Get document successful" -else - echo "Get document failed" - handle_error -fi +# Build and run the Rust application +echo "Building and running the Rust application..." +cargo build && cargo run # Check if the credentials file was created -if [ -f "vault-credentials.txt" ] || [ -f "vault-credentials.json" ]; then - echo "Test successful! Credentials were saved" +if [ -f "vault-credentials.txt" ]; then + echo "Test successful! Credentials were saved to vault-credentials.txt" + # Extract the unseal keys for demonstration + UNSEAL_KEYS=$(grep "Key" vault-credentials.txt | head -n 3 | awk '{print $3}') + ROOT_TOKEN=$(grep "Root Token" vault-credentials.txt | awk '{print $3}') - if [ -f "vault-credentials.txt" ]; then - # Extract the unseal keys for demonstration - UNSEAL_KEYS=$(grep "Key" vault-credentials.txt | head -n 3 | awk '{print $3}') - ROOT_TOKEN=$(grep "Root Token" vault-credentials.txt | awk '{print $3}') + echo "Root Token: $ROOT_TOKEN" + echo "First 3 Unseal Keys (needed for threshold):" + echo "$UNSEAL_KEYS" - echo "Root Token: $ROOT_TOKEN" - echo "First 3 Unseal Keys (needed for threshold):" - echo "$UNSEAL_KEYS" - fi + # Clean up temporary files + rm -f vault-credentials.txt +else + echo "Test failed! Credentials file was not created." + exit 1 fi -echo -e "\nTest complete! All tests passed." +echo -e "\nTest complete! Cleaning up..." +# Stop Vault server +kill -9 $VAULT_PID +rm "$VAULT_PID_FILE" + +# Clean up test environment +rm -rf /tmp/vault-test +rm -f ./vault_server.log + +echo "All cleaned up. Test successful!"