use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use agent_mesh_protocol::{Fingerprint, SerdeSig, UserKey, UserPublic};
use serde::{Deserialize, Serialize};
use crate::wire_framing::push_field;
const DOMAIN: &[u8] = b"newt/dock-registry/v1";
const SUBJECT: &str = "peers";
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum DockScope {
Mirror,
MirrorInject,
}
impl DockScope {
#[must_use]
pub fn allows_read(self) -> bool {
matches!(self, Self::Mirror | Self::MirrorInject)
}
#[must_use]
pub fn allows_inject(self) -> bool {
matches!(self, Self::MirrorInject)
}
#[must_use]
pub fn as_wire(self) -> &'static str {
match self {
Self::Mirror => "mirror",
Self::MirrorInject => "mirror-inject",
}
}
pub fn parse(s: &str) -> anyhow::Result<Self> {
match s.trim() {
"mirror" => Ok(Self::Mirror),
"mirror-inject" => Ok(Self::MirrorInject),
other => {
anyhow::bail!("unknown dock scope `{other}` (use `mirror` or `mirror-inject`)")
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DockRecord {
pub peer_agent_fingerprint: String,
pub peer_label: String,
pub peer_pubkey: String,
pub scope: DockScope,
pub issued_generation: u64,
pub transcript_id: String,
pub revoked: bool,
pub sig: Option<SerdeSig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DockBundle {
pub issuer: String,
pub subject: String,
pub docks: Vec<DockRecord>,
}
#[derive(Debug, Clone, Default)]
pub struct DockRegistry {
bundles: BTreeMap<(String, String), DockBundle>,
sources: BTreeMap<(String, String), PathBuf>,
root: Option<UserPublic>,
}
pub fn load_docks(
config_path: &Path,
root_key: Option<&UserPublic>,
) -> (DockRegistry, Vec<String>) {
let mut registry = DockRegistry {
bundles: BTreeMap::new(),
sources: BTreeMap::new(),
root: root_key.cloned(),
};
let mut warnings = Vec::new();
let dir = docks_dir(config_path);
let mut paths = match std::fs::read_dir(&dir) {
Ok(entries) => entries
.filter_map(Result::ok)
.map(|e| e.path())
.collect::<Vec<_>>(),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return (registry, warnings),
Err(error) => {
warnings.push(format!("{}: {error}", dir.display()));
return (registry, warnings);
}
};
paths.sort();
for path in paths {
if path.extension().and_then(|s| s.to_str()) != Some("toml") {
continue;
}
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(error) => {
warnings.push(format!("{}: {error}", path.display()));
continue;
}
};
let bundle: DockBundle = match toml::from_str(&text) {
Ok(bundle) => bundle,
Err(error) => {
warnings.push(format!("{}: malformed docks ({error})", path.display()));
continue;
}
};
let Some(root) = root_key else {
if !bundle.docks.is_empty() {
warnings.push(format!(
"{}: no operator root; docks dropped",
path.display()
));
}
continue;
};
if !issuer_matches(&bundle.issuer, root) {
warnings.push(format!("{}: foreign issuer; docks dropped", path.display()));
continue;
}
let mut kept = Vec::new();
for record in bundle.docks {
if verify_record(&bundle.issuer, &bundle.subject, &record, root).is_ok() {
kept.push(record);
} else {
warnings.push(format!(
"{}: unsigned or tampered dock dropped",
path.display()
));
}
}
if !kept.is_empty() {
let key = (bundle.issuer.clone(), bundle.subject.clone());
registry.bundles.insert(
key.clone(),
DockBundle {
docks: kept,
..bundle
},
);
registry.sources.insert(key, path);
}
}
(registry, warnings)
}
pub fn load_docks_with_identity(
config_path: &Path,
identity_pem: &Path,
) -> (DockRegistry, Vec<String>) {
match UserKey::load(identity_pem) {
Ok(key) => load_docks(config_path, Some(&key.public())),
Err(error) => (
DockRegistry::default(),
vec![format!("{}: {error}", identity_pem.display())],
),
}
}
pub fn approve_dock_with_identity(
config_path: &Path,
identity_pem: &Path,
peer_agent_fingerprint: &str,
peer_label: &str,
peer_pubkey: &str,
scope: DockScope,
transcript_id: &str,
) -> anyhow::Result<PathBuf> {
let root = UserKey::load(identity_pem)?;
approve_dock(
config_path,
peer_agent_fingerprint,
peer_label,
peer_pubkey,
scope,
transcript_id,
&root,
)
}
pub fn revoke_dock_with_identity(
config_path: &Path,
identity_pem: &Path,
peer_fingerprint_prefix: &str,
) -> anyhow::Result<String> {
let root = UserKey::load(identity_pem)?;
revoke_dock(config_path, peer_fingerprint_prefix, &root)
}
impl DockRegistry {
#[must_use]
pub fn approved(&self, peer_agent_fingerprint: &str) -> Option<DockRecord> {
let root = self.root.as_ref()?;
for ((issuer, subject), source) in &self.sources {
let Ok(text) = std::fs::read_to_string(source) else {
continue;
};
let Ok(bundle) = toml::from_str::<DockBundle>(&text) else {
continue;
};
if &bundle.issuer != issuer || &bundle.subject != subject {
continue;
}
if let Some(record) = bundle.docks.iter().find(|record| {
!record.revoked
&& record.peer_agent_fingerprint == peer_agent_fingerprint
&& verify_record(&bundle.issuer, &bundle.subject, record, root).is_ok()
}) {
return Some(record.clone());
}
}
None
}
#[must_use]
pub fn live(&self) -> Vec<DockRecord> {
self.bundles
.values()
.flat_map(|bundle| bundle.docks.iter())
.filter(|record| !record.revoked)
.cloned()
.collect()
}
#[must_use]
pub fn len(&self) -> usize {
self.bundles.values().map(|bundle| bundle.docks.len()).sum()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[must_use]
pub fn sign_record(
issuer: &str,
subject: &str,
mut record: DockRecord,
root_key: &UserKey,
) -> DockRecord {
record.sig = Some(SerdeSig(
root_key.sign(&signing_payload(issuer, subject, &record)),
));
record
}
pub fn approve_dock(
config_path: &Path,
peer_agent_fingerprint: &str,
peer_label: &str,
peer_pubkey: &str,
scope: DockScope,
transcript_id: &str,
root_key: &UserKey,
) -> anyhow::Result<PathBuf> {
if peer_agent_fingerprint.is_empty() {
anyhow::bail!("a peer agent fingerprint is required");
}
if !fingerprint_binds_pubkey(peer_agent_fingerprint, peer_pubkey) {
anyhow::bail!(
"peer fingerprint {peer_agent_fingerprint} is not BLAKE3(pubkey) — refusing to sign a decoupled approval"
);
}
let issuer = root_key.public().fingerprint().hex();
let dir = docks_dir(config_path);
std::fs::create_dir_all(&dir)?;
let path = dir.join(format!("{SUBJECT}.toml"));
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&path))?;
let mut bundle = match std::fs::read_to_string(&path) {
Ok(text) => toml::from_str::<DockBundle>(&text)?,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.to_owned(),
docks: Vec::new(),
},
Err(error) => return Err(error.into()),
};
if bundle.issuer != issuer || bundle.subject != SUBJECT {
anyhow::bail!("{} belongs to another operator", path.display());
}
let next_generation = bundle
.docks
.iter()
.filter(|held| held.peer_agent_fingerprint == peer_agent_fingerprint)
.map(|held| held.issued_generation + 1)
.max()
.unwrap_or(0);
bundle
.docks
.retain(|held| held.peer_agent_fingerprint != peer_agent_fingerprint);
let record = DockRecord {
peer_agent_fingerprint: peer_agent_fingerprint.to_owned(),
peer_label: peer_label.to_owned(),
peer_pubkey: peer_pubkey.to_owned(),
scope,
issued_generation: next_generation,
transcript_id: transcript_id.to_owned(),
revoked: false,
sig: None,
};
bundle
.docks
.push(sign_record(&issuer, SUBJECT, record, root_key));
crate::atomic_fs::atomic_write(&path, toml::to_string(&bundle)?.as_bytes())?;
Ok(path)
}
pub fn revoke_dock(
config_path: &Path,
peer_fingerprint_prefix: &str,
root_key: &UserKey,
) -> anyhow::Result<String> {
if peer_fingerprint_prefix.is_empty() {
anyhow::bail!("a peer fingerprint prefix is required");
}
let issuer = root_key.public().fingerprint().hex();
let path = docks_dir(config_path).join(format!("{SUBJECT}.toml"));
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&path))?;
let text = std::fs::read_to_string(&path)
.map_err(|error| anyhow::anyhow!("{}: {error}", path.display()))?;
let mut bundle: DockBundle = toml::from_str(&text)?;
if bundle.issuer != issuer || bundle.subject != SUBJECT {
anyhow::bail!("{} belongs to another operator", path.display());
}
let matches: Vec<usize> = bundle
.docks
.iter()
.enumerate()
.filter(|(_, held)| {
held.peer_agent_fingerprint
.starts_with(peer_fingerprint_prefix)
&& !held.revoked
})
.map(|(index, _)| index)
.collect();
let [index] = matches[..] else {
anyhow::bail!(
"`{peer_fingerprint_prefix}` matches {} live docks; use a longer prefix",
matches.len()
);
};
let mut record = bundle.docks[index].clone();
record.revoked = true;
record.issued_generation += 1;
let fingerprint = record.peer_agent_fingerprint.clone();
bundle.docks[index] = sign_record(&issuer, SUBJECT, record, root_key);
crate::atomic_fs::atomic_write(&path, toml::to_string(&bundle)?.as_bytes())?;
Ok(fingerprint)
}
pub fn revoke_all_docks(config_path: &Path, root_key: &UserKey) -> anyhow::Result<Vec<String>> {
let issuer = root_key.public().fingerprint().hex();
let path = docks_dir(config_path).join(format!("{SUBJECT}.toml"));
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&path))?;
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(error) => return Err(error.into()),
};
let mut bundle: DockBundle = toml::from_str(&text)?;
if bundle.issuer != issuer || bundle.subject != SUBJECT {
anyhow::bail!("{} belongs to another operator", path.display());
}
let mut revoked = Vec::new();
for record in &mut bundle.docks {
if !record.revoked {
record.revoked = true;
record.issued_generation += 1;
revoked.push(record.peer_agent_fingerprint.clone());
*record = sign_record(&issuer, SUBJECT, record.clone(), root_key);
}
}
if !revoked.is_empty() {
crate::atomic_fs::atomic_write(&path, toml::to_string(&bundle)?.as_bytes())?;
}
Ok(revoked)
}
#[must_use]
pub fn agent_fingerprint_of_pubkey(pubkey: &[u8; 32]) -> String {
Fingerprint::of_bytes(pubkey).hex()
}
#[must_use]
pub fn decode_agent_pubkey(hex: &str) -> Option<[u8; 32]> {
decode_hex::<32>(hex)
}
fn decode_hex<const N: usize>(hex: &str) -> Option<[u8; N]> {
let hex = hex.trim();
if hex.len() != 2 * N {
return None;
}
let mut out = [0u8; N];
for (i, byte) in out.iter_mut().enumerate() {
*byte = u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok()?;
}
Some(out)
}
#[must_use]
fn fingerprint_binds_pubkey(fingerprint: &str, pubkey_hex: &str) -> bool {
match decode_agent_pubkey(pubkey_hex) {
Some(bytes) => agent_fingerprint_of_pubkey(&bytes) == fingerprint,
None => false,
}
}
const DOCK_RP_ID: &str = "newt-dock/v1";
const DOCK_COSE_ALG: i64 = -8;
#[must_use]
pub fn pubkey_words(pubkey: &[u8; 32]) -> [&'static str; crate::sas_transcript::SAS_WORD_COUNT] {
let mut payload = Vec::with_capacity(64);
payload.extend_from_slice(b"newt/dock-pubkey-words/v1");
push_field(&mut payload, pubkey);
crate::sas_transcript::sas_words(&Fingerprint::of_bytes(&payload))
}
pub const PAIRING_CODE_DIGITS: usize = 6;
#[must_use]
pub fn pairing_commitment(
hub_pubkey: &[u8; 32],
host_pubkey: &[u8; 32],
host_nonce: &[u8; 16],
) -> [u8; 32] {
let mut payload = Vec::with_capacity(112);
payload.extend_from_slice(b"newt/dock-pairing-commit/v1");
push_field(&mut payload, hub_pubkey);
push_field(&mut payload, host_pubkey);
push_field(&mut payload, host_nonce);
Fingerprint::of_bytes(&payload).0
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Pairing {
pub code: String,
pub transcript: PairingTranscript,
pub transcript_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PairingTranscript {
pub protocol: String,
pub hub_pubkey: String,
pub host_pubkey: String,
pub host_commitment: String,
pub hub_nonce: String,
pub host_nonce: String,
}
impl content_addressable::ContentAddressable for PairingTranscript {
fn canonical_form(&self) -> Result<Vec<u8>, content_addressable::ContentError> {
content_addressable::canonical::to_canonical_dagcbor(self)
}
}
#[must_use]
pub fn pairing(
hub_pubkey: &[u8; 32],
host_pubkey: &[u8; 32],
hub_nonce: &[u8; 16],
host_nonce: &[u8; 16],
) -> Option<Pairing> {
use content_addressable::ContentAddressable as _;
let transcript = PairingTranscript {
protocol: "newt/dock-pairing/v1".into(),
hub_pubkey: hex(hub_pubkey),
host_pubkey: hex(host_pubkey),
host_commitment: hex(&pairing_commitment(hub_pubkey, host_pubkey, host_nonce)),
hub_nonce: hex(hub_nonce),
host_nonce: hex(host_nonce),
};
let id = transcript.content_id().ok()?;
let mut payload = b"newt/dock-pairing-code/v1".to_vec();
push_field(&mut payload, &transcript.canonical_form().ok()?);
let digest = Fingerprint::of_bytes(&payload);
let mut head = [0u8; 8];
head.copy_from_slice(&digest.0[..8]);
Some(Pairing {
code: format!("{:06}", u64::from_be_bytes(head) % 1_000_000),
transcript,
transcript_id: id.to_string(),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DockCeremony {
pub transcript_id: String,
pub sas_words: [&'static str; crate::sas_transcript::SAS_WORD_COUNT],
pub commitment: String,
}
#[must_use]
pub fn dock_ceremony(
issuer: &str,
peer_label: &str,
peer_pubkey: &[u8; 32],
nonce: &[u8],
blinding: &[u8],
) -> DockCeremony {
use crate::sas_transcript::{commit, TranscriptInputs};
let commitment = commit(peer_pubkey, blinding);
let peer_fp = Fingerprint::of_bytes(peer_pubkey).hex();
let inputs = TranscriptInputs {
rp_id: DOCK_RP_ID,
issuer,
subject: peer_label,
mesh_agent_fingerprint: &peer_fp,
cose_alg: DOCK_COSE_ALG,
cose_pubkey: peer_pubkey,
commitment: &commitment,
enroll_nonce: nonce,
};
let transcript = inputs.transcript_id();
DockCeremony {
transcript_id: transcript.hex(),
sas_words: pubkey_words(peer_pubkey),
commitment: commitment.hex(),
}
}
pub const STAGED_HOST_TTL: std::time::Duration = std::time::Duration::from_secs(5 * 60);
pub const MAX_STAGED_HOSTS: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StagedHost {
pub peer_agent_fingerprint: String,
pub peer_pubkey: String,
pub peer_label: String,
pub last_seen_unix: u64,
pub hub_pubkey: String,
#[serde(default)]
pub host_commitment: Option<String>,
#[serde(default)]
pub hub_nonce: Option<String>,
#[serde(default)]
pub host_nonce: Option<String>,
}
impl StagedHost {
#[must_use]
pub fn pairing(&self) -> Option<Pairing> {
let hub = decode_hex::<32>(&self.hub_pubkey)?;
let host = decode_hex::<32>(&self.peer_pubkey)?;
let commitment = decode_hex::<32>(self.host_commitment.as_deref()?)?;
let hub_nonce = decode_hex::<16>(self.hub_nonce.as_deref()?)?;
let host_nonce = decode_hex::<16>(self.host_nonce.as_deref()?)?;
(pairing_commitment(&hub, &host, &host_nonce) == commitment)
.then(|| pairing(&hub, &host, &hub_nonce, &host_nonce))
.flatten()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairingStep {
Commit([u8; 32]),
Reveal([u8; 16]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairingReply {
Nonce([u8; 16]),
Paired,
Failed,
}
impl content_addressable::ContentAddressable for StagedHost {
fn canonical_form(&self) -> Result<Vec<u8>, content_addressable::ContentError> {
content_addressable::canonical::to_canonical_dagcbor(self)
}
}
#[derive(Debug, Serialize, Deserialize)]
struct StagedFile {
id: content_addressable::ContentId,
host: StagedHost,
}
pub fn stage_host(
config_path: &Path,
pubkey: &[u8; 32],
label: &str,
hub_pubkey: &[u8; 32],
now: std::time::SystemTime,
) -> anyhow::Result<()> {
if !is_host_label(label) {
anyhow::bail!("host label {label:?} is not a plain instance name");
}
let peer_agent_fingerprint = agent_fingerprint_of_pubkey(pubkey);
let staged = StagedHost {
peer_pubkey: hex(pubkey),
peer_label: label.to_owned(),
last_seen_unix: unix_secs(now),
hub_pubkey: hex(hub_pubkey),
peer_agent_fingerprint,
host_commitment: None,
hub_nonce: None,
host_nonce: None,
};
let dir = staged_dir(config_path);
std::fs::create_dir_all(&dir)?;
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&dir))?;
let live = read_staged(&dir, now, true);
let staged_count = live.len();
let mut staged = staged;
match live
.into_iter()
.find(|held| held.peer_agent_fingerprint == staged.peer_agent_fingerprint)
{
Some(held) if held.hub_pubkey == staged.hub_pubkey => {
staged.host_commitment = held.host_commitment;
staged.hub_nonce = held.hub_nonce;
staged.host_nonce = held.host_nonce;
}
Some(_) => {}
None if staged_count >= MAX_STAGED_HOSTS => {
anyhow::bail!("{MAX_STAGED_HOSTS} hosts are already staged; approve or wait them out");
}
None => {}
}
write_staged(&dir, staged)
}
pub fn pair_staged_host(
config_path: &Path,
host_pubkey: &[u8; 32],
hub_pubkey: &[u8; 32],
step: PairingStep,
fresh_nonce: [u8; 16],
now: std::time::SystemTime,
) -> anyhow::Result<PairingReply> {
let dir = staged_dir(config_path);
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&dir))?;
let fp = agent_fingerprint_of_pubkey(host_pubkey);
let hub_hex = hex(hub_pubkey);
let mut staged = read_staged(&dir, now, false)
.into_iter()
.find(|held| held.peer_agent_fingerprint == fp && held.hub_pubkey == hub_hex)
.ok_or_else(|| anyhow::anyhow!("host {fp} is not staged by this hub"))?;
let reply = match step {
PairingStep::Commit(commitment) => {
staged.host_commitment = Some(hex(&commitment));
staged.hub_nonce = Some(hex(&fresh_nonce));
staged.host_nonce = None;
PairingReply::Nonce(fresh_nonce)
}
PairingStep::Reveal(host_nonce) => {
let opens = staged
.host_commitment
.as_deref()
.and_then(decode_hex::<32>)
.filter(|_| staged.hub_nonce.is_some())
.is_some_and(|c| pairing_commitment(hub_pubkey, host_pubkey, &host_nonce) == c);
if opens {
staged.host_nonce = Some(hex(&host_nonce));
PairingReply::Paired
} else {
staged.host_commitment = None;
staged.hub_nonce = None;
staged.host_nonce = None;
PairingReply::Failed
}
}
};
write_staged(&dir, staged)?;
Ok(reply)
}
fn write_staged(dir: &Path, staged: StagedHost) -> anyhow::Result<()> {
use content_addressable::ContentAddressable as _;
let file = StagedFile {
id: staged.content_id()?,
host: staged,
};
let path = dir.join(format!("{}.toml", file.host.peer_agent_fingerprint));
crate::atomic_fs::atomic_write(&path, toml::to_string(&file)?.as_bytes())?;
Ok(())
}
fn hex(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{b:02x}")).collect()
}
#[must_use]
pub fn staged_hosts(config_path: &Path, now: std::time::SystemTime) -> Vec<StagedHost> {
read_staged(&staged_dir(config_path), now, false)
}
fn read_staged(dir: &Path, now: std::time::SystemTime, reap: bool) -> Vec<StagedHost> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let now = unix_secs(now);
let oldest = now.saturating_sub(STAGED_HOST_TTL.as_secs());
let mut hosts: Vec<StagedHost> = entries
.filter_map(|entry| {
let path = entry.ok()?.path();
if path.extension()? != "toml" {
return None;
}
let host = verified_staged(&path)
.filter(|host| host.last_seen_unix > oldest && host.last_seen_unix <= now);
if host.is_none() && reap {
let _ = std::fs::remove_file(&path);
}
host
})
.collect();
hosts.sort_by(|a, b| a.peer_label.cmp(&b.peer_label));
hosts
}
fn verified_staged(path: &Path) -> Option<StagedHost> {
use content_addressable::ContentAddressable as _;
let file: StagedFile = toml::from_str(&std::fs::read_to_string(path).ok()?).ok()?;
let host = file.host;
let addressed = host.content_id().ok()? == file.id;
let named_for_key = path.file_stem()?.to_str()? == host.peer_agent_fingerprint;
(addressed
&& named_for_key
&& fingerprint_binds_pubkey(&host.peer_agent_fingerprint, &host.peer_pubkey)
&& decode_agent_pubkey(&host.hub_pubkey).is_some()
&& is_host_label(&host.peer_label))
.then_some(host)
}
pub fn unstage_host(config_path: &Path, peer_agent_fingerprint: &str) -> anyhow::Result<()> {
let dir = staged_dir(config_path);
if !dir.exists() {
return Ok(());
}
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&dir))?;
remove_staged(&dir, peer_agent_fingerprint)
}
pub fn promote_staged_host<T>(
config_path: &Path,
peer_agent_fingerprint: &str,
transcript_id: &str,
approve: impl FnOnce() -> anyhow::Result<T>,
) -> anyhow::Result<T> {
let dir = staged_dir(config_path);
let _lock = crate::atomic_fs::acquire_lock(&crate::atomic_fs::lock_path_for(&dir))?;
let held = read_staged(&dir, std::time::SystemTime::now(), false)
.into_iter()
.find(|host| host.peer_agent_fingerprint == peer_agent_fingerprint)
.and_then(|host| host.pairing());
if held.is_none_or(|p| p.transcript_id != transcript_id) {
anyhow::bail!("the host no longer holds the pairing compared; nothing was approved");
}
let approved = approve()?;
remove_staged(&dir, peer_agent_fingerprint)?;
Ok(approved)
}
fn remove_staged(dir: &Path, peer_agent_fingerprint: &str) -> anyhow::Result<()> {
match std::fs::remove_file(dir.join(format!("{peer_agent_fingerprint}.toml"))) {
Err(error) if error.kind() != std::io::ErrorKind::NotFound => Err(error.into()),
_ => Ok(()),
}
}
fn is_host_label(label: &str) -> bool {
(1..=80).contains(&label.len())
&& label
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
fn unix_secs(at: std::time::SystemTime) -> u64 {
at.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs())
}
fn staged_dir(config_path: &Path) -> PathBuf {
config_path.with_file_name("ocap").join("docks.staged")
}
fn docks_dir(config_path: &Path) -> PathBuf {
config_path.with_file_name("ocap").join("docks.d")
}
fn issuer_matches(issuer: &str, root: &UserPublic) -> bool {
issuer == root.fingerprint().short() || issuer == root.fingerprint().hex()
}
fn verify_record(
issuer: &str,
subject: &str,
record: &DockRecord,
root: &UserPublic,
) -> Result<(), ()> {
if !issuer_matches(issuer, root) {
return Err(());
}
if !fingerprint_binds_pubkey(&record.peer_agent_fingerprint, &record.peer_pubkey) {
return Err(());
}
let Some(signature) = record.sig.as_ref() else {
return Err(());
};
root.verify(&signing_payload(issuer, subject, record), &signature.0)
.map_err(|_| ())
}
fn signing_payload(issuer: &str, subject: &str, record: &DockRecord) -> Vec<u8> {
let mut payload = Vec::with_capacity(256);
payload.extend_from_slice(DOMAIN);
push_field(&mut payload, issuer.as_bytes());
push_field(&mut payload, subject.as_bytes());
push_field(&mut payload, record.peer_agent_fingerprint.as_bytes());
push_field(&mut payload, record.peer_label.as_bytes());
push_field(&mut payload, record.peer_pubkey.as_bytes());
push_field(&mut payload, record.scope.as_wire().as_bytes());
push_field(&mut payload, &record.issued_generation.to_be_bytes());
push_field(&mut payload, record.transcript_id.as_bytes());
push_field(&mut payload, &[u8::from(record.revoked)]);
payload
}
#[cfg(test)]
mod tests {
use super::*;
use content_addressable::ContentAddressable as _;
use tempfile::TempDir;
fn pk_hex(seed: u8) -> String {
format!("{seed:02x}").repeat(32)
}
fn fp(seed: u8) -> String {
agent_fingerprint_of_pubkey(&[seed; 32])
}
fn record() -> DockRecord {
DockRecord {
peer_agent_fingerprint: fp(0),
peer_label: "laptop-b".into(),
peer_pubkey: pk_hex(0),
scope: DockScope::MirrorInject,
issued_generation: 0,
transcript_id: "tx-1".into(),
revoked: false,
sig: None,
}
}
fn write_bundle(dir: &TempDir, bundle: &DockBundle) {
let path = dir.path().join("ocap/docks.d");
std::fs::create_dir_all(&path).unwrap();
std::fs::write(path.join("peers.toml"), toml::to_string(bundle).unwrap()).unwrap();
}
#[test]
fn signed_dock_loads_and_resolves() {
let root = UserKey::generate();
let issuer = root.fingerprint().hex();
let signed = sign_record(&issuer, SUBJECT, record(), &root);
let dir = TempDir::new().unwrap();
write_bundle(
&dir,
&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![signed],
},
);
let (registry, warnings) =
load_docks(&dir.path().join("config.toml"), Some(&root.public()));
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
assert_eq!(registry.len(), 1);
assert!(registry.approved(&fp(0)).is_some());
assert!(registry.approved("unknown").is_none());
}
#[test]
#[cfg(unix)]
fn a_symlinked_docks_entry_with_a_foreign_signature_is_dropped() {
use std::os::unix::fs::symlink;
let root = UserKey::generate(); let attacker = UserKey::generate(); let issuer = attacker.fingerprint().hex();
let signed = sign_record(&issuer, SUBJECT, record(), &attacker);
let bundle = DockBundle {
issuer,
subject: SUBJECT.into(),
docks: vec![signed],
};
let dir = TempDir::new().unwrap();
let external = dir.path().join("attacker-bundle.toml");
std::fs::write(&external, toml::to_string(&bundle).unwrap()).unwrap();
let docks = dir.path().join("ocap/docks.d");
std::fs::create_dir_all(&docks).unwrap();
symlink(&external, docks.join("evil.toml")).unwrap();
let (registry, warnings) =
load_docks(&dir.path().join("config.toml"), Some(&root.public()));
assert!(
registry.approved(&fp(0)).is_none(),
"a foreign-signed symlinked dock must not approve anyone"
);
assert!(registry.is_empty());
assert!(
warnings.iter().any(|w| w.contains("foreign issuer")),
"the drop must be surfaced to the operator: {warnings:?}"
);
}
#[test]
fn the_authority_decision_is_case_exact_not_filesystem_case_dependent() {
let root = UserKey::generate();
let issuer = root.fingerprint().hex();
let dir = TempDir::new().unwrap();
let docks = dir.path().join("ocap/docks.d");
std::fs::create_dir_all(&docks).unwrap();
std::fs::write(
docks.join("peers.TOML"),
toml::to_string(&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![sign_record(&issuer, SUBJECT, record(), &root)],
})
.unwrap(),
)
.unwrap();
let (registry, _w) = load_docks(&dir.path().join("config.toml"), Some(&root.public()));
assert!(
registry.is_empty(),
"a .TOML-extension bundle must not be loaded (extension match is case-exact, fail-closed)"
);
let dir2 = TempDir::new().unwrap();
write_bundle(
&dir2,
&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![sign_record(&issuer, SUBJECT, record(), &root)],
},
);
let (registry2, _w2) = load_docks(&dir2.path().join("config.toml"), Some(&root.public()));
assert!(
registry2.approved(&fp(0)).is_some(),
"the exact fingerprint is approved"
);
assert!(
registry2.approved(&fp(0).to_uppercase()).is_none(),
"an upper-cased fingerprint is NOT approved (no case-folding)"
);
}
#[test]
fn a_tampered_scope_or_foreign_issuer_is_dropped() {
let root = UserKey::generate();
let other = UserKey::generate();
let issuer = root.fingerprint().hex();
let mut signed = sign_record(&issuer, SUBJECT, record(), &root);
signed.scope = DockScope::Mirror;
let dir = TempDir::new().unwrap();
write_bundle(
&dir,
&DockBundle {
issuer: other.fingerprint().hex(),
subject: SUBJECT.into(),
docks: vec![signed, record()],
},
);
let (registry, warnings) =
load_docks(&dir.path().join("config.toml"), Some(&root.public()));
assert!(registry.is_empty());
assert!(!warnings.is_empty());
}
#[test]
fn approve_then_revoke_removes_the_dock_from_resolve() {
let root = UserKey::generate();
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
approve_dock(
&config,
&fp(0),
"laptop-b",
&pk_hex(0),
DockScope::MirrorInject,
"tx-1",
&root,
)
.unwrap();
let (registry, _) = load_docks(&config, Some(&root.public()));
assert!(registry.approved(&fp(0)).is_some());
let full = revoke_dock(&config, &fp(0)[..8], &root).unwrap();
assert_eq!(full, fp(0));
let (registry, _) = load_docks(&config, Some(&root.public()));
assert!(
registry.approved(&fp(0)).is_none(),
"a revoked dock must never resolve"
);
assert!(registry.live().is_empty());
}
#[test]
fn load_with_identity_resolves_without_crossing_the_userpublic_seam() {
let root = UserKey::generate();
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let identity = dir.path().join("identity.pem");
root.save(&identity).unwrap();
approve_dock(
&config,
&fp(0),
"laptop-b",
&pk_hex(0),
DockScope::MirrorInject,
"tx-1",
&root,
)
.unwrap();
let (registry, warnings) = load_docks_with_identity(&config, &identity);
assert!(warnings.is_empty(), "unexpected warnings: {warnings:?}");
assert!(registry.approved(&fp(0)).is_some());
let (empty, warnings) = load_docks_with_identity(&config, &dir.path().join("nope.pem"));
assert!(empty.approved(&fp(0)).is_none());
assert!(!warnings.is_empty());
}
#[test]
fn re_approval_is_idempotent_and_bumps_generation() {
let root = UserKey::generate();
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
approve_dock(
&config,
&fp(0),
"laptop-b",
&pk_hex(0),
DockScope::MirrorInject,
"tx-1",
&root,
)
.unwrap();
approve_dock(
&config,
&fp(0),
"laptop-b-renamed",
&pk_hex(0),
DockScope::MirrorInject,
"tx-2",
&root,
)
.unwrap();
let (registry, _) = load_docks(&config, Some(&root.public()));
assert_eq!(registry.len(), 1, "re-approval replaces, never duplicates");
let dock = registry.approved(&fp(0)).unwrap();
assert_eq!(dock.peer_label, "laptop-b-renamed");
assert_eq!(dock.issued_generation, 1);
}
#[test]
fn revoke_all_revokes_every_live_dock() {
let root = UserKey::generate();
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
approve_dock(
&config,
&fp(0),
"a",
&pk_hex(0),
DockScope::MirrorInject,
"tx-a",
&root,
)
.unwrap();
approve_dock(
&config,
&fp(1),
"b",
&pk_hex(1),
DockScope::MirrorInject,
"tx-b",
&root,
)
.unwrap();
let revoked = revoke_all_docks(&config, &root).unwrap();
assert_eq!(revoked.len(), 2);
let (registry, _) = load_docks(&config, Some(&root.public()));
assert!(registry.live().is_empty());
}
#[test]
fn dial_time_read_rejects_a_flipped_byte() {
let root = UserKey::generate();
let issuer = root.fingerprint().hex();
let signed = sign_record(&issuer, SUBJECT, record(), &root);
let dir = TempDir::new().unwrap();
write_bundle(
&dir,
&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![signed],
},
);
let (registry, _) = load_docks(&dir.path().join("config.toml"), Some(&root.public()));
let path = dir.path().join("ocap/docks.d/peers.toml");
let mut tampered: DockBundle =
toml::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
tampered.docks[0].peer_label.push('!');
std::fs::write(&path, toml::to_string(&tampered).unwrap()).unwrap();
assert!(
registry.approved(&fp(0)).is_none(),
"a row edited after load must not resolve"
);
}
#[test]
fn agent_fingerprint_of_pubkey_matches_the_mesh_convention() {
let pubkey = [7u8; 32];
assert_eq!(
agent_fingerprint_of_pubkey(&pubkey),
Fingerprint::of_bytes(&pubkey).hex()
);
}
#[test]
fn dock_display_words_come_from_the_pubkey_so_the_peer_can_reproduce_them() {
let pubkey = [3u8; 32];
assert_eq!(pubkey_words(&pubkey), pubkey_words(&pubkey));
assert_ne!(pubkey_words(&pubkey), pubkey_words(&[4u8; 32]));
let a = dock_ceremony("issuer-fp", "laptop-b", &pubkey, b"nonce-1", b"blind-1");
assert_eq!(a.sas_words, pubkey_words(&pubkey));
let rerun = dock_ceremony("issuer-fp", "laptop-b", &pubkey, b"nonce-2", b"blind-1");
assert_eq!(a.sas_words, rerun.sas_words);
assert_ne!(a.transcript_id, rerun.transcript_id);
let other = dock_ceremony("issuer-fp", "laptop-b", &[4u8; 32], b"nonce-1", b"blind-1");
assert_ne!(a.sas_words, other.sas_words);
}
#[test]
fn a_record_binding_one_fingerprint_to_another_pubkey_is_refused_and_dropped() {
let root = UserKey::generate();
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let err = approve_dock(
&config,
&fp(1), "impostor",
&pk_hex(2), DockScope::MirrorInject,
"tx-x",
&root,
)
.unwrap_err();
assert!(err.to_string().contains("not BLAKE3(pubkey)"));
let issuer = root.fingerprint().hex();
let decoupled = DockRecord {
peer_agent_fingerprint: fp(1),
peer_pubkey: pk_hex(2),
..record()
};
let dir2 = TempDir::new().unwrap();
write_bundle(
&dir2,
&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![sign_record(&issuer, SUBJECT, decoupled, &root)],
},
);
let (registry, warnings) =
load_docks(&dir2.path().join("config.toml"), Some(&root.public()));
assert!(
registry.is_empty(),
"a decoupled fp/pubkey record must not load"
);
assert!(!warnings.is_empty());
assert!(registry.approved(&fp(1)).is_none());
}
#[test]
fn a_malformed_pubkey_record_is_dropped() {
let root = UserKey::generate();
let issuer = root.fingerprint().hex();
for bad in ["zz".repeat(32), "00".repeat(20), String::new()] {
let mut rec = record();
rec.peer_pubkey = bad;
let dir = TempDir::new().unwrap();
write_bundle(
&dir,
&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![sign_record(&issuer, SUBJECT, rec, &root)],
},
);
let (registry, _) = load_docks(&dir.path().join("config.toml"), Some(&root.public()));
assert!(
registry.is_empty(),
"a malformed-pubkey record must not load"
);
}
}
#[test]
fn concurrent_approvals_do_not_lost_update() {
let root = UserKey::generate();
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let identity = dir.path().join("identity.pem");
root.save(&identity).unwrap();
let handles: Vec<_> = (0u8..6)
.map(|s| {
let config = config.clone();
let identity = identity.clone();
std::thread::spawn(move || {
let deadline = std::time::Instant::now() + crate::test_guard::HANG_GUARD;
loop {
match approve_dock_with_identity(
&config,
&identity,
&agent_fingerprint_of_pubkey(&[s; 32]),
&format!("peer-{s}"),
&format!("{s:02x}").repeat(32),
DockScope::MirrorInject,
"tx",
) {
Ok(_) => return,
Err(error) if crate::atomic_fs::is_lock_contended(&error) => {
assert!(
std::time::Instant::now() < deadline,
"peer-{s} never acquired the lock within the hang guard — \
that is a stuck lock, not contention: {error:#}"
);
std::thread::yield_now();
}
Err(error) => {
panic!("peer-{s} failed for a non-contention reason: {error:#}")
}
}
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
let (registry, _) = load_docks(&config, Some(&root.public()));
assert_eq!(
registry.live().len(),
6,
"every concurrent approval must survive the lock-serialized write"
);
}
#[test]
fn dock_scope_permits_per_operation_and_parses_least_authority() {
assert!(DockScope::Mirror.allows_read());
assert!(!DockScope::Mirror.allows_inject());
assert!(DockScope::MirrorInject.allows_read());
assert!(DockScope::MirrorInject.allows_inject());
assert_eq!(DockScope::parse("mirror").unwrap(), DockScope::Mirror);
assert_eq!(
DockScope::parse("mirror-inject").unwrap(),
DockScope::MirrorInject
);
assert!(DockScope::parse("co-drive").is_err());
assert!(DockScope::parse("").is_err());
}
#[test]
fn an_unknown_scope_token_drops_the_whole_record_fail_closed() {
let root = UserKey::generate();
let issuer = root.fingerprint().hex();
let signed = sign_record(&issuer, SUBJECT, record(), &root);
let dir = TempDir::new().unwrap();
write_bundle(
&dir,
&DockBundle {
issuer: issuer.clone(),
subject: SUBJECT.into(),
docks: vec![signed],
},
);
let path = dir.path().join("ocap/docks.d/peers.toml");
let text = std::fs::read_to_string(&path)
.unwrap()
.replace("mirror-inject", "co-drive");
std::fs::write(&path, text).unwrap();
let (registry, warnings) =
load_docks(&dir.path().join("config.toml"), Some(&root.public()));
assert!(registry.is_empty(), "an unknown scope must not load");
assert!(!warnings.is_empty());
}
#[test]
fn decode_agent_pubkey_round_trips_and_rejects_bad_input() {
assert_eq!(decode_agent_pubkey(&pk_hex(7)), Some([7u8; 32]));
assert!(decode_agent_pubkey("short").is_none());
assert!(decode_agent_pubkey(&"zz".repeat(32)).is_none());
assert!(fingerprint_binds_pubkey(&fp(3), &pk_hex(3)));
assert!(!fingerprint_binds_pubkey(&fp(3), &pk_hex(4)));
}
#[test]
fn a_dock_grant_is_a_location_scoped_bearer_record_gated_by_root_key_possession() {
let operator = UserKey::generate();
let dir_b = TempDir::new().unwrap();
let config_b = dir_b.path().join("config.toml");
let identity_b = dir_b.path().join("identity.pem");
operator.save(&identity_b).unwrap();
approve_dock(
&config_b,
&fp(0),
"caller-a",
&pk_hex(0),
DockScope::MirrorInject,
"tx",
&operator,
)
.unwrap();
let (reg_b, _) = load_docks_with_identity(&config_b, &identity_b);
assert!(
reg_b.approved(&fp(0)).is_some(),
"A must resolve at its own responder B"
);
let dir_c = TempDir::new().unwrap();
let config_c = dir_c.path().join("config.toml");
let docks_c = docks_dir(&config_c);
std::fs::create_dir_all(&docks_c).unwrap();
let bytes = std::fs::read(docks_dir(&config_b).join(format!("{SUBJECT}.toml"))).unwrap();
std::fs::write(docks_c.join(format!("{SUBJECT}.toml")), &bytes).unwrap();
let (reg_c_no_key, warnings) =
load_docks_with_identity(&config_c, &dir_c.path().join("identity.pem"));
assert!(
reg_c_no_key.approved(&fp(0)).is_none(),
"a copied grant must NOT resolve without the co-located operator root key"
);
assert!(
!warnings.is_empty(),
"the missing root key must surface as an operator-visible warning"
);
let identity_c = dir_c.path().join("identity.pem");
operator.save(&identity_c).unwrap();
let (reg_c_with_key, _) = load_docks_with_identity(&config_c, &identity_c);
assert!(
reg_c_with_key.approved(&fp(0)).is_some(),
"with the co-located operator root key the bearer grant resolves"
);
}
const HUB: [u8; 32] = [9; 32];
#[test]
fn a_pairing_code_is_six_digits_bound_to_both_keys_and_both_nonces() {
let p = pairing(&[1; 32], &[2; 32], &[3; 16], &[4; 16]).unwrap();
assert_eq!(p.code.len(), PAIRING_CODE_DIGITS);
let id = p.transcript.content_id().unwrap();
assert_eq!(
p.transcript_id,
id.to_string(),
"the id is the transcript's CID"
);
assert_eq!(
p.transcript_id
.parse::<content_addressable::ContentId>()
.unwrap(),
id
);
assert!(p.code.bytes().all(|b| b.is_ascii_digit()), "{}", p.code);
assert_eq!(
p,
pairing(&[1; 32], &[2; 32], &[3; 16], &[4; 16]).unwrap(),
"deterministic"
);
for other in [
pairing(&[2; 32], &[1; 32], &[3; 16], &[4; 16]),
pairing(&[1; 32], &[2; 32], &[5; 16], &[4; 16]),
pairing(&[1; 32], &[2; 32], &[3; 16], &[5; 16]),
] {
assert_ne!(other.unwrap().transcript_id, p.transcript_id);
}
}
#[test]
fn a_staged_host_pairs_only_by_opening_its_commitment() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let (host, host_nonce, hub_nonce) = ([7u8; 32], [5u8; 16], [6u8; 16]);
let commit = PairingStep::Commit(pairing_commitment(&HUB, &host, &host_nonce));
let step = |s| pair_staged_host(&config, &host, &HUB, s, hub_nonce, at(1)).unwrap();
let live = || staged_hosts(&config, at(1)).remove(0);
assert!(
pair_staged_host(&config, &host, &HUB, commit, hub_nonce, at(1)).is_err(),
"only a staged host pairs"
);
stage_host(&config, &host, "nuc1", &HUB, at(1)).unwrap();
assert_eq!(step(commit), PairingReply::Nonce(hub_nonce));
assert_eq!(live().pairing(), None, "not complete before the reveal");
assert_eq!(step(PairingStep::Reveal([0; 16])), PairingReply::Failed);
assert_eq!(
live().host_commitment,
None,
"a failed reveal clears pairing"
);
step(commit);
assert_eq!(step(PairingStep::Reveal(host_nonce)), PairingReply::Paired);
let expected = pairing(&HUB, &host, &hub_nonce, &host_nonce).unwrap();
assert_eq!(live().pairing(), Some(expected.clone()));
stage_host(&config, &host, "nuc1", &HUB, at(1)).unwrap();
assert_eq!(
live().pairing(),
Some(expected),
"a refresh keeps the pairing"
);
stage_host(&config, &host, "nuc1", &[8; 32], at(1)).unwrap();
assert_eq!(live().pairing(), None, "another hub's staging starts over");
}
#[test]
fn a_staged_host_is_promoted_only_under_the_pairing_compared_and_atomically() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let (host, now) = ([7u8; 32], std::time::SystemTime::now());
let fp = agent_fingerprint_of_pubkey(&host);
let step = |s| pair_staged_host(&config, &host, &HUB, s, [6; 16], now);
stage_host(&config, &host, "nuc1", &HUB, now).unwrap();
step(PairingStep::Commit(pairing_commitment(
&HUB, &host, &[5; 16],
)))
.unwrap();
step(PairingStep::Reveal([5; 16])).unwrap();
let compared = staged_hosts(&config, now)[0].pairing().unwrap();
let refused = promote_staged_host(&config, &fp, "another", || -> anyhow::Result<()> {
panic!("nothing is signed under a pairing the host does not hold")
});
assert!(refused.is_err());
assert_eq!(staged_hosts(&config, now).len(), 1, "the record is left");
let (started, racing) = std::sync::mpsc::channel();
let promoted = promote_staged_host(&config, &fp, &compared.transcript_id, || {
let config = config.clone();
let race = std::thread::spawn(move || {
started.send(()).unwrap();
let commit = PairingStep::Commit(pairing_commitment(&HUB, &host, &[8; 16]));
pair_staged_host(&config, &host, &HUB, commit, [6; 16], now)
});
racing.recv().unwrap();
std::thread::sleep(std::time::Duration::from_millis(200));
Ok(race)
})
.unwrap();
assert!(
promoted.join().unwrap().is_err(),
"the racing step waited, then found the host promoted"
);
assert!(staged_hosts(&config, now).is_empty(), "consumed");
}
fn at(secs: u64) -> std::time::SystemTime {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(secs)
}
#[test]
fn a_staged_host_is_listed_until_it_expires_and_gone_once_unstaged() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let pubkey = [7u8; 32];
stage_host(&config, &pubkey, "nuc1", &HUB, at(1_000)).unwrap();
let staged = staged_hosts(&config, at(1_000));
assert_eq!(staged.len(), 1);
assert_eq!(staged[0].peer_label, "nuc1");
assert_eq!(
staged[0].peer_agent_fingerprint,
agent_fingerprint_of_pubkey(&pubkey)
);
assert_eq!(decode_agent_pubkey(&staged[0].peer_pubkey), Some(pubkey));
let ttl = STAGED_HOST_TTL.as_secs();
assert_eq!(staged_hosts(&config, at(1_000 + ttl - 1)).len(), 1);
assert!(staged_hosts(&config, at(1_000 + ttl)).is_empty(), "expired");
stage_host(&config, &pubkey, "nuc1", &HUB, at(1_000 + ttl)).unwrap();
assert_eq!(staged_hosts(&config, at(1_000 + ttl)).len(), 1, "refreshed");
unstage_host(&config, &staged[0].peer_agent_fingerprint).unwrap();
assert!(staged_hosts(&config, at(1_000 + ttl)).is_empty());
unstage_host(&config, &staged[0].peer_agent_fingerprint).unwrap();
}
#[test]
fn a_label_that_is_not_an_instance_name_is_not_staged() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
for label in ["", "nuc 1", "nuc\n1", "../x", &"a".repeat(81)] {
assert!(
stage_host(&config, &[7u8; 32], label, &HUB, at(1)).is_err(),
"{label:?}"
);
}
assert!(staged_hosts(&config, at(1)).is_empty());
}
#[test]
fn a_staged_file_not_named_for_its_own_key_is_skipped() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
stage_host(&config, &[7u8; 32], "nuc1", &HUB, at(1)).unwrap();
let staged_dir = staged_dir(&config);
let genuine = staged_dir.join(format!("{}.toml", agent_fingerprint_of_pubkey(&[7u8; 32])));
let other = agent_fingerprint_of_pubkey(&[8u8; 32]);
std::fs::rename(genuine, staged_dir.join(format!("{other}.toml"))).unwrap();
assert!(staged_hosts(&config, at(1)).is_empty());
}
#[test]
fn an_edited_proposal_no_longer_matches_its_content_id() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
stage_host(&config, &[7u8; 32], "nuc1", &HUB, at(1)).unwrap();
let path =
staged_dir(&config).join(format!("{}.toml", agent_fingerprint_of_pubkey(&[7u8; 32])));
let text = std::fs::read_to_string(&path).unwrap();
assert!(text.contains("\"nuc1\""), "{text}");
std::fs::write(&path, text.replace("\"nuc1\"", "\"nuc9\"")).unwrap();
assert!(
staged_hosts(&config, at(1)).is_empty(),
"an edited label is not trusted"
);
}
#[test]
fn a_proposal_observed_in_the_future_is_not_live() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
stage_host(&config, &[7u8; 32], "nuc1", &HUB, at(1_000)).unwrap();
assert!(staged_hosts(&config, at(999)).is_empty());
assert_eq!(staged_hosts(&config, at(1_000)).len(), 1);
}
#[test]
fn staging_reaps_dead_entries_and_caps_the_store() {
let dir = TempDir::new().unwrap();
let config = dir.path().join("config.toml");
let key = |i: usize| {
let mut k = [0u8; 32];
k[..8].copy_from_slice(&(i as u64).to_be_bytes());
k
};
for i in 0..MAX_STAGED_HOSTS {
stage_host(&config, &key(i), &format!("h{i}"), &HUB, at(1)).unwrap();
}
assert!(
stage_host(&config, &key(MAX_STAGED_HOSTS), "late", &HUB, at(1)).is_err(),
"a new host past the cap is refused"
);
stage_host(&config, &key(0), "h0", &HUB, at(1)).unwrap();
let files = || std::fs::read_dir(staged_dir(&config)).unwrap().count();
assert_eq!(files(), MAX_STAGED_HOSTS);
let later = at(1 + STAGED_HOST_TTL.as_secs());
stage_host(&config, &key(MAX_STAGED_HOSTS), "late", &HUB, later).unwrap();
assert_eq!(files(), 1, "expired entries are reaped when staging");
}
}