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};
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();
assert_eq!(ed25519_public_blob(&sk), pub_blob);
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();
let _ = canonical_task_bytes(&t);
}
}