use anyhow::{Context, bail};
use base64::Engine;
use base64::engine::general_purpose::STANDARD;
use liminal_server::config::types::MachineConfig;
use lys_core::ca::create_certificate_request;
use lys_core::keys::Ed25519Identity;
use std::io::{Read, Write};
use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use std::sync::Arc;
use zeroize::Zeroizing;
fn read_seed(path: &Path) -> anyhow::Result<Zeroizing<[u8; 32]>> {
let metadata = std::fs::symlink_metadata(path).context("cannot inspect agent seed")?;
if !metadata.is_file() {
bail!("agent seed must be a regular file");
}
if metadata.permissions().mode() & 0o777 != 0o600 {
bail!("agent seed permissions must be 0600");
}
if metadata.len() > 66 {
bail!("agent seed must be 64 hexadecimal digits");
}
let mut digits = Zeroizing::new(String::new());
std::fs::File::open(path)
.context("cannot open agent seed")?
.take(66)
.read_to_string(&mut digits)
.context("cannot read agent seed")?;
let mut seed = Zeroizing::new([0; 32]);
hex::decode_to_slice(digits.trim(), &mut *seed)
.map_err(|_| anyhow::anyhow!("agent seed must be 64 hexadecimal digits"))?;
Ok(seed)
}
fn seed(path: &Path) -> anyhow::Result<Zeroizing<[u8; 32]>> {
match std::fs::symlink_metadata(path) {
Ok(_) => return read_seed(path),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => return Err(error).context("cannot inspect agent seed"),
}
let parent = path
.parent()
.filter(|parent| !parent.as_os_str().is_empty())
.unwrap_or_else(|| Path::new("."));
let mut random = Zeroizing::new([0; 32]);
std::fs::File::open("/dev/urandom")
.and_then(|mut file| file.read_exact(&mut *random))
.context("cannot generate agent seed")?;
let mut pending = tempfile::Builder::new()
.prefix(".liminal-agent-")
.tempfile_in(parent)
.context("cannot create agent seed")?;
let digits = Zeroizing::new(hex::encode(random.as_slice()));
pending
.write_all(digits.as_bytes())
.context("cannot write agent seed")?;
pending
.as_file()
.sync_all()
.context("cannot flush agent seed")?;
match pending.persist_noclobber(path) {
Ok(file) => {
drop(file);
std::fs::File::open(parent)
.and_then(|directory| directory.sync_all())
.context("cannot flush agent seed directory")?;
Ok(random)
}
Err(error) if error.error.kind() == std::io::ErrorKind::AlreadyExists => read_seed(path),
Err(error) => Err(error.error).context("cannot publish agent seed"),
}
}
pub fn run(path: &Path) -> anyhow::Result<()> {
let text = std::fs::read_to_string(path).context("cannot read server config")?;
let config: toml::Value = toml::from_str(&text).context("cannot parse server config")?;
let section = config
.get("auth")
.and_then(|auth| auth.get("lys"))
.and_then(|lys| lys.get("machine"))
.context("auth.lys.machine is required")?;
let machine: MachineConfig = section
.clone()
.try_into()
.context("cannot read agent configuration")?;
if machine.agent_id.is_empty()
|| !machine
.agent_id
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
{
bail!("auth.lys.machine.agent_id must be one nonempty route segment");
}
if machine.identity_path.as_os_str().is_empty() {
bail!("auth.lys.machine.identity_path is required");
}
let key = Arc::new(Ed25519Identity::from_seed(&seed(&machine.identity_path)?));
let csr = create_certificate_request(&key, &machine.agent_id)
.context("cannot sign agent certificate request")?;
let encoded = STANDARD.encode(csr);
let stdout = std::io::stdout();
let mut output = stdout.lock();
writeln!(output, "agent_id: {}", machine.agent_id)?;
writeln!(
output,
"public_key: {}",
hex::encode(key.public_key_bytes())
)?;
writeln!(output, "-----BEGIN CERTIFICATE REQUEST-----")?;
for line in encoded.as_bytes().chunks(64) {
writeln!(output, "{}", std::str::from_utf8(line)?)?;
}
writeln!(output, "-----END CERTIFICATE REQUEST-----")?;
output.flush().context("cannot write agent identity")
}