From ed56ef95a9d83ecfbb6a98de17f2f4d75e53e146 Mon Sep 17 00:00:00 2001 From: Aditya Gupta Date: Sat, 27 Jun 2026 09:04:10 +0530 Subject: [PATCH] feat: Implemented client spec for bootstrap-auth-server with b me and b trust --- b.sh | 2 +- bootstrap.sh | 6 ++ lib/routes.sh | 8 ++ plugins.json | 6 ++ plugins/auth.sh | 246 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 plugins/auth.sh diff --git a/b.sh b/b.sh index d380330..08657b1 100755 --- a/b.sh +++ b/b.sh @@ -86,7 +86,7 @@ _b_completion() { # If completing the first argument after 'b' if [ "$COMP_CWORD" -eq 1 ]; then - opts="all con gone up ware bware" + opts="all con gone up ware bware me trust" local routes_dir="$HOME/.config/bootstrap" local installer_keys="" diff --git a/bootstrap.sh b/bootstrap.sh index 64a1cbc..dfb2b1c 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -105,6 +105,12 @@ install_bootstrap() { mkdir -p "$routes_dir/installers" cp -r "$_SCRIPT_DIR/installers/"* "$routes_dir/installers/" fi + + # Also copy plugins if they exist locally + if [ -d "$_SCRIPT_DIR/plugins" ]; then + mkdir -p "$routes_dir/plugins" + cp -r "$_SCRIPT_DIR/plugins/"* "$routes_dir/plugins/" + fi else log_info "Downloading bootstrap scripts..." local base_url="https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master" diff --git a/lib/routes.sh b/lib/routes.sh index d41cf80..98d6297 100755 --- a/lib/routes.sh +++ b/lib/routes.sh @@ -216,6 +216,14 @@ for script in "${SCRIPTS[@]}"; do exit 1 fi ;; + me) + run_plugin "auth" "me" "$@" + exit $? + ;; + trust) + run_plugin "auth" "trust" "$@" + exit $? + ;; con) if [ -f "$BOOTSTRAP_DIR/commands/con.sh" ]; then . "$BOOTSTRAP_DIR/commands/con.sh" "$@" diff --git a/plugins.json b/plugins.json index fbb73d7..4bc7848 100644 --- a/plugins.json +++ b/plugins.json @@ -1,5 +1,11 @@ { "plugins": { + "auth": { + "version": "1.0.0", + "url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/auth.sh", + "bootstrap": "2.2.0", + "description": "Client Authentication and Provisioning Plugin" + }, "weather": { "version": "1.0.0", "url": "https://git.adityagupta.dev/sortedcord/bootstrap/raw/branch/master/plugins/weather.sh", diff --git a/plugins/auth.sh b/plugins/auth.sh new file mode 100644 index 0000000..349562e --- /dev/null +++ b/plugins/auth.sh @@ -0,0 +1,246 @@ +#!/usr/bin/env bash +# +# Authentication & Provisioning Plugin for Bootstrap CLI +# Handles requester (b me) and approver (b trust) flows. +# + +set -euo pipefail + +# Ensure dependencies are met +for dep in ssh-keygen curl jq age; do + if ! has_command "$dep"; then + log_error "Dependency '$dep' is missing. Please install it to use authentication." + exit 1 + fi +done + +# Ensure public key exists next to private key for ssh-keygen -Y sign +ensure_pubkey_exists() { + local priv_key="$1" + local pub_key="${priv_key}.pub" + if [ ! -f "$pub_key" ]; then + ssh-keygen -y -f "$priv_key" > "$pub_key" + fi +} + +COMMAND="${1:-}" +if [ -z "$COMMAND" ]; then + echo "Usage: b auth [args...]" >&2 + exit 1 +fi +shift + +# Defaults +SERVER_URL="https://b.adityagupta.dev/auth" +KEY_DIR="$HOME/.config/bootstrap-client" +POLL_INTERVAL=5 +ADMIN_KEY="$HOME/.ssh/id_ed25519" +USER_CODE="" + +if [ "$COMMAND" = "trust" ]; then + if [ $# -lt 1 ]; then + log_error "user_code is required for trust." + echo "Usage: b trust [--server ] [--admin-key ]" >&2 + exit 1 + fi + USER_CODE="$1" + shift +fi + +# Parse remaining arguments +while [ $# -gt 0 ]; do + case "$1" in + --server) + SERVER_URL="$2" + shift 2 + ;; + --key-dir) + KEY_DIR="$2" + shift 2 + ;; + --poll-interval) + POLL_INTERVAL="$2" + shift 2 + ;; + --admin-key) + ADMIN_KEY="$2" + shift 2 + ;; + *) + log_error "Unknown argument: $1" + exit 1 + ;; + esac +done + +if [ "$COMMAND" = "me" ]; then + mkdir -p "$KEY_DIR" + local_key="$KEY_DIR/id_ed25519" + + if [ ! -f "$local_key" ]; then + log_info "Generating local Ed25519 key pair under $KEY_DIR..." + ssh-keygen -t ed25519 -N "" -f "$local_key" >/dev/null + fi + + ensure_pubkey_exists "$local_key" + pub_key=$(cat "${local_key}.pub") + hostname=$(hostname 2>/dev/null || uname -n) + os=$(uname -s 2>/dev/null || echo "linux") + + # Safely construct JSON payload + json_payload=$(jq -n \ + --arg hn "$hostname" \ + --arg os "$os" \ + --arg pk "$pub_key" \ + '{hostname: $hn, os: $os, public_key: $pk}') + + log_info "Registering device with $SERVER_URL..." + + register_response=$(curl -fsSL -X POST \ + -H "Content-Type: application/json" \ + -d "$json_payload" \ + "$SERVER_URL/api/register") + + user_code=$(echo "$register_response" | jq -r '.user_code // empty') + challenge_nonce=$(echo "$register_response" | jq -r '.challenge_nonce // empty') + + if [ -z "$user_code" ] || [ -z "$challenge_nonce" ]; then + log_error "Failed to retrieve registration codes from server response." + exit 1 + fi + + echo "--------------------------------------------------------" + log_success "Device registration initiated successfully!" + echo "Please authorize this device on your administrator machine using:" + echo " b trust $user_code --server $SERVER_URL" + echo "--------------------------------------------------------" + echo "Verification Code: $user_code" + echo "--------------------------------------------------------" + log_info "Waiting for administrator approval (polling every ${POLL_INTERVAL}s)..." + + # Prepare challenge poll file signing + temp_nonce_file=$(mktemp) + temp_sig_file="${temp_nonce_file}.sig" + echo -n "$challenge_nonce" > "$temp_nonce_file" + + # Ensure cleanup of temp files + cleanup() { + rm -f "$temp_nonce_file" "$temp_sig_file" + } + trap cleanup EXIT INT TERM + + while true; do + rm -f "$temp_sig_file" + + # Sign challenge nonce + if ! ssh-keygen -Y sign -f "$local_key" -n "bootstrap" "$temp_nonce_file" >/dev/null 2>&1; then + log_error "Cryptographic signing of challenge nonce failed." + exit 1 + fi + + # Get raw base64 from armored signature file + signature_b64=$(grep -v '^-' "$temp_sig_file" | tr -d '\n') + + poll_payload=$(jq -n \ + --arg uc "$user_code" \ + --arg sig "$signature_b64" \ + '{user_code: $uc, signature: $sig}') + + poll_out=$(mktemp) + http_code=$(curl -s -o "$poll_out" -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -d "$poll_payload" \ + "$SERVER_URL/api/challenge/poll") + + poll_body=$(cat "$poll_out") + rm -f "$poll_out" + + if [ "$http_code" = "200" ]; then + enc_secrets=$(echo "$poll_body" | jq -r '.encrypted_secrets // empty') + if [ -n "$enc_secrets" ] && [ "$enc_secrets" != "null" ]; then + log_success "Device approved by administrator! Decrypting secrets payload..." + + decrypted_file="$KEY_DIR/secrets.decrypted" + if echo "$enc_secrets" | base64 -d | age --decrypt -i "$local_key" > "$decrypted_file" 2>/dev/null; then + log_success "Secrets successfully provisioned and written to: $decrypted_file" + cat "$decrypted_file" + break + else + log_error "Decryption using age failed. Please ensure the private key has not been altered." + exit 1 + fi + fi + fi + + sleep "$POLL_INTERVAL" + done + +elif [ "$COMMAND" = "trust" ]; then + if [ ! -f "$ADMIN_KEY" ]; then + log_error "Admin private key not found at: $ADMIN_KEY" + exit 1 + fi + + ensure_pubkey_exists "$ADMIN_KEY" + + log_info "Fetching pending device details for user code: $USER_CODE" + pending_response=$(curl -fsSL "$SERVER_URL/api/pending/$USER_CODE") + + requester_pub_key=$(echo "$pending_response" | jq -r '.public_key // empty') + if [ -z "$requester_pub_key" ]; then + log_error "No pending registration found for code '$USER_CODE'." + exit 1 + fi + + echo "--------------------------------------------------------" + echo "Pending Device Public Key:" + echo "$requester_pub_key" + echo "--------------------------------------------------------" + + # Prompt for confirmation (read from tty to support pipeline scenarios) + read -r -p "Do you trust and approve this device? [y/N]: " confirm_choice "$temp_pubkey_file" + + # Cleanup trap + cleanup_trust() { + rm -f "$temp_pubkey_file" "$temp_pubkey_sig_file" + } + trap cleanup_trust EXIT INT TERM + + if ! ssh-keygen -Y sign -f "$ADMIN_KEY" -n "bootstrap" "$temp_pubkey_file" >/dev/null 2>&1; then + log_error "Cryptographic signing using administrator key failed." + exit 1 + fi + + signature_b64=$(grep -v '^-' "$temp_pubkey_sig_file" | tr -d '\n') + + # Get fingerprint + admin_pubkey_str=$(ssh-keygen -y -f "$ADMIN_KEY") + temp_admin_pub=$(mktemp) + echo "$admin_pubkey_str" > "$temp_admin_pub" + approver_fingerprint=$(ssh-keygen -lf "$temp_admin_pub" | awk '{print $2}') + rm -f "$temp_admin_pub" + + # Prepare payload + approve_payload=$(jq -n \ + --arg uc "$USER_CODE" \ + --arg fp "$approver_fingerprint" \ + --arg sig "$signature_b64" \ + '{user_code: $uc, approver_public_key_fingerprint: $fp, signature: $sig}') + + log_info "Submitting cryptographic approval to server..." + curl -fsSL -X POST \ + -H "Content-Type: application/json" \ + -d "$approve_payload" \ + "$SERVER_URL/api/approve" + + log_success "Device with code $USER_CODE has been approved." +fi