feat: Added initial scaffold, db schema and entrypoint

This commit is contained in:
2026-06-25 10:15:12 +05:30
parent b9a7bb8319
commit a55fd2e1bd
6 changed files with 3300 additions and 3 deletions

3109
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,21 @@
[package]
name = "bootstrap-auth-server"
version = "0.1.0"
edition = "2026"
edition = "2021"
[dependencies]
aes-gcm = "0.10.3"
age = { version = "0.11.3", features = ["ssh"] }
axum = "0.8.9"
base64 = "0.22.1"
dotenvy = "0.15.7"
ed25519-dalek = "2.2.0"
rand = "0.10.1"
serde = "1.0.228"
serde_json = "1.0.150"
sha2 = "0.11.0"
sqlx = { version = "0.9.0", features = ["runtime-tokio", "sqlite", "tls-rustls"] }
ssh-key = { version = "0.6.7", features = ["ed25519"] }
tokio = { version = "1.52.3", features = ["full"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }

107
README.md Normal file
View File

@@ -0,0 +1,107 @@
# Architecture: Asymmetric Cryptography (RSA) Authentication & E2E Secrets Sync
This architecture replaces static tokens with an asymmetric key pair (RSA or Ed25519) generated on each client device. This provides challenge-response authentication and end-to-end encryption for synchronized secrets.
---
## 1. Sequence Diagram: Device Registration & Challenge-Response Auth
```mermaid
sequenceDiagram
autonumber
actor User as User (Aditya)
participant DevB as Device B (New Machine)
participant Server as Auth Server (Node.js/Go)
participant DevA as Device A (Trusted Machine)
Note over DevB: 1. Generate RSA key pair locally if missing<br/>(~/.config/bootstrap/id_rsa)
User->>DevB: Runs `b me`
DevB->>Server: POST /api/register (sends hostname, os, DevB_pubkey)
Server-->>DevB: Returns user_code (ABCD) & challenge_nonce
DevB->>User: Prints: "Run 'b me approve ABCD' on Device A"
DevB->>Server: Starts polling POST /api/challenge/poll (with user_code)
User->>DevA: Runs `b me approve ABCD`
DevA->>Server: GET /api/pending/ABCD (Fetches DevB_pubkey)
Server-->>DevA: Returns DevB_pubkey
Note over DevA: DevA signs DevB_pubkey with its own private key:<br/>Signature = Sign(DevA_privkey, DevB_pubkey)
DevA->>Server: POST /api/approve (sends user_code, DevA_pubkey, Signature)
Note over Server: Server verifies Signature using stored DevA_pubkey.<br/>If valid, adds DevB_pubkey to authorized_keys.
Server-->>DevA: Returns "Approved!"
DevA->>User: Prints: "Approved ABCD"
Note over DevB,Server: Next polling interval
DevB->>Server: POST /api/challenge/poll (with user_code)
Note over Server: Server sees DevB is approved.<br/>Encrypts secrets payload using DevB_pubkey:<br/>EncryptedSecrets = Encrypt(DevB_pubkey, secrets)
Server-->>DevB: Returns EncryptedSecrets
Note over DevB: Decrypts secrets using its private key:<br/>secrets = Decrypt(DevB_privkey, EncryptedSecrets)
DevB->>DevB: Saves secrets locally (0600 permissions)
DevB->>User: Prints: "Successfully authenticated!"
```
---
## 2. Key Cryptographic Concepts
1. **Identity & Authentication (Challenge-Response):**
Instead of storing a static bearer token in `~/.config/bootstrap/.auth_token`, the client proves its identity by solving a cryptographic challenge.
- The server sends a random string (nonce).
- The client signs the nonce with its private key and returns the signature.
- The server verifies the signature using the client's registered public key.
2. **End-to-End Encryption (E2E):**
Secrets are stored in plaintext on the server (or encrypted at rest) but are encrypted using the client's public key before transmission. Only the client holding the corresponding private key can decrypt and read them.
3. **Offline Fallback (What if Device A is offline?):**
- **Option A (Master Key Signatures):** You keep a "Master Key Pair" offline. You can sign Device B's public key offline and send the signature to the server to authorize it.
- **Option B (Server SSH Access):** Since the server's authorized public keys are stored in a simple configuration file (e.g. `authorized_keys.json` or text file), you can SSH into the server and append Device B's public key directly.
---
## 3. API Endpoints
### A. Register New Device
* **Endpoint:** `POST /api/register`
* **Request:**
```json
{
"hostname": "my-new-laptop",
"os": "linux",
"public_key": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA..."
}
```
* **Response:**
```json
{
"user_code": "ABCD",
"expires_in": 300
}
```
### B. Approve Pending Device
* **Endpoint:** `POST /api/approve`
* **Request:**
```json
{
"user_code": "ABCD",
"approver_public_key_fingerprint": "SHA256:...",
"signature": "base64_encoded_signature_of_new_device_public_key"
}
```
### C. Challenge Poll
* **Endpoint:** `POST /api/challenge/poll`
* **Request:**
```json
{
"user_code": "ABCD",
"signature": "base64_encoded_signature_of_challenge_nonce"
}
```
* **Response (when approved):**
```json
{
"encrypted_secrets": "base64_encrypted_payload"
}
```

BIN
data.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,24 @@
-- Initial Schema for Bootstrap Auth Server
CREATE TABLE devices (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hostname TEXT NOT NULL,
os TEXT NOT NULL,
public_key TEXT NOT NULL UNIQUE,
approved_at DATETIME,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE pending_requests (
user_code TEXT PRIMARY KEY,
device_id INTEGER NOT NULL,
challenge_nonce TEXT NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY(device_id) REFERENCES devices(id) ON DELETE CASCADE
);
CREATE TABLE secrets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_name TEXT NOT NULL UNIQUE,
encrypted_value TEXT NOT NULL
);

View File

@@ -1,3 +1,45 @@
fn main() {
println!("Hello, world!");
use axum::{Router, routing::get};
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool};
use std::net::SocketAddr;
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() {
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive("bootstrap_auth_server=debug".parse().unwrap()),
)
.init();
dotenvy::dotenv().ok();
let db_url = std::env::var("DATABASE_URL").unwrap_or_else(|_| "sqlite://data.db?mode=rwc".to_string());
tracing::info!("Connecting to database at {}", db_url);
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(&db_url)
.await
.expect("Failed to connect to SQLite database");
tracing::info!("Running database migrations...");
sqlx::migrate!("./migrations")
.run(&pool)
.await
.expect("Failed to run database migrations");
tracing::info!("Migrations successful.");
let app = Router::new()
.route("/health", get(|| async { "OK" }))
.with_state(pool);
let port = std::env::var("SERVER_PORT").unwrap_or_else(|_| "3000".to_string());
let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
tracing::info!("Listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}