dragoon-server 0.1.0

Public-relay server for the dragoon remote-executor: axum + rusqlite + ed25519 task signing + per-user message inbox.
Documentation
//! Server task-signing keypair persistence (table `server_signing_key`).
//!
//! Generated on first call, survives restarts; the public part is delivered
//! to every worker during `init` and used to verify each task signature
//! the server hands out.

use anyhow::Result;
use chrono::Utc;
use ed25519_dalek::{pkcs8::{DecodePrivateKey, EncodePrivateKey}, SigningKey};
use rand::{rngs::OsRng, RngCore};
use rusqlite::{Connection, OptionalExtension};

use dragoon_proto::{pubkey::fingerprint_pubkey_blob, task_sig::ed25519_public_blob};

/// Ensure the server keypair exists. Returns its `SHA256:...` fingerprint.
pub fn ensure(conn: &Connection) -> Result<String> {
    if let Some(fp) = read_fingerprint(conn)? {
        return Ok(fp);
    }
    let mut seed = [0u8; 32];
    OsRng.fill_bytes(&mut seed);
    let sk = SigningKey::from_bytes(&seed);
    let pem = sk
        .to_pkcs8_pem(ed25519_dalek::pkcs8::spki::der::pem::LineEnding::LF)
        .expect("ed25519 PKCS#8 PEM encoding is infallible");
    let pem_bytes = pem.as_bytes();
    let pub_blob = ed25519_public_blob(&sk);
    let fp = fingerprint_pubkey_blob(&pub_blob);
    let now = Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Micros, true);
    conn.execute(
        "INSERT INTO server_signing_key (id, priv_pem, pub_blob, fingerprint, created_at)
         VALUES (1, ?, ?, ?, ?)",
        rusqlite::params![pem_bytes, &pub_blob, &fp, now],
    )?;
    Ok(fp)
}

fn read_fingerprint(conn: &Connection) -> Result<Option<String>> {
    let row = conn
        .query_row(
            "SELECT fingerprint FROM server_signing_key WHERE id=1",
            [],
            |r| r.get::<_, String>(0),
        )
        .optional()?;
    Ok(row)
}

pub fn get_private(conn: &Connection) -> Result<SigningKey> {
    let pem: Vec<u8> = conn.query_row(
        "SELECT priv_pem FROM server_signing_key WHERE id=1",
        [],
        |r| r.get(0),
    )?;
    let pem_str = std::str::from_utf8(&pem)?;
    let sk = SigningKey::from_pkcs8_pem(pem_str)
        .map_err(|e| anyhow::anyhow!("decode pkcs8 pem: {e}"))?;
    Ok(sk)
}

pub fn get_public_blob(conn: &Connection) -> Result<Vec<u8>> {
    let blob: Vec<u8> = conn.query_row(
        "SELECT pub_blob FROM server_signing_key WHERE id=1",
        [],
        |r| r.get(0),
    )?;
    Ok(blob)
}

pub fn fingerprint(conn: &Connection) -> Result<String> {
    Ok(conn.query_row(
        "SELECT fingerprint FROM server_signing_key WHERE id=1",
        [],
        |r| r.get(0),
    )?)
}

#[cfg(test)]
mod tests {
    use super::*;
    use dragoon_proto::task_sig::{canonical_task_bytes, sign_task, verify_task};
    use dragoon_proto::models::{Task, TaskKind, TaskLimits, TaskState};

    fn fresh() -> Connection {
        let c = crate::db::connect_in_memory().unwrap();
        crate::db::bootstrap(&c).unwrap();
        c
    }

    #[test]
    fn ensure_creates_then_idempotent() {
        let c = fresh();
        let a = ensure(&c).unwrap();
        let b = ensure(&c).unwrap();
        assert_eq!(a, b);
        assert!(a.starts_with("SHA256:"));
    }

    #[test]
    fn private_and_public_round_trip() {
        let c = fresh();
        ensure(&c).unwrap();
        let sk = get_private(&c).unwrap();
        let pub_blob = get_public_blob(&c).unwrap();
        // The persisted public must match the private's derivation.
        assert_eq!(ed25519_public_blob(&sk), pub_blob);

        // sign / verify round-trip via the persisted keypair
        let t = Task {
            task_id: "t".into(),
            worker_name: "w".into(),
            submitter: "u".into(),
            kind: TaskKind::Command,
            payload: "echo".into(),
            collect: vec![],
            limits: TaskLimits::default(),
            state: TaskState::Queued,
            submitted_at: chrono::Utc::now(),
            started_at: None,
            finished_at: None,
            exit_code: None,
            final_pwd: None,
            artifacts: vec![],
            error: None,
            fetch_path: None,
            worker_seq: 1,
        };
        let (sig_b64, fp) = sign_task(&sk, &t);
        assert_eq!(fp, fingerprint(&c).unwrap());
        verify_task(&t, &sig_b64, &pub_blob).unwrap();

        // also double-check canonical bytes recomputation matches what was signed
        let _ = canonical_task_bytes(&t);
    }
}