diff --git a/cmd/mpcium/main.go b/cmd/mpcium/main.go index f4d7ac96..f1762ee4 100644 --- a/cmd/mpcium/main.go +++ b/cmd/mpcium/main.go @@ -31,7 +31,7 @@ import ( ) const ( - Version = "0.3.1" + Version = "0.3.2" DefaultBackupPeriodSeconds = 300 // (5 minutes) ) diff --git a/deployments/systemd/README.md b/deployments/systemd/README.md index ebf47a92..482f88e5 100644 --- a/deployments/systemd/README.md +++ b/deployments/systemd/README.md @@ -10,16 +10,6 @@ This directory contains deployment scripts and configurations for **production d Mpcium is a distributed threshold cryptographic system that requires multiple nodes to collaborate for secure operations. This deployment guide covers setting up a **secure, production-ready** MPC cluster with proper security hardening, systemd integration, and operational best practices. -## Quick Start (Recommended) - -For automated deployment, use the setup script: - -```bash -sudo ./setup-config.sh -``` - -This script handles all configuration, permissions, and service setup automatically. - ## Prerequisites ### Infrastructure Requirements @@ -99,7 +89,8 @@ On **one designated node** only: mpcium-cli generate-initiator --encrypt ``` -⚠️ **Important**: +⚠️ **Important**: + - This creates an encrypted private key file with `.key.age` extension that you'll need to securely distribute to application nodes that initiate MPC operations - Copy the public key from `initiator_identity.json` and update the `event_initiator_pubkey` field in `/etc/mpcium/config.yaml` on **all nodes** diff --git a/deployments/systemd/setup-config.sh b/deployments/systemd/setup-config.sh index 3e920a3b..c1a285b2 100755 --- a/deployments/systemd/setup-config.sh +++ b/deployments/systemd/setup-config.sh @@ -109,37 +109,28 @@ check_root() { fi } -# Prompt for environment selection -setup_environment() { - log_step "Setting up environment..." +# Get environment from config.yaml +get_environment_from_config() { + log_step "Reading environment from config.yaml..." - local environment="" - echo -e "${BLUE}[PROMPT]${NC} Select environment:" - echo -e " ${YELLOW}1) development${NC} - For testing and development" - echo -e " ${YELLOW}2) production${NC} - For production deployment with full security" + local config_file="/etc/mpcium/config.yaml" - while true; do - read -p "> Enter choice (1 or 2): " choice - echo - - case "$choice" in - 1) - environment="development" - break - ;; - 2) - environment="production" - break - ;; - *) - log_error "Invalid choice. Please enter 1 for development or 2 for production." - ;; - esac - done + if [[ ! -f "$config_file" ]]; then + log_error "Config file not found at $config_file" + exit 1 + fi + + # Extract environment from config.yaml + local environment=$(grep "^environment:" "$config_file" | sed 's/environment: *//g' | sed 's/"//g' | sed "s/'//g") + + if [[ -z "$environment" ]]; then + environment="production" # default + log_warn "Environment not specified in config.yaml, defaulting to production" + fi # Store the environment for later use MPCIUM_ENVIRONMENT="$environment" - log_info "Environment set to: $MPCIUM_ENVIRONMENT" + log_info "Environment from config.yaml: $MPCIUM_ENVIRONMENT" } # Prompt for MPCIUM_NODE_NAME if not provided @@ -426,62 +417,128 @@ check_environment() { fi } -# Check config.yaml for missing credentials and prompt if needed -check_and_update_config_credentials() { - log_step "Checking configuration credentials..." +# Validate config.yaml has all required credentials +validate_config_credentials() { + log_step "Validating configuration credentials..." - local config_file="/etc/mpcium/config.yaml" - local updated=false + local config_file="${1:-/etc/mpcium/config.yaml}" + local errors=0 if [[ ! -f "$config_file" ]]; then log_error "Config file not found at $config_file - the configuration file doesn't exist yet" return 1 fi - # Initialize flags to track which credentials were collected - COLLECT_NATS_PASSWORD=false - COLLECT_CONSUL_PASSWORD=false - COLLECT_CONSUL_TOKEN=false - - # Check for missing nats.password - if missing or empty, prompt for .env - if ! grep -A 10 "^nats:" "$config_file" | grep -q "password:" || grep -A 10 "^nats:" "$config_file" | grep -q "password: *$" || grep -A 10 "^nats:" "$config_file" | grep -q 'password: ""'; then - log_info "NATS password not configured - collecting for .env file" - local nats_password - prompt_secret_optional "NATS password" "nats_password" - NATS_PASSWORD="$nats_password" - COLLECT_NATS_PASSWORD=true + log_info "Validating config file: $config_file" + + # Note: badger_password is provided via systemd credentials, not config.yaml + log_info "[i] badger_password will be provided via systemd credentials" + + # Check for required event_initiator_pubkey + if ! grep -q "^event_initiator_pubkey:" "$config_file" || grep -q "^event_initiator_pubkey: *$" "$config_file" || grep -q '^event_initiator_pubkey: ""' "$config_file"; then + log_error "❌ event_initiator_pubkey not configured in config.yaml" + ((errors++)) else - log_info "NATS password already configured in config.yaml - skipping" + log_info "✓ event_initiator_pubkey configured" fi - # Check for missing consul.password - if missing or empty, prompt for .env - if ! grep -A 10 "^consul:" "$config_file" | grep -q "password:" || grep -A 10 "^consul:" "$config_file" | grep -q "password: *$" || grep -A 10 "^consul:" "$config_file" | grep -q 'password: ""'; then - log_info "Consul password not configured - collecting for .env file" - local consul_password - prompt_secret_optional "Consul password" "consul_password" - CONSUL_PASSWORD="$consul_password" - COLLECT_CONSUL_PASSWORD=true + # Check for NATS configuration + local nats_url=$(grep -A 10 "^nats:" "$config_file" | grep "url:" | sed 's/.*url: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + if [[ -z "$nats_url" ]]; then + log_error "❌ nats.url not configured in config.yaml" + ((errors++)) else - log_info "Consul password already configured in config.yaml - skipping" + log_info "✓ nats.url configured: $nats_url" + + # If NATS URL uses TLS, validate TLS certificate configuration + if [[ "$nats_url" =~ ^tls:// ]]; then + log_info "[TLS] TLS URL detected, validating certificate configuration..." + + local client_cert=$(grep -A 20 "^nats:" "$config_file" | grep -A 10 "tls:" | grep "client_cert:" | sed 's/.*client_cert: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + local client_key=$(grep -A 20 "^nats:" "$config_file" | grep -A 10 "tls:" | grep "client_key:" | sed 's/.*client_key: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + local ca_cert=$(grep -A 20 "^nats:" "$config_file" | grep -A 10 "tls:" | grep "ca_cert:" | sed 's/.*ca_cert: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + + local tls_errors=0 + + if [[ -z "$client_cert" ]]; then + log_error "❌ nats.tls.client_cert not configured (required for TLS URL)" + ((errors++)) + ((tls_errors++)) + else + log_info "✓ nats.tls.client_cert configured: $client_cert" + fi + + if [[ -z "$client_key" ]]; then + log_error "❌ nats.tls.client_key not configured (required for TLS URL)" + ((errors++)) + ((tls_errors++)) + else + log_info "✓ nats.tls.client_key configured: $client_key" + fi + + if [[ -z "$ca_cert" ]]; then + log_error "❌ nats.tls.ca_cert not configured (required for TLS URL)" + ((errors++)) + ((tls_errors++)) + else + log_info "✓ nats.tls.ca_cert configured: $ca_cert" + fi + + if [[ $tls_errors -eq 0 ]]; then + log_info "[OK] All NATS TLS certificates configured" + fi + else + log_warn "[!] NATS URL is not using TLS (consider using tls:// for production)" + fi fi - # Check for missing consul.token - if missing or empty, prompt for .env - if ! grep -A 10 "^consul:" "$config_file" | grep -q "token:" || grep -A 10 "^consul:" "$config_file" | grep -q "token: *$" || grep -A 10 "^consul:" "$config_file" | grep -q 'token: ""'; then - log_info "Consul token not configured - collecting for .env file" - local consul_token - prompt_secret_optional "Consul token" "consul_token" - CONSUL_TOKEN="$consul_token" - COLLECT_CONSUL_TOKEN=true + # Check for Consul configuration + local consul_address=$(grep -A 10 "^consul:" "$config_file" | grep "address:" | sed 's/.*address: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + if [[ -z "$consul_address" ]]; then + log_error "❌ consul.address not configured in config.yaml" + ((errors++)) else - log_info "Consul token already configured in config.yaml - skipping" + log_info "✓ consul.address configured: $consul_address" + + # If Consul address uses HTTPS, validate token configuration + if [[ "$consul_address" =~ ^https:// ]]; then + log_info "[HTTPS] HTTPS address detected, validating token configuration..." + + local consul_token=$(grep -A 10 "^consul:" "$config_file" | grep "token:" | sed 's/.*token: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + + if [[ -z "$consul_token" ]]; then + log_error "❌ consul.token not configured (required for HTTPS address)" + ((errors++)) + else + log_info "✓ consul.token configured" + fi + else + log_warn "[!] Consul address is not using HTTPS (consider using https:// for production)" + + # Still check if token is configured for non-HTTPS (optional but recommended) + local consul_token=$(grep -A 10 "^consul:" "$config_file" | grep "token:" | sed 's/.*token: *//g' | sed 's/"//g' | sed "s/'//g" | sed 's/#.*//g' | sed 's/ *$//g') + if [[ -n "$consul_token" ]]; then + log_info "✓ consul.token configured" + else + log_warn "[!] consul.token not configured (recommended for security)" + fi + fi fi - log_info "All required credentials checked and collected for .env file" + # Validate required credentials are present + if [[ $errors -gt 0 ]]; then + log_error "❌ Configuration validation failed with $errors error(s)" + log_error "Please configure the missing values in $config_file before proceeding" + return 1 + fi + + log_info "[OK] All required credentials configured in config.yaml" + return 0 } -# Setup secrets and environment file -setup_secrets() { - log_step "Setting up secrets and environment..." +# Setup environment file (simplified) +setup_environment_file() { + log_step "Setting up environment file..." local env_file="$MPCIUM_HOME/.env" @@ -491,58 +548,27 @@ setup_secrets() { return 1 fi - # Check and update config.yaml credentials if needed - check_and_update_config_credentials + # Validate config.yaml has all required credentials + if ! validate_config_credentials; then + log_error "Configuration validation failed. Please fix configuration issues before proceeding." + return 1 + fi log_info "Creating environment file..." cat > "$env_file" << EOF # Mpcium Environment Variables # Generated on $(date) -# WARNING: This file contains sensitive information - keep secure! +# Note: All credentials are now configured in /etc/mpcium/config.yaml ENVIRONMENT=${MPCIUM_ENVIRONMENT} MPCIUM_NODE_NAME=${MPCIUM_NODE_NAME} EOF - # Only add credentials to .env if they were collected (missing from config.yaml) - if [[ "$COLLECT_NATS_PASSWORD" == "true" ]]; then - echo "NATS_PASSWORD=${NATS_PASSWORD}" >> "$env_file" - if [[ -z "$NATS_PASSWORD" ]]; then - log_info "Added NATS password (empty) to environment file" - else - log_info "Added NATS password to environment file" - fi - fi - - if [[ "$COLLECT_CONSUL_PASSWORD" == "true" ]]; then - echo "CONSUL_PASSWORD=${CONSUL_PASSWORD}" >> "$env_file" - if [[ -z "$CONSUL_PASSWORD" ]]; then - log_info "Added Consul password (empty) to environment file" - else - log_info "Added Consul password to environment file" - fi - fi - - if [[ "$COLLECT_CONSUL_TOKEN" == "true" ]]; then - echo "CONSUL_TOKEN=${CONSUL_TOKEN}" >> "$env_file" - if [[ -z "$CONSUL_TOKEN" ]]; then - log_info "Added Consul token (empty) to environment file" - else - log_info "Added Consul token to environment file" - fi - fi - # Secure the environment file - only root can read/write, service user can read chown root:mpcium "$env_file" chmod 640 "$env_file" log_info "Environment file created at: $env_file" - # Clear sensitive variables from memory - [[ "$COLLECT_NATS_PASSWORD" == "true" ]] && unset NATS_PASSWORD - [[ "$COLLECT_CONSUL_PASSWORD" == "true" ]] && unset CONSUL_PASSWORD - [[ "$COLLECT_CONSUL_TOKEN" == "true" ]] && unset CONSUL_TOKEN - unset COLLECT_NATS_PASSWORD COLLECT_CONSUL_PASSWORD COLLECT_CONSUL_TOKEN - - log_info "Secrets setup complete" + log_info "Environment file setup complete" } # Verify deployment structure @@ -613,7 +639,7 @@ verify_deployment() { else log_info " ✓ Identity file exists: ${node_name}_identity.json" fi - + `` # Check private key file only for current node # Other nodes' private keys should NOT be present for security reasons local current_node_name_from_env @@ -708,13 +734,13 @@ main() { log_info "Starting Mpcium configuration setup..." check_root - setup_environment + get_environment_from_config setup_node_name check_binaries ensure_configuration create_user install_systemd_service - setup_secrets + setup_environment_file # Run configuration verification log_step "Running configuration verification..." @@ -733,11 +759,49 @@ case "${1:-deploy}" in "deploy") main ;; - "secrets-only") - check_root - setup_environment - setup_node_name - setup_secrets + "validate-only") + validate_config_credentials + ;; + "validate-config") + log_info "Config file validation utility" + echo + + config_path="" + while true; do + echo -e "${BLUE}[PROMPT]${NC} Enter path to config.yaml file to validate:" + echo -e " ${YELLOW}Examples:${NC}" + echo -e " /etc/mpcium/config.yaml" + echo -e " ./config.yaml" + echo -e " ./config.prod.yaml.template" + read -p "> " config_path + echo + + if [[ -z "$config_path" ]]; then + log_error "Path cannot be empty. Please try again." + continue + fi + + if [[ ! -f "$config_path" ]]; then + log_error "File not found: $config_path. Please try again." + continue + fi + + break + done + + log_info "Validating config file: $config_path" + echo + + if validate_config_credentials "$config_path"; then + echo + log_info "[SUCCESS] Config validation completed successfully!" + log_info "The config file is properly configured." + else + echo + log_error "[FAIL] Config validation failed!" + log_error "Please fix the configuration issues above." + exit 1 + fi ;; "update-creds") check_root @@ -757,7 +821,8 @@ case "${1:-deploy}" in echo "" echo "Commands:" echo " deploy Full configuration setup (default)" - echo " secrets-only Update secrets only (no service start)" + echo " validate-only Validate /etc/mpcium/config.yaml credentials only" + echo " validate-config Validate any config file (prompts for path)" echo " update-creds Update service credentials only" echo " verify Verify configuration and files" echo " status Show service status" diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go index d312b90c..dbb32a5d 100644 --- a/pkg/identity/identity.go +++ b/pkg/identity/identity.go @@ -484,7 +484,7 @@ func (s *fileStore) VerifySignature(msg *types.ECDHMessage) error { // Verify the signature if !ed25519.Verify(senderPk, msgBytes, msg.Signature) { - return fmt.Errorf("invalid signature") + return fmt.Errorf("invalid signature from %s with public key %s", msg.From, hex.EncodeToString(senderPk)) } return nil diff --git a/pkg/mpc/registry.go b/pkg/mpc/registry.go index 92c2cf6d..c27b12dd 100644 --- a/pkg/mpc/registry.go +++ b/pkg/mpc/registry.go @@ -72,7 +72,7 @@ func NewRegistry( logger.Fatal("mpc_threshold must be greater than 0", nil) } - return ®istry{ + reg := ®istry{ consulKV: consulKV, nodeID: nodeID, peerNodeIDs: getPeerIDsExceptSelf(nodeID, peerNodeIDs), @@ -84,6 +84,10 @@ func NewRegistry( ecdhSession: ecdhSession, mpcThreshold: mpcThreshold, } + + go reg.consumeECDHErrors() + + return reg } func getPeerIDsExceptSelf(nodeID string, peerNodeIDs []string) []string { @@ -159,9 +163,9 @@ func (r *registry) Ready() error { } _, err = r.healthCheck.Listen(r.composeHealthCheckTopic(r.nodeID), func(data []byte) { - peerID, ecdhReadyPeersCount, _ := parseHealthDataSplit(string(data)) - logger.Debug("Health check ok", "peerID", peerID) - if ecdhReadyPeersCount < int(r.GetReadyPeersCountExcludeSelf()) { + peerID, isEcdhReady, _ := parseHealthDataSplit(string(data)) + logger.Debug("Health check ok", "peerID", peerID, "isEcdhReady", isEcdhReady) + if !isEcdhReady { logger.Info("[ECDH exchange retriggerd] not all peers are ready", "peerID", peerID) go r.triggerECDHExchange() @@ -356,20 +360,27 @@ func (r *registry) composeHealthCheckTopic(nodeID string) string { } func (r *registry) composeHealthData() string { - return fmt.Sprintf("%s,%d", r.nodeID, r.ecdhSession.GetReadyPeersCount()) + isECDHReady := r.isECDHReady() + return fmt.Sprintf("%s,%t", r.nodeID, isECDHReady) } -func parseHealthDataSplit(s string) (peerID string, readyCount int, err error) { +func parseHealthDataSplit(s string) (peerID string, ready bool, err error) { parts := strings.SplitN(s, ",", 2) if len(parts) != 2 { - return "", 0, fmt.Errorf("invalid format: %q", s) + return "", false, fmt.Errorf("invalid format: %q", s) } peerID = parts[0] - readyCount, err = strconv.Atoi(parts[1]) + ready, err = strconv.ParseBool(parts[1]) if err != nil { - return "", 0, err + return "", false, err } - return peerID, readyCount, nil + return peerID, ready, nil +} +// consumeECDHErrors consumes errors from ECDH session and logs them +func (r *registry) consumeECDHErrors() { + for err := range r.ecdhSession.ErrChan() { + logger.Error("ECDH error", err) + } } diff --git a/scripts/print-keys/main.go b/scripts/print-keys/main.go new file mode 100644 index 00000000..aeb2cdb9 --- /dev/null +++ b/scripts/print-keys/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "syscall" + + "github.com/dgraph-io/badger/v4" + "github.com/dgraph-io/badger/v4/options" + "github.com/urfave/cli/v3" + "golang.org/x/term" +) + +func main() { + app := &cli.Command{ + Name: "print-keys", + Usage: "Print all keys from a BadgerDB database", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "db-path", + Aliases: []string{"p"}, + Usage: "Path to the BadgerDB database directory", + Required: true, + }, + }, + Action: printKeys, + } + + if err := app.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} + +func printKeys(ctx context.Context, cmd *cli.Command) error { + dbPath := cmd.String("db-path") + + // Prompt for password + fmt.Print("Enter database password: ") + passwordBytes, err := term.ReadPassword(int(syscall.Stdin)) + if err != nil { + return fmt.Errorf("failed to read password: %v", err) + } + fmt.Println() // Print newline after password input + password := string(passwordBytes) + + // Configure BadgerDB options + opts := badger.DefaultOptions(dbPath). + WithCompression(options.ZSTD). + WithEncryptionKey([]byte(password)). + WithIndexCacheSize(16 << 20). + WithBlockCacheSize(32 << 20). + WithReadOnly(true) // Open in read-only mode for safety + + // Open the database + db, err := badger.Open(opts) + if err != nil { + return fmt.Errorf("failed to open BadgerDB: %v", err) + } + defer db.Close() + + fmt.Printf("Opening database at: %s\n", dbPath) + fmt.Println("=== All Keys in Database ===") + + // Iterate through all keys + err = db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false // We only need keys, not values + it := txn.NewIterator(opts) + defer it.Close() + + count := 0 + for it.Rewind(); it.Valid(); it.Next() { + item := it.Item() + key := string(item.Key()) + count++ + fmt.Printf("%d. %s\n", count, key) + } + + if count == 0 { + fmt.Println("No keys found in the database.") + } else { + fmt.Printf("\nTotal keys: %d\n", count) + } + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to iterate over database: %v", err) + } + + return nil +}