1use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
29use serde::{Deserialize, Serialize};
30
31use crate::jekt_sign::{decode_public_key_b64, ed25519_sign_b64, ed25519_verify_b64, wan_instance_id, FIELD_SEP};
32
33const PRESENCE_DOMAIN: &str = "amx-install-presence-v1";
36const AGENT_FIELD_SEP: char = '\u{2}';
38const AGENT_SEP: char = '\u{3}';
40
41pub const PRESENCE_VERSION: u32 = 2;
43pub const PRESENCE_VERSION_V1: u32 = 1;
45pub const PRESENCE_VERSION_GOODBYE: u32 = 3;
47pub const PRESENCE_VERSION_LINES: u32 = 4;
49pub const MAX_PRESENCE_LINE_CHARS: usize = 200;
51pub const MAX_PRESENCE_LINE_BYTES: usize = 16 * 1024;
54pub const PRESENCE_STATES: [&str; 5] = ["working", "waiting", "idle", "stopped", "error"];
56pub const MAX_PRESENCE_AGENTS: usize = 200;
58pub const MAX_PRESENCE_NAME_CHARS: usize = 128;
60pub const MAX_PRESENCE_VERSION_CHARS: usize = 64;
61pub const MAX_CHANNELS_RUNNING: u32 = 99;
62const MAX_OS_LEN: usize = 16;
63
64#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
68pub struct PresenceAgent {
69 pub name: String,
70 pub kind: String,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub state: Option<String>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub line: Option<String>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
80pub struct InstallPresence {
81 pub v: u32,
82 pub instance_id: String,
83 pub instance_public_key: String,
84 pub hostname: String,
85 pub channel: String,
86 pub os: String,
87 pub version: String,
88 pub channels_running: u32,
89 pub agents: Vec<PresenceAgent>,
90 pub published_at_ms: u64,
91 #[serde(default, skip_serializing_if = "is_false")]
95 pub gone: bool,
96 pub sig: String,
97}
98
99fn is_false(b: &bool) -> bool {
100 !*b
101}
102
103fn has_control_char(s: &str) -> bool {
104 s.chars().any(|c| u32::from(c) < 0x20)
105}
106
107fn text_in_bounds(s: &str, max_chars: usize) -> bool {
108 (1..=max_chars).contains(&s.chars().count()) && !has_control_char(s)
109}
110
111fn os_in_bounds(os: &str) -> bool {
113 os.is_empty()
114 || (os.len() <= MAX_OS_LEN
115 && os.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_' || b == b'-'))
116}
117
118fn agent_in_bounds(agent: &PresenceAgent) -> bool {
119 text_in_bounds(&agent.name, MAX_PRESENCE_NAME_CHARS)
120 && (agent.kind == "host" || agent.kind == "container")
121 && agent.state.as_deref().is_none_or(|s| PRESENCE_STATES.contains(&s))
122 && agent.line.as_deref().is_none_or(|l| text_in_bounds(l, MAX_PRESENCE_LINE_CHARS))
123}
124
125pub fn canonical_agents(agents: impl IntoIterator<Item = PresenceAgent>) -> Vec<PresenceAgent> {
130 let mut keyed: Vec<(String, PresenceAgent)> = agents
131 .into_iter()
132 .filter(agent_in_bounds)
133 .map(|a| (a.name.to_lowercase(), a))
134 .collect();
135 keyed.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.name.cmp(&b.1.name)));
136 keyed.dedup_by(|later, earlier| later.1.name == earlier.1.name);
137 keyed.truncate(MAX_PRESENCE_AGENTS);
138 keyed.into_iter().map(|(_, a)| a).collect()
139}
140
141fn fit_lines(mut agents: Vec<PresenceAgent>) -> Vec<PresenceAgent> {
144 let mut left = MAX_PRESENCE_LINE_BYTES;
145 for a in &mut agents {
146 match a.line.as_ref().map(String::len) {
147 Some(n) if n <= left => left -= n,
148 Some(_) => a.line = None,
149 None => {}
150 }
151 }
152 agents
153}
154
155impl InstallPresence {
156 #[allow(clippy::too_many_arguments)]
162 pub fn sign(
163 instance_private_key: &[u8],
164 hostname: &str,
165 channel: &str,
166 os: &str,
167 version: &str,
168 channels_running: u32,
169 agents: impl IntoIterator<Item = PresenceAgent>,
170 published_at_ms: u64,
171 ) -> Option<Self> {
172 Self::sign_version(
173 PRESENCE_VERSION,
174 instance_private_key,
175 hostname,
176 channel,
177 os,
178 version,
179 channels_running,
180 agents,
181 published_at_ms,
182 )
183 }
184
185 #[allow(clippy::too_many_arguments)]
190 pub fn sign_version(
191 v: u32,
192 instance_private_key: &[u8],
193 hostname: &str,
194 channel: &str,
195 os: &str,
196 version: &str,
197 channels_running: u32,
198 agents: impl IntoIterator<Item = PresenceAgent>,
199 published_at_ms: u64,
200 ) -> Option<Self> {
201 let seed: [u8; 32] = instance_private_key.try_into().ok()?;
202 let public_key = ed25519_dalek::SigningKey::from_bytes(&seed).verifying_key().to_bytes();
203 let gone = v == PRESENCE_VERSION_GOODBYE;
204 let agents = agents.into_iter().map(|mut a| {
205 if v == PRESENCE_VERSION_V1 || gone {
206 a.state = None;
207 }
208 if v < PRESENCE_VERSION_LINES {
209 a.line = None;
210 }
211 a
212 });
213 let mut record = Self {
214 v,
215 instance_id: wan_instance_id(&public_key),
216 instance_public_key: BASE64.encode(public_key),
217 hostname: hostname.to_string(),
218 channel: channel.to_string(),
219 os: os.to_string(),
220 version: version.to_string(),
221 channels_running: channels_running.clamp(1, MAX_CHANNELS_RUNNING),
222 agents: fit_lines(canonical_agents(agents)),
223 published_at_ms,
224 gone,
225 sig: String::new(),
226 };
227 if !record.in_bounds() {
228 return None;
229 }
230 record.sig = ed25519_sign_b64(&seed, &record.signed_material())?;
231 Some(record)
232 }
233
234 pub fn signed_material(&self) -> String {
241 let agents = self
242 .agents
243 .iter()
244 .map(|a| {
245 let state = a.state.as_deref().unwrap_or("");
246 match self.v {
247 PRESENCE_VERSION_V1 => format!("{}{AGENT_FIELD_SEP}{}", a.name, a.kind),
248 PRESENCE_VERSION_LINES => {
249 let line = a.line.as_deref().unwrap_or("");
250 format!("{}{AGENT_FIELD_SEP}{}{AGENT_FIELD_SEP}{state}{AGENT_FIELD_SEP}{line}", a.name, a.kind)
251 }
252 _ => format!("{}{AGENT_FIELD_SEP}{}{AGENT_FIELD_SEP}{state}", a.name, a.kind),
253 }
254 })
255 .collect::<Vec<_>>()
256 .join(&AGENT_SEP.to_string());
257 let gone = match (self.v >= PRESENCE_VERSION_GOODBYE, self.gone) {
258 (true, true) => format!("{FIELD_SEP}1"),
259 (true, false) => FIELD_SEP.to_string(),
260 (false, _) => String::new(),
261 };
262 format!(
263 "{PRESENCE_DOMAIN}{FIELD_SEP}{}{FIELD_SEP}{}{FIELD_SEP}{}{FIELD_SEP}{}{FIELD_SEP}{}{FIELD_SEP}\
264 {}{FIELD_SEP}{}{FIELD_SEP}{agents}{FIELD_SEP}{}{gone}",
265 self.v,
266 self.instance_id,
267 self.hostname,
268 self.channel,
269 self.os,
270 self.version,
271 self.channels_running,
272 self.published_at_ms
273 )
274 }
275
276 pub fn in_bounds(&self) -> bool {
282 let stateless = || self.agents.iter().all(|a| a.state.is_none());
283 let lineless = || self.agents.iter().all(|a| a.line.is_none());
284 let version_ok = match self.v {
285 PRESENCE_VERSION_V1 => !self.gone && stateless() && lineless(),
286 PRESENCE_VERSION => !self.gone && lineless(),
287 PRESENCE_VERSION_GOODBYE => lineless() && (!self.gone || stateless()),
288 PRESENCE_VERSION_LINES => !self.gone || (stateless() && lineless()),
289 _ => false,
290 };
291 version_ok
292 && text_in_bounds(&self.hostname, MAX_PRESENCE_NAME_CHARS)
293 && text_in_bounds(&self.channel, MAX_PRESENCE_NAME_CHARS)
294 && text_in_bounds(&self.version, MAX_PRESENCE_VERSION_CHARS)
295 && os_in_bounds(&self.os)
296 && (1..=MAX_CHANNELS_RUNNING).contains(&self.channels_running)
297 && self.agents.len() <= MAX_PRESENCE_AGENTS
298 && self.agents == canonical_agents(self.agents.iter().cloned())
299 }
300
301 pub fn verify(&self) -> bool {
304 let Some(public_key) = decode_public_key_b64(&self.instance_public_key) else { return false };
305 self.in_bounds()
306 && wan_instance_id(&public_key) == self.instance_id
307 && ed25519_verify_b64(&public_key, &self.signed_material(), &self.sig)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314
315 fn agent(name: &str, kind: &str) -> PresenceAgent {
316 PresenceAgent { name: name.to_string(), kind: kind.to_string(), state: None, line: None }
317 }
318
319 fn agent_in(name: &str, kind: &str, state: &str) -> PresenceAgent {
320 PresenceAgent { state: Some(state.to_string()), ..agent(name, kind) }
321 }
322
323 fn hex(bytes: &[u8]) -> String {
324 bytes.iter().map(|b| format!("{b:02x}")).collect()
325 }
326
327 const SEED_HEX: &str = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20";
330 const MATERIAL_HEX: &str = "\
331 616d782d696e7374616c6c2d70726573656e63652d76310131016d7733616d3436773577656578346134667172633361766e\
332 7561016e61726b6f01737461626c650177696e646f777301302e35392e31310133016167656e747802636f6e7461696e6572\
333 034167656e745902686f73740343616d70657202686f73740131373931333532343933333838";
334 const SIG: &str = "7FxOua+TWFMp3fwOP/mGr+YXjgLfKESklzZ3odiOu5WH5QrstJlk4JSV5WrkMo9rnywbYWdFhRZ7KySij/GNAg==";
335
336 fn seed() -> [u8; 32] {
337 let mut out = [0u8; 32];
338 for (i, b) in out.iter_mut().enumerate() {
339 *b = u8::from_str_radix(&SEED_HEX[i * 2..i * 2 + 2], 16).unwrap();
340 }
341 out
342 }
343
344 fn vector_record() -> InstallPresence {
345 InstallPresence::sign_version(
346 PRESENCE_VERSION_V1,
347 &seed(),
348 "narko",
349 "stable",
350 "windows",
351 "0.59.11",
352 3,
353 [agent("Camper", "host"), agent("agentx", "container"), agent("AgentY", "host")],
355 1_791_352_493_388,
356 )
357 .expect("the vector is in bounds")
358 }
359
360 #[test]
361 fn the_shared_vector_signs_byte_for_byte() {
362 let record = vector_record();
363 assert_eq!(hex(record.signed_material().as_bytes()), MATERIAL_HEX);
364 assert_eq!(record.sig, SIG);
365 assert_eq!(
366 serde_json::to_value(&record).unwrap(),
367 serde_json::json!({
368 "v": 1,
369 "instance_id": "mw3am46w5weex4a4fqrc3avnua",
370 "instance_public_key": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=",
371 "hostname": "narko",
372 "channel": "stable",
373 "os": "windows",
374 "version": "0.59.11",
375 "channels_running": 3,
376 "agents": [
377 {"name": "agentx", "kind": "container"},
378 {"name": "AgentY", "kind": "host"},
379 {"name": "Camper", "kind": "host"},
380 ],
381 "published_at_ms": 1_791_352_493_388u64,
382 "sig": SIG,
383 })
384 );
385 assert!(record.verify());
386 let back: InstallPresence = serde_json::from_str(&serde_json::to_string(&record).unwrap()).unwrap();
388 assert!(back.verify());
389 }
390
391 const MATERIAL_V2_HEX: &str = "\
394 616d782d696e7374616c6c2d70726573656e63652d76310132016d7733616d3436773577656578346134667172633361766e\
395 7561016e61726b6f01737461626c650177696e646f777301302e35392e31310133016167656e747802636f6e7461696e6572\
396 02776f726b696e67034167656e745902686f7374020343616d70657202686f73740269646c65013137393133353234393333\
397 3838";
398 const SIG_V2: &str = "ELDWTLT+BEoxMn/o/AsYYVbGLgIAbJjDPuQ6/AbAZ8fk+jqphiEtEn/c/5ZKjHJZdQF+hxIKzmnq6gLZzmWDDw==";
399
400 fn vector_v2_record() -> InstallPresence {
401 InstallPresence::sign(
402 &seed(),
403 "narko",
404 "stable",
405 "windows",
406 "0.59.11",
407 3,
408 [agent_in("Camper", "host", "idle"), agent_in("agentx", "container", "working"), agent("AgentY", "host")],
409 1_791_352_493_388,
410 )
411 .expect("the vector is in bounds")
412 }
413
414 #[test]
415 fn the_v2_shared_vector_signs_byte_for_byte() {
416 let record = vector_v2_record();
417 assert_eq!(record.v, 2);
418 assert_eq!(hex(record.signed_material().as_bytes()), MATERIAL_V2_HEX);
419 assert_eq!(record.sig, SIG_V2);
420 assert_eq!(
421 serde_json::to_value(&record).unwrap(),
422 serde_json::json!({
423 "v": 2,
424 "instance_id": "mw3am46w5weex4a4fqrc3avnua",
425 "instance_public_key": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=",
426 "hostname": "narko",
427 "channel": "stable",
428 "os": "windows",
429 "version": "0.59.11",
430 "channels_running": 3,
431 "agents": [
432 {"name": "agentx", "kind": "container", "state": "working"},
433 {"name": "AgentY", "kind": "host"},
434 {"name": "Camper", "kind": "host", "state": "idle"},
435 ],
436 "published_at_ms": 1_791_352_493_388u64,
437 "sig": SIG_V2,
438 })
439 );
440 assert!(record.verify());
441 let back: InstallPresence = serde_json::from_str(&serde_json::to_string(&record).unwrap()).unwrap();
442 assert_eq!(back, record);
443 assert!(back.verify());
444 }
445
446 const MATERIAL_V3_HEX: &str = "\
449 616d782d696e7374616c6c2d70726573656e63652d76310133016d7733616d3436773577656578346134667172633361766e\
450 7561016e61726b6f01737461626c650177696e646f777301302e35392e31310133016167656e747802636f6e7461696e6572\
451 02034167656e745902686f7374020343616d70657202686f73740201313739313335323439333338380131";
452 const SIG_V3: &str = "CihvXkUTZu79ykAvxqgN2WOkOw5t+FsYJ5YRF3I94MiHZCsi301grrVNLErajob9g+AEd2J2e2Kt1ulOR5KpCQ==";
453
454 fn vector_v3_record() -> InstallPresence {
455 InstallPresence::sign_version(
456 PRESENCE_VERSION_GOODBYE,
457 &seed(),
458 "narko",
459 "stable",
460 "windows",
461 "0.59.11",
462 3,
463 [agent_in("Camper", "host", "idle"), agent_in("agentx", "container", "working"), agent("AgentY", "host")],
464 1_791_352_493_388,
465 )
466 .expect("the vector is in bounds")
467 }
468
469 #[test]
470 fn the_v3_goodbye_vector_signs_byte_for_byte() {
471 let record = vector_v3_record();
472 assert_eq!(hex(record.signed_material().as_bytes()), MATERIAL_V3_HEX);
473 assert_eq!(record.sig, SIG_V3);
474 assert_eq!(
475 serde_json::to_value(&record).unwrap(),
476 serde_json::json!({
477 "v": 3,
478 "instance_id": "mw3am46w5weex4a4fqrc3avnua",
479 "instance_public_key": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=",
480 "hostname": "narko",
481 "channel": "stable",
482 "os": "windows",
483 "version": "0.59.11",
484 "channels_running": 3,
485 "agents": [
486 {"name": "agentx", "kind": "container"},
487 {"name": "AgentY", "kind": "host"},
488 {"name": "Camper", "kind": "host"},
489 ],
490 "published_at_ms": 1_791_352_493_388u64,
491 "gone": true,
492 "sig": SIG_V3,
493 }),
494 "every state absent, gone set"
495 );
496 assert!(record.verify());
497 let back: InstallPresence = serde_json::from_str(&serde_json::to_string(&record).unwrap()).unwrap();
498 assert_eq!(back, record);
499 assert!(back.verify());
500 let material = record.signed_material();
502 assert!(material.starts_with(&format!("{PRESENCE_DOMAIN}{FIELD_SEP}3{FIELD_SEP}")), "{material:?}");
503 assert!(material.ends_with(&format!("1791352493388{FIELD_SEP}1")), "{material:?}");
504 assert_eq!(material.split(FIELD_SEP).count(), 11);
505 }
506
507 const SIG_V3_NOT_GONE: &str =
510 "/CV2c+4Cc7FJvrbhbzh3fhUnSS6388P/mY2fy8m3FDCrgcauiLrssFCkZh6Y5LemxpbP+UDdRi04Pu4iwEUiCw==";
511
512 #[test]
513 fn a_v3_record_without_gone_signs_an_empty_last_field() {
514 let mut record = vector_v3_record();
515 record.gone = false;
516 let material = record.signed_material();
517 assert!(material.ends_with(&format!("1791352493388{FIELD_SEP}")), "{material:?}");
518 assert_eq!(material.split(FIELD_SEP).count(), 11);
519 assert!(!record.verify(), "the goodbye's signature covers gone");
520 record.sig = ed25519_sign_b64(&seed(), &material).unwrap();
521 assert_eq!(record.sig, SIG_V3_NOT_GONE);
522 assert!(record.verify());
523 let wire = serde_json::to_value(&record).unwrap();
524 assert!(wire.get("gone").is_none(), "{wire}");
525 let back: InstallPresence = serde_json::from_value(wire).unwrap();
526 assert!(back.verify());
527 let mut stateful = record.clone();
529 stateful.agents[0].state = Some("working".into());
530 assert!(stateful.in_bounds());
531 }
532
533 #[test]
534 fn v1_and_v2_keep_ten_fields() {
535 assert_eq!(vector_record().signed_material().split(FIELD_SEP).count(), 10);
536 assert_eq!(vector_v2_record().signed_material().split(FIELD_SEP).count(), 10);
537 }
538
539 #[test]
540 fn gone_is_signed_and_only_a_v3_record_may_say_it() {
541 let goodbye = vector_v3_record();
542 let tampered: Vec<(&str, Box<dyn Fn(&mut InstallPresence)>)> = vec![
543 ("gone cleared", Box::new(|r| r.gone = false)),
544 ("a state added", Box::new(|r| r.agents[0].state = Some("working".into()))),
545 ("version lowered to 2", Box::new(|r| r.v = 2)),
546 ];
547 for (what, change) in tampered {
548 let mut r = goodbye.clone();
549 change(&mut r);
550 assert!(!r.verify(), "{what} but the goodbye still verifies");
551 }
552 for mut live in [vector_record(), vector_v2_record()] {
554 live.gone = true;
555 assert!(!live.in_bounds() && !live.verify(), "v{} with gone", live.v);
556 }
557 let v2 = serde_json::to_value(vector_v2_record()).unwrap();
559 assert!(v2.get("gone").is_none(), "{v2}");
560 let back: InstallPresence = serde_json::from_value(v2).unwrap();
561 assert!(!back.gone && back.verify());
562 }
563
564 #[test]
565 fn a_changed_added_or_removed_state_fails_verification() {
566 let good = vector_v2_record();
567 let tampered: Vec<(&str, Box<dyn Fn(&mut InstallPresence)>)> = vec![
568 ("state changed", Box::new(|r| r.agents[0].state = Some("idle".into()))),
569 ("state added", Box::new(|r| r.agents[1].state = Some("working".into()))),
570 ("state removed", Box::new(|r| r.agents[2].state = None)),
571 ("version lowered", Box::new(|r| r.v = 1)),
572 ("unknown version", Box::new(|r| r.v = 3)),
573 ];
574 for (what, change) in tampered {
575 let mut r = good.clone();
576 change(&mut r);
577 assert!(!r.verify(), "{what} but the record still verifies");
578 }
579 }
580
581 #[test]
582 fn a_v1_record_carries_no_state() {
583 let r = InstallPresence::sign_version(
584 PRESENCE_VERSION_V1,
585 &seed(),
586 "narko",
587 "stable",
588 "windows",
589 "1",
590 1,
591 [agent_in("AgentY", "host", "working")],
592 1,
593 )
594 .unwrap();
595 assert_eq!(r.agents, vec![agent("AgentY", "host")], "the state is dropped, not signed");
596 assert!(r.verify());
597 let mut smuggled = r.clone();
598 smuggled.agents[0].state = Some("working".into());
599 assert!(!smuggled.verify(), "a v1 signature does not cover a state");
600 }
601
602 #[test]
603 fn an_unknown_state_leaves_the_agent_out() {
604 assert_eq!(
605 canonical_agents([agent_in("a", "host", "busy"), agent_in("b", "host", "idle")]),
606 vec![agent_in("b", "host", "idle")]
607 );
608 }
609
610 #[test]
611 fn a_change_to_any_signed_field_fails_verification() {
612 let good = vector_record();
613 let tampered: Vec<(&str, Box<dyn Fn(&mut InstallPresence)>)> = vec![
614 ("hostname", Box::new(|r| r.hostname = "area54".into())),
615 ("channel", Box::new(|r| r.channel = "dev".into())),
616 ("os", Box::new(|r| r.os = "linux".into())),
617 ("version", Box::new(|r| r.version = "0.59.12".into())),
618 ("channels_running", Box::new(|r| r.channels_running = 4)),
619 ("published_at_ms", Box::new(|r| r.published_at_ms += 1)),
620 ("agent kind", Box::new(|r| r.agents[0].kind = "host".into())),
621 ("agent dropped", Box::new(|r| {
622 r.agents.pop();
623 })),
624 ("instance_id", Box::new(|r| r.instance_id = "a".repeat(26))),
625 ("sig", Box::new(|r| r.sig = BASE64.encode([0u8; 64]))),
626 ];
627 for (what, change) in tampered {
628 let mut r = good.clone();
629 change(&mut r);
630 assert!(!r.verify(), "{what} changed but the record still verifies");
631 }
632 }
633
634 #[test]
635 fn another_key_cannot_claim_the_instance() {
636 let mut r = vector_record();
637 let other = InstallPresence::sign(&[9u8; 32], "narko", "stable", "windows", "0.59.11", 3, [], 1).unwrap();
638 r.instance_public_key = other.instance_public_key;
639 assert!(!r.verify(), "the id no longer names the key");
640 }
641
642 #[test]
643 fn no_agents_signs_an_empty_list_field() {
644 let r = InstallPresence::sign(&seed(), "narko", "stable", "", "0.59.11", 1, [], 5).unwrap();
645 assert!(r.agents.is_empty());
646 assert!(r.signed_material().contains(&format!("{FIELD_SEP}{FIELD_SEP}5")));
647 assert!(r.verify());
648 }
649
650 #[test]
651 fn agents_out_of_bounds_are_left_out_and_the_list_is_capped() {
652 let agents = canonical_agents([
653 agent("ok", "host"),
654 agent("bad\u{1}name", "host"),
655 agent("tab\tname", "host"),
656 agent("", "host"),
657 agent(&"x".repeat(MAX_PRESENCE_NAME_CHARS + 1), "host"),
658 agent("weird", "vm"),
659 agent("ok", "container"),
660 ]);
661 assert_eq!(agents, vec![agent("ok", "host")], "each name once, the first kept");
662
663 let many: Vec<_> = (0..MAX_PRESENCE_AGENTS + 10).map(|i| agent(&format!("a{i:03}"), "host")).collect();
664 let r = InstallPresence::sign(&seed(), "narko", "stable", "windows", "1", 1, many, 1).unwrap();
665 assert_eq!(r.agents.len(), MAX_PRESENCE_AGENTS);
666 assert!(r.verify());
667 }
668
669 #[test]
670 fn a_top_level_field_out_of_bounds_is_not_signed() {
671 let sign = |hostname: &str, channel: &str, os: &str, version: &str| {
672 InstallPresence::sign(&seed(), hostname, channel, os, version, 1, [], 1)
673 };
674 assert!(sign("", "stable", "windows", "1").is_none());
675 assert!(sign("nar\nko", "stable", "windows", "1").is_none());
676 assert!(sign(&"h".repeat(MAX_PRESENCE_NAME_CHARS + 1), "stable", "windows", "1").is_none());
677 assert!(sign("narko", "", "windows", "1").is_none());
678 assert!(sign("narko", "stable", "Windows", "1").is_none());
679 assert!(sign("narko", "stable", "windows", "").is_none());
680 assert!(sign("narko", "stable", "windows", &"9".repeat(MAX_PRESENCE_VERSION_CHARS + 1)).is_none());
681 assert!(InstallPresence::sign(&[1u8; 31], "narko", "stable", "windows", "1", 1, [], 1).is_none());
682 }
683
684 #[test]
685 fn channels_running_is_clamped() {
686 let at = |n| InstallPresence::sign(&seed(), "narko", "stable", "windows", "1", n, [], 1).unwrap().channels_running;
687 assert_eq!(at(0), 1);
688 assert_eq!(at(3), 3);
689 assert_eq!(at(500), MAX_CHANNELS_RUNNING);
690 }
691
692 #[test]
693 fn agents_out_of_canonical_order_do_not_verify() {
694 let mut r = vector_record();
695 r.agents.reverse();
696 assert!(!r.verify());
697 }
698
699 const MATERIAL_V4_HEX: &str = "\
702 616d782d696e7374616c6c2d70726573656e63652d76310134016d7733616d3436773577656578346134667172633361766e\
703 7561016e61726b6f01737461626c650177696e646f777301302e35392e31310133016167656e747802636f6e7461696e6572\
704 02776f726b696e6702466978696e6720746865206c6f67696e2072616365034167656e745902686f737402020343616d7065\
705 7202686f73740269646c6502526576696577696e672074686520636166c3a9206d656e750131373931333532343933333838\
706 01";
707 const SIG_V4: &str = "uQEoRZnWxRgCTX/hCtx8zCwqtrsG7DleLCnBjWs7zGWOY2fcTtIgOavDJQSyPe7MH+MlntCG5hGMie17k+FVCA==";
708
709 fn with_line(a: PresenceAgent, line: &str) -> PresenceAgent {
710 PresenceAgent { line: Some(line.to_string()), ..a }
711 }
712
713 fn vector_v4_record() -> InstallPresence {
714 InstallPresence::sign_version(
715 PRESENCE_VERSION_LINES,
716 &seed(),
717 "narko",
718 "stable",
719 "windows",
720 "0.59.11",
721 3,
722 [
723 with_line(agent_in("Camper", "host", "idle"), "Reviewing the café menu"),
724 with_line(agent_in("agentx", "container", "working"), "Fixing the login race"),
725 agent("AgentY", "host"),
726 ],
727 1_791_352_493_388,
728 )
729 .expect("the vector is in bounds")
730 }
731
732 #[test]
733 fn the_v4_vector_signs_byte_for_byte() {
734 let record = vector_v4_record();
735 assert_eq!(hex(record.signed_material().as_bytes()), MATERIAL_V4_HEX);
736 assert_eq!(record.sig, SIG_V4);
737 assert_eq!(
738 serde_json::to_value(&record).unwrap(),
739 serde_json::json!({
740 "v": 4,
741 "instance_id": "mw3am46w5weex4a4fqrc3avnua",
742 "instance_public_key": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=",
743 "hostname": "narko",
744 "channel": "stable",
745 "os": "windows",
746 "version": "0.59.11",
747 "channels_running": 3,
748 "agents": [
749 {"name": "agentx", "kind": "container", "state": "working", "line": "Fixing the login race"},
750 {"name": "AgentY", "kind": "host"},
751 {"name": "Camper", "kind": "host", "state": "idle", "line": "Reviewing the café menu"},
752 ],
753 "published_at_ms": 1_791_352_493_388u64,
754 "sig": SIG_V4,
755 })
756 );
757 assert!(record.verify());
758 let back: InstallPresence = serde_json::from_str(&serde_json::to_string(&record).unwrap()).unwrap();
759 assert_eq!(back, record);
760 assert!(back.verify());
761 }
762
763 #[test]
764 fn a_line_needs_v4_and_a_live_record() {
765 let lined = || with_line(agent("AgentY", "host"), "Building");
766 for v in [PRESENCE_VERSION_V1, PRESENCE_VERSION, PRESENCE_VERSION_GOODBYE] {
767 let r = InstallPresence::sign_version(v, &seed(), "narko", "stable", "", "1", 1, [lined()], 1).unwrap();
768 assert_eq!(r.agents[0].line, None, "v{v} drops the line");
769 let mut forged = r.clone();
770 forged.agents[0].line = Some("Building".into());
771 assert!(!forged.in_bounds(), "v{v} can't carry a line its signature doesn't cover");
772 }
773 let mut goodbye = vector_v4_record();
774 goodbye.gone = true;
775 assert!(!goodbye.in_bounds(), "a v4 goodbye carries no line or state");
776 }
777
778 #[test]
779 fn an_agent_with_an_out_of_bounds_line_is_left_out() {
780 let ok = with_line(agent("Ok", "host"), "fine");
781 let control = with_line(agent("Ctl", "host"), "two
782lines");
783 let long = with_line(agent("Long", "host"), &"x".repeat(MAX_PRESENCE_LINE_CHARS + 1));
784 let empty = with_line(agent("Empty", "host"), "");
785 let names: Vec<String> = canonical_agents([ok, control, long, empty]).into_iter().map(|a| a.name).collect();
786 assert_eq!(names, ["Ok"]);
787 }
788
789 #[test]
790 fn lines_past_the_byte_budget_are_dropped_in_order() {
791 let line = "x".repeat(MAX_PRESENCE_LINE_CHARS);
792 let n = MAX_PRESENCE_LINE_BYTES / line.len() + 3;
793 let agents = (0..n).map(|i| with_line(agent(&format!("a{i:03}"), "host"), &line));
794 let r = InstallPresence::sign_version(PRESENCE_VERSION_LINES, &seed(), "narko", "stable", "", "1", 1, agents, 1).unwrap();
795 let kept = r.agents.iter().filter(|a| a.line.is_some()).count();
796 assert_eq!(kept, MAX_PRESENCE_LINE_BYTES / line.len());
797 assert!(r.agents[..kept].iter().all(|a| a.line.is_some()), "the first ones keep theirs");
798 assert_eq!(r.agents.len(), n, "every agent is still listed");
799 assert!(r.verify());
800 assert!(serde_json::to_string(&r).unwrap().len() < 24 * 1024, "the lines are most of it");
801 }
802}