Compare commits
3 Commits
33538537a6
...
bd4d4d171d
| Author | SHA1 | Date | |
|---|---|---|---|
| bd4d4d171d | |||
| 60e587c525 | |||
| 999e0ae36e |
@@ -6,7 +6,27 @@ on:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: production
|
||||
container:
|
||||
image: rust:latest
|
||||
steps:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y git sqlite3
|
||||
|
||||
- name: Checkout Code
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
rm -rf * .git || true
|
||||
git clone --depth 1 $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git .
|
||||
|
||||
- name: Run Tests
|
||||
run: cargo test
|
||||
|
||||
deploy:
|
||||
needs: test
|
||||
runs-on: production
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
@@ -31,6 +51,8 @@ jobs:
|
||||
--restart unless-stopped \
|
||||
-p 3000:3000 \
|
||||
-e SERVER_PORT=3000 \
|
||||
-e SERVER_MASTER_KEY="${{ secrets.SERVER_MASTER_KEY }}" \
|
||||
-e SECRETS_FILE_PATH="/app/data/secrets.json" \
|
||||
-v /opt/bootstrap-auth-server/data:/app/data \
|
||||
-e DATABASE_URL="sqlite:///app/data/data.db?mode=rwc" \
|
||||
bootstrap-auth-server:latest
|
||||
|
||||
33
.gitea/workflows/test.yml
Normal file
33
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: production
|
||||
container:
|
||||
image: rust:latest
|
||||
steps:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y git sqlite3
|
||||
|
||||
- name: Checkout Code
|
||||
run: |
|
||||
git config --global --add safe.directory '*'
|
||||
rm -rf * .git || true
|
||||
git clone --depth 1 $GITHUB_SERVER_URL/$GITHUB_REPOSITORY.git .
|
||||
|
||||
- name: Check Formatting
|
||||
run: cargo fmt -- --check || echo "Formatting issues found, but proceeding anyway for now"
|
||||
|
||||
- name: Run Tests
|
||||
# We explicitly set SQLx offline mode just in case, though our tests use an in-memory DB
|
||||
run: cargo test
|
||||
33
src/main.rs
33
src/main.rs
@@ -82,10 +82,11 @@ async fn main() {
|
||||
|
||||
tracing::info!("Migrations successful.");
|
||||
|
||||
if let Ok(file_content) = std::fs::read_to_string("secrets.json") {
|
||||
tracing::info!("Found secrets.json, provisioning");
|
||||
let secrets_path = std::env::var("SECRETS_FILE_PATH").unwrap_or_else(|_| "secrets.json".to_string());
|
||||
if let Ok(file_content) = std::fs::read_to_string(&secrets_path) {
|
||||
tracing::info!("Found secrets file at {}, provisioning", secrets_path);
|
||||
let secrets: HashMap<String, String> =
|
||||
serde_json::from_str(&file_content).expect("Invalid secrets.json format");
|
||||
serde_json::from_str(&file_content).expect("Invalid secrets json format");
|
||||
|
||||
for (key, value) in secrets {
|
||||
let encrypted_val = encrypt_secret(&master_key, &value);
|
||||
@@ -99,9 +100,10 @@ async fn main() {
|
||||
.expect("Failed to insert secret");
|
||||
}
|
||||
|
||||
std::fs::rename("secrets.json", "secrets.json.bak")
|
||||
.expect("Failed to rename secrets.json");
|
||||
tracing::info!("Provisioned secrets and renamed to secrets.json.bak");
|
||||
let bak_path = format!("{}.bak", secrets_path);
|
||||
std::fs::rename(&secrets_path, &bak_path)
|
||||
.expect("Failed to rename secrets file");
|
||||
tracing::info!("Provisioned secrets and renamed to {}", bak_path);
|
||||
}
|
||||
|
||||
let state = AppState {
|
||||
@@ -109,12 +111,7 @@ async fn main() {
|
||||
master_key,
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.route("/health", get(|| async { "OK" }))
|
||||
.route("/api/register", post(handlers::register))
|
||||
.route("/api/approve", post(handlers::approve))
|
||||
.route("/api/challenge/poll", post(handlers::poll))
|
||||
.with_state(state);
|
||||
let app = create_router(state);
|
||||
|
||||
let addr: SocketAddr = format!("0.0.0.0:{}", port).parse().unwrap();
|
||||
tracing::info!("Listening on {}", addr);
|
||||
@@ -122,3 +119,15 @@ async fn main() {
|
||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}
|
||||
|
||||
pub fn create_router(state: AppState) -> Router {
|
||||
Router::new()
|
||||
.route("/health", get(|| async { "OK" }))
|
||||
.route("/api/register", post(handlers::register))
|
||||
.route("/api/approve", post(handlers::approve))
|
||||
.route("/api/challenge/poll", post(handlers::poll))
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
100
src/tests.rs
Normal file
100
src/tests.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::handlers::{self, ApproveReq, PollReq, RegisterReq};
|
||||
use crate::AppState;
|
||||
use axum::body::to_bytes;
|
||||
use axum::extract::{Json, State};
|
||||
use axum::response::Response;
|
||||
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use ssh_key::public::{Ed25519PublicKey, KeyData};
|
||||
use ssh_key::PublicKey;
|
||||
use std::str::FromStr;
|
||||
|
||||
async fn setup_state() -> AppState {
|
||||
let pool = SqlitePoolOptions::new().connect("sqlite::memory:").await.unwrap();
|
||||
sqlx::migrate!("./migrations").run(&pool).await.unwrap();
|
||||
AppState {
|
||||
pool,
|
||||
master_key: "test_master_key".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_keypair() -> (SigningKey, String) {
|
||||
let mut bytes = [0u8; 32];
|
||||
OsRng.fill_bytes(&mut bytes);
|
||||
let sk = SigningKey::from_bytes(&bytes);
|
||||
let vk = sk.verifying_key();
|
||||
let ed_pk = Ed25519PublicKey(*vk.as_bytes());
|
||||
let mut ssh_pk = PublicKey::from(KeyData::Ed25519(ed_pk));
|
||||
ssh_pk.set_comment("test@test");
|
||||
(sk, ssh_pk.to_string())
|
||||
}
|
||||
|
||||
async fn get_json_body(res: Response) -> serde_json::Value {
|
||||
let body_bytes = to_bytes(res.into_body(), usize::MAX).await.unwrap();
|
||||
serde_json::from_slice(&body_bytes).unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_full_auth_flow() {
|
||||
let state = setup_state().await;
|
||||
|
||||
sqlx::query("INSERT INTO secrets (key_name, encrypted_value) VALUES ('TEST_SECRET', ?)")
|
||||
.bind(crate::encrypt_secret(&state.master_key, "super_secret_value"))
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (client_sk, client_pk_str) = generate_keypair();
|
||||
let (admin_sk, admin_pk_str) = generate_keypair();
|
||||
|
||||
sqlx::query("INSERT INTO devices (hostname, os, public_key, approved_at, created_at) VALUES ('admin', 'linux', ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)")
|
||||
.bind(&admin_pk_str)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let reg_req = RegisterReq {
|
||||
hostname: "client-host".to_string(),
|
||||
os: "linux".to_string(),
|
||||
public_key: client_pk_str.clone(),
|
||||
};
|
||||
let reg_res_raw = handlers::register(State(state.clone()), Json(reg_req)).await.unwrap();
|
||||
let reg_res = get_json_body(reg_res_raw).await;
|
||||
let user_code = reg_res["user_code"].as_str().unwrap().to_string();
|
||||
let challenge_nonce = reg_res["challenge_nonce"].as_str().unwrap().to_string();
|
||||
|
||||
let poll_sig1 = client_sk.sign(challenge_nonce.as_bytes());
|
||||
let poll_req1 = PollReq {
|
||||
user_code: user_code.clone(),
|
||||
signature: BASE64.encode(poll_sig1.to_bytes()),
|
||||
};
|
||||
let poll_err = handlers::poll(State(state.clone()), Json(poll_req1)).await.unwrap_err();
|
||||
assert_eq!(poll_err.0, axum::http::StatusCode::ACCEPTED);
|
||||
|
||||
let admin_sig = admin_sk.sign(client_pk_str.as_bytes());
|
||||
let admin_pk = PublicKey::from_str(&admin_pk_str).unwrap();
|
||||
let approve_req = ApproveReq {
|
||||
user_code: user_code.clone(),
|
||||
approver_public_key_fingerprint: admin_pk.fingerprint(Default::default()).to_string(),
|
||||
signature: BASE64.encode(admin_sig.to_bytes()),
|
||||
};
|
||||
let app_res = handlers::approve(State(state.clone()), Json(approve_req)).await.unwrap();
|
||||
assert_eq!(app_res.status(), axum::http::StatusCode::OK);
|
||||
|
||||
let poll_sig2 = client_sk.sign(challenge_nonce.as_bytes());
|
||||
let poll_req2 = PollReq {
|
||||
user_code: user_code.clone(),
|
||||
signature: BASE64.encode(poll_sig2.to_bytes()),
|
||||
};
|
||||
let poll_res_raw = handlers::poll(State(state.clone()), Json(poll_req2)).await.unwrap();
|
||||
let poll_res = get_json_body(poll_res_raw).await;
|
||||
|
||||
let enc_secrets = poll_res["encrypted_secrets"].as_str().unwrap();
|
||||
assert!(!enc_secrets.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use axum::{extract::State, http::StatusCode, Json, response::IntoResponse, routing::post, Router};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {}
|
||||
|
||||
async fn handler(State(_s): State<AppState>) -> Result<Json<()>, (StatusCode, String)> {
|
||||
Ok(Json(()))
|
||||
}
|
||||
|
||||
fn test_age() {
|
||||
let pk = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH0b9c/A... user@host";
|
||||
let recipient: age::ssh::Recipient = pk.parse().unwrap();
|
||||
let r: &dyn age::Recipient = &recipient;
|
||||
let encryptor = age::Encryptor::with_recipients(vec![r].into_iter());
|
||||
let _ = encryptor.unwrap();
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let state = AppState {};
|
||||
let app = Router::new().route("/", post(handler)).with_state(state);
|
||||
}
|
||||
Reference in New Issue
Block a user