use anyhow::Context as _;
use base64::Engine as _;
use knot_runtime::{OsEntropy, PublicKeyBytes};
use knot_secrets::{MasterKey, SealedStore};
use knot_types::KnotServiceUrl;
use crate::config::ValidatedConfig;
pub const STATIC_VERSION: &str = "v1.15.0-static.1";
/// the running binary by path, size and mtime: a deploy or rebuild changes it, hashing it would cost a read
pub fn built_by() -> &'static str {
static BUILT_BY: std::sync::OnceLock<String> = std::sync::OnceLock::new();
BUILT_BY.get_or_init(|| {
let identify = || -> std::io::Result<String> {
let path = std::fs::canonicalize(std::env::current_exe()?)?;
let meta = std::fs::metadata(&path)?;
let mtime = meta
.modified()?
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
Ok(format!(
"{}@{}+{}",
path.display(),
mtime.as_nanos(),
meta.len()
))
};
identify().unwrap_or_default()
})
}
pub fn health_body() -> serde_json::Value {
serde_json::json!({ "version": format!("knot-static {STATIC_VERSION}") })
}
pub use crate::xrpc::org_tangled_knot_describe_knot::describe_body;
pub(crate) fn read_env_or_files(
var_name: &str,
env_files: &[&std::path::Path],
) -> anyhow::Result<String> {
if let Ok(val) = std::env::var(var_name) {
return Ok(val);
}
for path in env_files {
let Ok(contents) = std::fs::read_to_string(path) else {
continue;
};
for line in contents.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let stripped = trimmed.strip_prefix("export ").unwrap_or(trimmed);
if let Some((k, v)) = stripped.split_once('=')
&& k.trim() == var_name
{
let v = v.trim().trim_matches('"').trim_matches('\'');
return Ok(v.to_string());
}
}
}
anyhow::bail!("read master key from environment variable {var_name}")
}
pub(crate) fn load_master_key(config: &ValidatedConfig) -> anyhow::Result<MasterKey> {
let sibling_env = config
.sealed_key_file
.parent()
.map(|p| p.join("master.env"));
let mut candidates: Vec<&std::path::Path> = Vec::new();
if let Some(ref p) = sibling_env {
candidates.push(p.as_path());
}
candidates.push(std::path::Path::new("/var/lib/knot-static/master.env"));
let raw = read_env_or_files(&config.master_key_env, &candidates)?;
let bytes = base64::engine::general_purpose::STANDARD
.decode(raw.trim())
.context("decode master key as base64")?;
MasterKey::new(bytes).context("master key from environment")
}
pub(crate) fn open_store(config: &ValidatedConfig) -> anyhow::Result<SealedStore> {
let master_key = load_master_key(config)?;
SealedStore::open(&config.sealed_key_file, &master_key, Box::new(OsEntropy))
.context("open sealed key store")
}
pub(crate) fn load_knot_signer(
config: &ValidatedConfig,
) -> anyhow::Result<knot_runtime::K256Signer> {
open_store(config)?
.signer(&config.knot_did)
.context("get knot signer")
}
fn load_knot_public_key(config: &ValidatedConfig) -> anyhow::Result<PublicKeyBytes> {
open_store(config)?
.public_key(&config.knot_did)
.context("retrieve knot signing public key")
}
pub fn knot_did_document(config: &ValidatedConfig) -> anyhow::Result<serde_json::Value> {
let signing_key = load_knot_public_key(config)?;
knot_did_document_from_key(config, &signing_key)
}
pub fn knot_did_document_from_key(
config: &ValidatedConfig,
signing_key: &PublicKeyBytes,
) -> anyhow::Result<serde_json::Value> {
let service_url = KnotServiceUrl::new(config.service_url()?.to_string())
.context("parse knot service URL for DID document")?;
let mut doc = knot_atproto::knot_did_document(&config.knot_did, signing_key, &service_url);
strip_pds_service(&mut doc);
Ok(doc)
}
fn strip_pds_service(doc: &mut serde_json::Value) {
if let Some(services) = doc.get_mut("service").and_then(|s| s.as_array_mut()) {
services.retain(|s| s.get("id").and_then(|id| id.as_str()) != Some("#atproto_pds"));
}
}
#[cfg(test)]
mod did_doc_tests {
use super::*;
#[test]
fn did_document_has_no_pds_service() {
let mut doc = serde_json::json!({
"service": [
{"id": "#tangled_knot", "type": "TangledKnot", "serviceEndpoint": "https://git.klbr.net"},
{"id": "#atproto_pds", "type": "AtprotoPersonalDataServer", "serviceEndpoint": "https://git.klbr.net"}
]
});
strip_pds_service(&mut doc);
let services = doc["service"].as_array().unwrap();
assert_eq!(services.len(), 1);
assert_eq!(services[0]["type"], "TangledKnot");
assert!(!doc.to_string().contains("atproto_pds"));
}
}