feat: Added initial scaffold, db schema and entrypoint
This commit is contained in:
3109
Cargo.lock
generated
Normal file
3109
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Cargo.toml
17
Cargo.toml
@@ -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
107
README.md
Normal 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"
|
||||
}
|
||||
```
|
||||
24
migrations/20260625000000_initial_schema.sql
Normal file
24
migrations/20260625000000_initial_schema.sql
Normal 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
|
||||
);
|
||||
46
src/main.rs
46
src/main.rs
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user