1use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
37use ed25519_dalek::{Signature, Verifier, VerifyingKey};
38use hmac::{Hmac, Mac};
39use sha2::Sha256;
40
41type HmacSha256 = Hmac<Sha256>;
42
43pub(crate) const FIELD_SEP: char = '\u{1}';
51
52fn signed_material(msgid: &str, source_agent: &str, target_agent: &str, ts_secs: i64, message: &str) -> String {
53 format!("{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}{target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}")
54}
55
56pub fn decode_key(b64: &str) -> Option<Vec<u8>> {
62 BASE64.decode(b64).ok()
63}
64
65pub fn sign_jekt(key: &[u8], msgid: &str, source_agent: &str, target_agent: &str, ts_secs: i64, message: &str) -> String {
69 let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
70 let mut mac = HmacSha256::new_from_slice(key).expect("HMAC accepts a key of any length");
71 mac.update(material.as_bytes());
72 BASE64.encode(mac.finalize().into_bytes())
73}
74
75pub fn verify_jekt(
79 key: &[u8],
80 msgid: &str,
81 source_agent: &str,
82 target_agent: &str,
83 ts_secs: i64,
84 message: &str,
85 sig_b64: &str,
86) -> bool {
87 let Ok(sig) = BASE64.decode(sig_b64) else { return false };
88 let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
89 let Ok(mut mac) = HmacSha256::new_from_slice(key) else { return false };
90 mac.update(material.as_bytes());
91 mac.verify_slice(&sig).is_ok()
92}
93
94fn reagent_public_key(key_id: &str) -> Option<[u8; 32]> {
133 match key_id {
134 "reagent-v1-dev" => Some([
135 104, 98, 241, 120, 99, 50, 230, 185, 228, 117, 241, 110, 130, 85, 252, 75, 141, 152,
136 224, 92, 125, 154, 123, 40, 96, 149, 214, 172, 94, 120, 116, 200,
137 ]),
138 "reagent-v1" => Some([
139 185, 72, 99, 0, 119, 36, 15, 50, 117, 0, 134, 93, 66, 39, 65, 21, 64, 229, 3, 157,
140 238, 188, 79, 40, 74, 42, 99, 108, 28, 101, 243, 205,
141 ]),
142 _ => None,
143 }
144}
145
146pub fn is_reagent_trusted_signing_key(key_id: &str) -> bool {
155 key_id == "reagent-v1"
156}
157
158pub fn verify_reagent_jekt(
169 key_id: &str,
170 msgid: &str,
171 source_agent: &str,
172 target_agent: &str,
173 ts_secs: i64,
174 message: &str,
175 sig_b64: &str,
176) -> bool {
177 let Some(pubkey_bytes) = reagent_public_key(key_id) else { return false };
178 let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_bytes) else { return false };
179 let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
180 let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
181 let signature = Signature::from_bytes(&sig_arr);
182 let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
183 verifying_key.verify(material.as_bytes(), &signature).is_ok()
184}
185
186pub fn verify_trusted_reagent_jekt(
190 key_id: &str,
191 msgid: &str,
192 source_agent: &str,
193 target_agent: &str,
194 ts_secs: i64,
195 message: &str,
196 sig_b64: &str,
197) -> bool {
198 is_reagent_trusted_signing_key(key_id)
199 && verify_reagent_jekt(key_id, msgid, source_agent, target_agent, ts_secs, message, sig_b64)
200}
201
202pub fn generate_lan_keypair(seed: [u8; 32]) -> ([u8; 32], [u8; 32]) {
224 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
225 (signing_key.verifying_key().to_bytes(), seed)
226}
227
228pub fn sign_lan_jekt(
236 private_key: &[u8],
237 msgid: &str,
238 source_agent: &str,
239 target_agent: &str,
240 ts_secs: i64,
241 message: &str,
242) -> Option<String> {
243 let seed: [u8; 32] = private_key.try_into().ok()?;
244 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
245 let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
246 let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
247 Some(BASE64.encode(signature.to_bytes()))
248}
249
250pub fn verify_lan_jekt(
258 public_key: &[u8],
259 msgid: &str,
260 source_agent: &str,
261 target_agent: &str,
262 ts_secs: i64,
263 message: &str,
264 sig_b64: &str,
265) -> bool {
266 let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
267 let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
268 let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
269 let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
270 let signature = Signature::from_bytes(&sig_arr);
271 let material = signed_material(msgid, source_agent, target_agent, ts_secs, message);
272 verifying_key.verify(material.as_bytes(), &signature).is_ok()
273}
274
275const CHANNEL_DOMAIN: &str = "amx-jekt-channel-v1";
296
297fn channel_signed_material(
308 msgid: &str,
309 source_agent: &str,
310 source_channel: &str,
311 target_agent: &str,
312 ts_secs: i64,
313 message: &str,
314) -> String {
315 format!(
316 "{CHANNEL_DOMAIN}{FIELD_SEP}{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}\
317 {source_channel}{FIELD_SEP}{target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}"
318 )
319}
320
321pub fn sign_channel_jekt(
331 private_key: &[u8],
332 msgid: &str,
333 source_agent: &str,
334 source_channel: &str,
335 target_agent: &str,
336 ts_secs: i64,
337 message: &str,
338) -> Option<String> {
339 let seed: [u8; 32] = private_key.try_into().ok()?;
340 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
341 let material =
342 channel_signed_material(msgid, source_agent, source_channel, target_agent, ts_secs, message);
343 let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
344 Some(BASE64.encode(signature.to_bytes()))
345}
346
347#[allow(clippy::too_many_arguments)]
358pub fn verify_channel_jekt(
359 public_key: &[u8],
360 msgid: &str,
361 source_agent: &str,
362 source_channel: &str,
363 target_agent: &str,
364 ts_secs: i64,
365 message: &str,
366 sig_b64: &str,
367) -> bool {
368 let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
369 let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
370 let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
371 let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
372 let signature = Signature::from_bytes(&sig_arr);
373 let material =
374 channel_signed_material(msgid, source_agent, source_channel, target_agent, ts_secs, message);
375 verifying_key.verify(material.as_bytes(), &signature).is_ok()
376}
377
378mod channel_v2;
380pub use channel_v2::{sign_channel_jekt_v2, verify_channel_jekt_v2};
381mod lan_v2;
382pub use lan_v2::{sign_lan_jekt_v2, verify_lan_jekt_v2};
383
384const WAN_DOMAIN: &str = "amx-jekt-wan-v1";
413
414fn wan_signed_material(
438 msgid: &str,
439 source_agent: &str,
440 source_host: &str,
441 source_channel: &str,
442 target_agent: &str,
443 ts_secs: i64,
444 message: &str,
445) -> String {
446 format!(
447 "{WAN_DOMAIN}{FIELD_SEP}{msgid}{FIELD_SEP}{source_agent}{FIELD_SEP}\
448 {source_host}{FIELD_SEP}{source_channel}{FIELD_SEP}\
449 {target_agent}{FIELD_SEP}{ts_secs}{FIELD_SEP}{message}"
450 )
451}
452
453pub fn generate_wan_keypair(seed: [u8; 32]) -> ([u8; 32], [u8; 32]) {
465 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
466 (signing_key.verifying_key().to_bytes(), seed)
467}
468
469pub fn sign_wan_jekt(
478 private_key: &[u8],
479 msgid: &str,
480 source_agent: &str,
481 source_host: &str,
482 source_channel: &str,
483 target_agent: &str,
484 ts_secs: i64,
485 message: &str,
486) -> Option<String> {
487 let seed: [u8; 32] = private_key.try_into().ok()?;
488 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
489 let material = wan_signed_material(
490 msgid,
491 source_agent,
492 source_host,
493 source_channel,
494 target_agent,
495 ts_secs,
496 message,
497 );
498 let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
499 Some(BASE64.encode(signature.to_bytes()))
500}
501
502pub fn verify_wan_jekt(
513 public_key: &[u8],
514 msgid: &str,
515 source_agent: &str,
516 source_host: &str,
517 source_channel: &str,
518 target_agent: &str,
519 ts_secs: i64,
520 message: &str,
521 sig_b64: &str,
522) -> bool {
523 let Ok(pubkey_arr) = <[u8; 32]>::try_from(public_key) else { return false };
524 let Ok(verifying_key) = VerifyingKey::from_bytes(&pubkey_arr) else { return false };
525 let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
526 let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
527 let signature = Signature::from_bytes(&sig_arr);
528 let material = wan_signed_material(
529 msgid,
530 source_agent,
531 source_host,
532 source_channel,
533 target_agent,
534 ts_secs,
535 message,
536 );
537 verifying_key.verify(material.as_bytes(), &signature).is_ok()
538}
539
540const WAN_CERT_DOMAIN: &str = "amx-wan-agent-cert-v1";
552
553const WAN_REVOKE_DOMAIN: &str = "amx-wan-instance-revoke-v1";
555
556pub const WAN_SIG_MAX_AGE_SECS: i64 = 1800 + 300;
560
561pub const WAN_SIG_MAX_FUTURE_SKEW_SECS: i64 = 300;
564
565pub const WAN_INSTANCE_ID_LEN: usize = 26;
567
568pub const WAN_KEY_FP_LEN: usize = 43;
570
571const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
572
573fn base32_lower_nopad(bytes: &[u8]) -> String {
575 let mut out = String::with_capacity((bytes.len() * 8).div_ceil(5));
576 let mut buffer: u32 = 0;
577 let mut bits = 0;
578 for &byte in bytes {
579 buffer = (buffer << 8) | u32::from(byte);
580 bits += 8;
581 while bits >= 5 {
582 bits -= 5;
583 out.push(BASE32_ALPHABET[((buffer >> bits) & 0x1f) as usize] as char);
584 }
585 }
586 if bits > 0 {
587 out.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1f) as usize] as char);
588 }
589 out
590}
591
592pub fn wan_instance_id(instance_public_key: &[u8; 32]) -> String {
598 use sha2::Digest;
599 let digest = Sha256::digest(instance_public_key);
600 base32_lower_nopad(&digest[..16])
601}
602
603pub fn wan_key_fingerprint(agent_public_key: &[u8; 32]) -> String {
607 use base64::engine::general_purpose::URL_SAFE_NO_PAD;
608 use sha2::Digest;
609 URL_SAFE_NO_PAD.encode(Sha256::digest(agent_public_key))
610}
611
612pub fn is_valid_wan_host_hint(host_hint: &str) -> bool {
615 (1..=48).contains(&host_hint.len())
616 && host_hint.bytes().all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'.' || b == b'-')
617}
618
619pub fn wan_instance_display_label(host_hint: &str, verified_instance_id: &str) -> String {
624 let hint = if is_valid_wan_host_hint(host_hint) { host_hint } else { "?" };
625 let id8: String = verified_instance_id.chars().take(8).collect();
626 format!("{hint}~{id8}")
627}
628
629pub fn wan_sig_is_fresh(wan_ts_secs: i64, now_secs: i64) -> bool {
633 now_secs.saturating_sub(wan_ts_secs) <= WAN_SIG_MAX_AGE_SECS
634 && wan_ts_secs.saturating_sub(now_secs) <= WAN_SIG_MAX_FUTURE_SKEW_SECS
635}
636
637fn wan_id_eq(a: &str, b: &str) -> bool {
641 a.trim().eq_ignore_ascii_case(b.trim())
642}
643
644fn wan_cert_material(
645 instance_id: &str,
646 agent_id: &str,
647 channel: &str,
648 agent_public_key_b64: &str,
649 host_hint: &str,
650 issued_at: i64,
651) -> String {
652 let agent_id = agent_id.to_ascii_lowercase();
653 format!(
654 "{WAN_CERT_DOMAIN}{FIELD_SEP}{instance_id}{FIELD_SEP}{agent_id}{FIELD_SEP}\
655 {channel}{FIELD_SEP}{agent_public_key_b64}{FIELD_SEP}{host_hint}{FIELD_SEP}{issued_at}"
656 )
657}
658
659fn wan_revoke_material(old_instance_id: &str, new_instance_id: &str, revoked_at: i64) -> String {
660 format!("{WAN_REVOKE_DOMAIN}{FIELD_SEP}{old_instance_id}{FIELD_SEP}{new_instance_id}{FIELD_SEP}{revoked_at}")
661}
662
663pub(crate) fn ed25519_sign_b64(private_key: &[u8], material: &str) -> Option<String> {
664 let seed: [u8; 32] = private_key.try_into().ok()?;
665 let signing_key = ed25519_dalek::SigningKey::from_bytes(&seed);
666 let signature = ed25519_dalek::Signer::sign(&signing_key, material.as_bytes());
667 Some(BASE64.encode(signature.to_bytes()))
668}
669
670pub(crate) fn ed25519_verify_b64(public_key: &[u8; 32], material: &str, sig_b64: &str) -> bool {
671 let Ok(verifying_key) = VerifyingKey::from_bytes(public_key) else { return false };
672 let Ok(sig_bytes) = BASE64.decode(sig_b64) else { return false };
673 let Ok(sig_arr) = <[u8; 64]>::try_from(sig_bytes.as_slice()) else { return false };
674 verifying_key.verify(material.as_bytes(), &Signature::from_bytes(&sig_arr)).is_ok()
675}
676
677pub(crate) fn decode_public_key_b64(b64: &str) -> Option<[u8; 32]> {
678 BASE64.decode(b64).ok()?.try_into().ok()
679}
680
681#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
688pub struct WanKeyRecord {
689 pub instance_id: String,
690 pub agent_id: String,
691 pub channel: String,
692 pub key_fp: String,
693 pub instance_pubkey: String,
694 pub agent_pubkey: String,
695 pub host_hint: String,
696 pub issued_at: i64,
697 pub cert_sig: String,
698}
699
700impl WanKeyRecord {
701 pub fn certify(
705 instance_private_key: &[u8],
706 agent_id: &str,
707 channel: &str,
708 agent_public_key: &[u8; 32],
709 host_hint: &str,
710 issued_at: i64,
711 ) -> Option<Self> {
712 let seed: [u8; 32] = instance_private_key.try_into().ok()?;
713 let instance_public_key = ed25519_dalek::SigningKey::from_bytes(&seed).verifying_key().to_bytes();
714 let instance_id = wan_instance_id(&instance_public_key);
715 let agent_pubkey = BASE64.encode(agent_public_key);
716 let material = wan_cert_material(&instance_id, agent_id, channel, &agent_pubkey, host_hint, issued_at);
717 Some(Self {
718 instance_id,
719 agent_id: agent_id.to_ascii_lowercase(),
720 channel: channel.to_string(),
721 key_fp: wan_key_fingerprint(agent_public_key),
722 instance_pubkey: BASE64.encode(instance_public_key),
723 agent_pubkey,
724 host_hint: host_hint.to_string(),
725 issued_at,
726 cert_sig: ed25519_sign_b64(&seed, &material)?,
727 })
728 }
729
730 pub fn check_chain(&self) -> Option<[u8; 32]> {
736 let instance_public_key = decode_public_key_b64(&self.instance_pubkey)?;
737 let agent_public_key = decode_public_key_b64(&self.agent_pubkey)?;
738 if wan_instance_id(&instance_public_key) != self.instance_id
739 || wan_key_fingerprint(&agent_public_key) != self.key_fp
740 || self.agent_id != self.agent_id.to_ascii_lowercase()
741 {
742 return None;
743 }
744 let material = wan_cert_material(
745 &self.instance_id,
746 &self.agent_id,
747 &self.channel,
748 &self.agent_pubkey,
749 &self.host_hint,
750 self.issued_at,
751 );
752 ed25519_verify_b64(&instance_public_key, &material, &self.cert_sig).then_some(agent_public_key)
753 }
754}
755
756#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
759pub struct WanRevocation {
760 pub old_instance_id: String,
761 pub old_instance_pubkey: String,
762 pub new_instance_id: String,
763 pub revoked_at: i64,
764 pub sig: String,
765}
766
767impl WanRevocation {
768 pub fn sign(old_private_key: &[u8], new_instance_id: &str, revoked_at: i64) -> Option<Self> {
770 let seed: [u8; 32] = old_private_key.try_into().ok()?;
771 let old_public_key = ed25519_dalek::SigningKey::from_bytes(&seed).verifying_key().to_bytes();
772 let old_instance_id = wan_instance_id(&old_public_key);
773 let material = wan_revoke_material(&old_instance_id, new_instance_id, revoked_at);
774 Some(Self {
775 old_instance_id,
776 old_instance_pubkey: BASE64.encode(old_public_key),
777 new_instance_id: new_instance_id.to_string(),
778 revoked_at,
779 sig: ed25519_sign_b64(&seed, &material)?,
780 })
781 }
782
783 pub fn verify(&self) -> bool {
785 let Some(old_public_key) = decode_public_key_b64(&self.old_instance_pubkey) else { return false };
786 wan_instance_id(&old_public_key) == self.old_instance_id
787 && ed25519_verify_b64(
788 &old_public_key,
789 &wan_revoke_material(&self.old_instance_id, &self.new_instance_id, self.revoked_at),
790 &self.sig,
791 )
792 }
793}
794
795#[derive(Debug, Clone, PartialEq, Eq)]
798pub struct WanCarried<'a> {
799 pub sig: &'a str,
800 pub msg_id: &'a str,
801 pub ts_secs: i64,
802 pub source_agent: &'a str,
803 pub target_agent: &'a str,
804 pub source_host: &'a str,
805 pub source_channel: &'a str,
806 pub key_fp: &'a str,
807}
808
809#[derive(Debug, Clone, Copy, PartialEq, Eq)]
813pub enum WanCheckFailure {
814 EnvelopeMismatch,
817 Stale,
819 CertInvalid,
822 RecordMismatch,
825 BadSignature,
828}
829
830impl WanCheckFailure {
831 pub fn verdict(self) -> Option<bool> {
834 match self {
835 Self::Stale => None,
836 Self::EnvelopeMismatch | Self::CertInvalid | Self::RecordMismatch | Self::BadSignature => Some(false),
837 }
838 }
839}
840
841pub fn check_wan_envelope(
847 carried: &WanCarried<'_>,
848 row_source_agent: &str,
849 polled_agent_id: &str,
850 now_secs: i64,
851) -> Result<(), WanCheckFailure> {
852 if !wan_id_eq(carried.source_agent, row_source_agent) || !wan_id_eq(carried.target_agent, polled_agent_id) {
853 return Err(WanCheckFailure::EnvelopeMismatch);
854 }
855 if !wan_sig_is_fresh(carried.ts_secs, now_secs) {
856 return Err(WanCheckFailure::Stale);
857 }
858 Ok(())
859}
860
861pub fn verify_wan_against_record(
866 carried: &WanCarried<'_>,
867 record: &WanKeyRecord,
868 message: &str,
869) -> Result<(), WanCheckFailure> {
870 let agent_public_key = record.check_chain().ok_or(WanCheckFailure::CertInvalid)?;
871 if !wan_id_eq(&record.instance_id, carried.source_host)
877 || !wan_id_eq(&record.channel, carried.source_channel)
878 || !wan_id_eq(&record.agent_id, carried.source_agent)
879 || record.key_fp != carried.key_fp
880 {
881 return Err(WanCheckFailure::RecordMismatch);
882 }
883 if !verify_wan_jekt(
884 &agent_public_key,
885 carried.msg_id,
886 carried.source_agent,
887 carried.source_host,
888 carried.source_channel,
889 carried.target_agent,
890 carried.ts_secs,
891 message,
892 carried.sig,
893 ) {
894 return Err(WanCheckFailure::BadSignature);
895 }
896 Ok(())
897}
898
899#[cfg(test)]
900mod tests {
901 use super::*;
902
903 fn key() -> Vec<u8> {
904 vec![7u8; 32]
905 }
906
907 #[test]
908 fn a_correctly_signed_message_verifies() {
909 let k = key();
910 let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
911 assert!(verify_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
912 }
913
914 #[test]
915 fn wrong_key_fails_verification() {
916 let sig = sign_jekt(&key(), "msg-1", "agentx", "agenty", 1_000, "hello");
917 let other_key = vec![9u8; 32];
918 assert!(!verify_jekt(&other_key, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
919 }
920
921 #[test]
922 fn tampered_message_fails_verification() {
923 let k = key();
924 let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
925 assert!(!verify_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "goodbye", &sig));
926 }
927
928 #[test]
929 fn signature_cannot_be_replayed_against_a_different_target() {
930 let k = key();
934 let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
935 assert!(!verify_jekt(&k, "msg-1", "agentx", "agentz", 1_000, "hello", &sig));
936 }
937
938 #[test]
939 fn signature_cannot_be_reattributed_to_a_different_claimed_sender() {
940 let k = key();
941 let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
942 assert!(!verify_jekt(&k, "msg-1", "agentz", "agenty", 1_000, "hello", &sig));
943 }
944
945 #[test]
946 fn different_msgid_fails_verification() {
947 let k = key();
948 let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
949 assert!(!verify_jekt(&k, "msg-2", "agentx", "agenty", 1_000, "hello", &sig));
950 }
951
952 #[test]
953 fn different_timestamp_fails_verification() {
954 let k = key();
955 let sig = sign_jekt(&k, "msg-1", "agentx", "agenty", 1_000, "hello");
956 assert!(!verify_jekt(&k, "msg-1", "agentx", "agenty", 1_001, "hello", &sig));
957 }
958
959 #[test]
960 fn malformed_base64_signature_fails_verification_not_panics() {
961 assert!(!verify_jekt(&key(), "msg-1", "agentx", "agenty", 1_000, "hello", "not-valid-base64!!"));
962 }
963
964 #[test]
965 fn empty_signature_fails_verification() {
966 assert!(!verify_jekt(&key(), "msg-1", "agentx", "agenty", 1_000, "hello", ""));
967 }
968
969 const FIXTURE_SIG_B64: &str =
977 "QehidZjJa2jYLPIPYSsVxUlm86W5Fdbr9PV3P4HJyZwJ68/HZR9EaAL0MpcVtTuZJW2+MMGebc0RH9HITNJGCw==";
978
979 #[test]
980 fn a_correctly_signed_reagent_message_verifies() {
981 assert!(verify_reagent_jekt(
982 "reagent-v1-dev",
983 "msg-1",
984 "github-consumer",
985 "agentx",
986 1_000,
987 "hello",
988 FIXTURE_SIG_B64,
989 ));
990 }
991
992 const PROD_FIXTURE_SIG_B64: &str =
999 "FCFjcvAzla329a39u8fFxOvRWaH1R2fUn8RsGtP9RaIbLbaS3aXgAQ7YB4ssWV5TDvAeGrwSkHfoeGi11iCPBg==";
1000
1001 #[test]
1002 fn a_correctly_signed_reagent_message_verifies_under_the_production_key() {
1003 assert!(verify_reagent_jekt(
1004 "reagent-v1",
1005 "msg-1",
1006 "github-consumer",
1007 "agentx",
1008 1_000,
1009 "hello",
1010 PROD_FIXTURE_SIG_B64,
1011 ));
1012 }
1013
1014 #[test]
1015 fn the_dev_and_prod_reagent_keys_are_not_interchangeable() {
1016 assert!(!verify_reagent_jekt(
1020 "reagent-v1-dev",
1021 "msg-1",
1022 "github-consumer",
1023 "agentx",
1024 1_000,
1025 "hello",
1026 PROD_FIXTURE_SIG_B64,
1027 ));
1028 assert!(!verify_reagent_jekt(
1029 "reagent-v1",
1030 "msg-1",
1031 "github-consumer",
1032 "agentx",
1033 1_000,
1034 "hello",
1035 FIXTURE_SIG_B64,
1036 ));
1037 }
1038
1039 #[test]
1047 fn the_production_key_is_trusted_for_tier_relaxation() {
1048 assert!(is_reagent_trusted_signing_key("reagent-v1"));
1049 }
1050
1051 #[test]
1052 fn the_exposed_dev_key_is_not_trusted_for_tier_relaxation_even_though_it_verifies() {
1053 assert!(!is_reagent_trusted_signing_key("reagent-v1-dev"));
1054 assert!(verify_reagent_jekt(
1057 "reagent-v1-dev",
1058 "msg-1",
1059 "github-consumer",
1060 "agentx",
1061 1_000,
1062 "hello",
1063 FIXTURE_SIG_B64,
1064 ));
1065 }
1066
1067 #[test]
1068 fn an_unregistered_key_id_is_not_trusted_for_tier_relaxation() {
1069 assert!(!is_reagent_trusted_signing_key("reagent-v2-does-not-exist"));
1070 }
1071
1072 #[test]
1075 fn a_production_key_signature_is_a_trusted_verification() {
1076 assert!(verify_trusted_reagent_jekt(
1077 "reagent-v1",
1078 "msg-1",
1079 "github-consumer",
1080 "agentx",
1081 1_000,
1082 "hello",
1083 PROD_FIXTURE_SIG_B64,
1084 ));
1085 }
1086
1087 #[test]
1088 fn a_valid_dev_key_signature_is_not_a_trusted_verification() {
1089 assert!(!verify_trusted_reagent_jekt(
1090 "reagent-v1-dev",
1091 "msg-1",
1092 "github-consumer",
1093 "agentx",
1094 1_000,
1095 "hello",
1096 FIXTURE_SIG_B64,
1097 ));
1098 }
1099
1100 #[test]
1101 fn a_tampered_production_key_message_is_not_a_trusted_verification() {
1102 assert!(!verify_trusted_reagent_jekt(
1103 "reagent-v1",
1104 "msg-1",
1105 "github-consumer",
1106 "agentx",
1107 1_000,
1108 "hello, tampered",
1109 PROD_FIXTURE_SIG_B64,
1110 ));
1111 }
1112
1113 #[test]
1114 fn reagent_unknown_key_id_fails_verification() {
1115 assert!(!verify_reagent_jekt(
1116 "reagent-v2-does-not-exist",
1117 "msg-1",
1118 "github-consumer",
1119 "agentx",
1120 1_000,
1121 "hello",
1122 FIXTURE_SIG_B64,
1123 ));
1124 }
1125
1126 #[test]
1127 fn reagent_tampered_message_fails_verification() {
1128 assert!(!verify_reagent_jekt(
1129 "reagent-v1-dev",
1130 "msg-1",
1131 "github-consumer",
1132 "agentx",
1133 1_000,
1134 "goodbye",
1135 FIXTURE_SIG_B64,
1136 ));
1137 }
1138
1139 #[test]
1140 fn reagent_wrong_target_fails_verification() {
1141 assert!(!verify_reagent_jekt(
1145 "reagent-v1-dev",
1146 "msg-1",
1147 "github-consumer",
1148 "agenty",
1149 1_000,
1150 "hello",
1151 FIXTURE_SIG_B64,
1152 ));
1153 }
1154
1155 #[test]
1156 fn reagent_wrong_source_fails_verification() {
1157 assert!(!verify_reagent_jekt(
1158 "reagent-v1-dev",
1159 "msg-1",
1160 "someone-else",
1161 "agentx",
1162 1_000,
1163 "hello",
1164 FIXTURE_SIG_B64,
1165 ));
1166 }
1167
1168 #[test]
1169 fn reagent_wrong_timestamp_fails_verification() {
1170 assert!(!verify_reagent_jekt(
1171 "reagent-v1-dev",
1172 "msg-1",
1173 "github-consumer",
1174 "agentx",
1175 1_001,
1176 "hello",
1177 FIXTURE_SIG_B64,
1178 ));
1179 }
1180
1181 #[test]
1182 fn reagent_wrong_msgid_fails_verification() {
1183 assert!(!verify_reagent_jekt(
1184 "reagent-v1-dev",
1185 "msg-2",
1186 "github-consumer",
1187 "agentx",
1188 1_000,
1189 "hello",
1190 FIXTURE_SIG_B64,
1191 ));
1192 }
1193
1194 #[test]
1195 fn reagent_malformed_base64_signature_fails_verification_not_panics() {
1196 assert!(!verify_reagent_jekt(
1197 "reagent-v1-dev",
1198 "msg-1",
1199 "github-consumer",
1200 "agentx",
1201 1_000,
1202 "hello",
1203 "not-valid-base64!!",
1204 ));
1205 }
1206
1207 #[test]
1208 fn reagent_wrong_length_signature_fails_verification_not_panics() {
1209 assert!(!verify_reagent_jekt(
1210 "reagent-v1-dev",
1211 "msg-1",
1212 "github-consumer",
1213 "agentx",
1214 1_000,
1215 "hello",
1216 "dG9vc2hvcnQ=", ));
1218 }
1219
1220 #[test]
1221 fn reagent_empty_signature_fails_verification() {
1222 assert!(!verify_reagent_jekt(
1223 "reagent-v1-dev",
1224 "msg-1",
1225 "github-consumer",
1226 "agentx",
1227 1_000,
1228 "hello",
1229 "",
1230 ));
1231 }
1232
1233 fn lan_seed(byte: u8) -> [u8; 32] {
1236 [byte; 32]
1237 }
1238
1239 #[test]
1240 fn lan_keypair_is_deterministic_from_seed_and_public_differs_from_private() {
1241 let (pub1, priv1) = generate_lan_keypair(lan_seed(3));
1242 let (pub2, priv2) = generate_lan_keypair(lan_seed(3));
1243 assert_eq!(pub1, pub2, "same seed must derive the same keypair");
1244 assert_eq!(priv1, priv2);
1245 assert_ne!(pub1, priv1, "public and private halves must differ");
1246 }
1247
1248 #[test]
1249 fn different_seeds_give_different_keypairs() {
1250 let (pub_a, _) = generate_lan_keypair(lan_seed(1));
1251 let (pub_b, _) = generate_lan_keypair(lan_seed(2));
1252 assert_ne!(pub_a, pub_b);
1253 }
1254
1255 #[test]
1256 fn a_correctly_signed_lan_message_verifies() {
1257 let (public_key, private_key) = generate_lan_keypair(lan_seed(5));
1258 let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1259 assert!(verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1260 }
1261
1262 #[test]
1263 fn lan_wrong_key_fails_verification() {
1264 let (_, private_key) = generate_lan_keypair(lan_seed(5));
1265 let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1266 let (other_public, _) = generate_lan_keypair(lan_seed(9));
1267 assert!(!verify_lan_jekt(&other_public, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1268 }
1269
1270 #[test]
1271 fn lan_tampered_message_fails_verification() {
1272 let (public_key, private_key) = generate_lan_keypair(lan_seed(5));
1273 let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1274 assert!(!verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "goodbye", &sig));
1275 }
1276
1277 #[test]
1278 fn lan_tampered_claimed_sender_fails_verification() {
1279 let (public_key, private_key) = generate_lan_keypair(lan_seed(5));
1283 let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1284 assert!(!verify_lan_jekt(&public_key, "msg-1", "someoneelse", "agenty", 1_000, "hello", &sig));
1285 }
1286
1287 #[test]
1288 fn lan_signing_with_wrong_length_key_returns_none_not_panics() {
1289 assert!(sign_lan_jekt(&[1u8; 16], "msg-1", "agentx", "agenty", 1_000, "hello").is_none());
1290 assert!(sign_lan_jekt(&[], "msg-1", "agentx", "agenty", 1_000, "hello").is_none());
1291 }
1292
1293 #[test]
1294 fn lan_verify_with_wrong_length_public_key_fails_not_panics() {
1295 let (_, private_key) = generate_lan_keypair(lan_seed(5));
1296 let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1297 assert!(!verify_lan_jekt(&[1u8; 16], "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1298 }
1299
1300 #[test]
1301 fn lan_verify_with_malformed_base64_signature_fails_not_panics() {
1302 let (public_key, _) = generate_lan_keypair(lan_seed(5));
1303 assert!(!verify_lan_jekt(
1304 &public_key,
1305 "msg-1",
1306 "agentx",
1307 "agenty",
1308 1_000,
1309 "hello",
1310 "not-valid-base64!!",
1311 ));
1312 }
1313
1314 #[test]
1315 fn lan_verify_with_wrong_length_signature_fails_not_panics() {
1316 let (public_key, _) = generate_lan_keypair(lan_seed(5));
1317 assert!(!verify_lan_jekt(
1318 &public_key,
1319 "msg-1",
1320 "agentx",
1321 "agenty",
1322 1_000,
1323 "hello",
1324 "dG9vc2hvcnQ=", ));
1326 }
1327
1328 #[test]
1329 fn lan_verify_with_empty_signature_fails_not_panics() {
1330 let (public_key, _) = generate_lan_keypair(lan_seed(5));
1331 assert!(!verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "hello", ""));
1332 }
1333
1334 #[test]
1335 fn a_lan_signature_does_not_verify_against_reagent_or_host_schemes() {
1336 let (_, private_key) = generate_lan_keypair(lan_seed(5));
1341 let sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1342 assert!(!verify_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello", &sig));
1343 }
1344
1345 #[test]
1348 fn a_correctly_signed_channel_message_verifies() {
1349 let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1350 let sig =
1351 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1352 .unwrap();
1353 assert!(verify_channel_jekt(
1354 &public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig
1355 ));
1356 }
1357
1358 #[test]
1359 fn channel_wrong_key_fails_verification() {
1360 let (_, private_key) = generate_lan_keypair(lan_seed(9));
1361 let (other_public, _) = generate_lan_keypair(lan_seed(10));
1362 let sig =
1363 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1364 .unwrap();
1365 assert!(!verify_channel_jekt(
1366 &other_public, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig
1367 ));
1368 }
1369
1370 #[test]
1371 fn channel_tampered_fields_fail_verification() {
1372 let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1373 let sig =
1374 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1375 .unwrap();
1376 for (msgid, src, chan, tgt, ts, msg) in [
1378 ("msg-2", "agentx", "chan-a", "agenty", 1_000, "hello"),
1379 ("msg-1", "agentz", "chan-a", "agenty", 1_000, "hello"),
1380 ("msg-1", "agentx", "chan-a", "agentz", 1_000, "hello"),
1381 ("msg-1", "agentx", "chan-a", "agenty", 1_001, "hello"),
1382 ("msg-1", "agentx", "chan-a", "agenty", 1_000, "goodbye"),
1383 ] {
1384 assert!(
1385 !verify_channel_jekt(&public_key, msgid, src, chan, tgt, ts, msg, &sig),
1386 "tampering with a signed field must fail: {msgid}/{src}/{chan}/{tgt}/{ts}/{msg}"
1387 );
1388 }
1389 }
1390
1391 #[test]
1392 fn a_signature_minted_for_one_channel_does_not_verify_as_another() {
1393 let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1398 let sig =
1399 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1400 .unwrap();
1401 assert!(!verify_channel_jekt(
1402 &public_key, "msg-1", "agentx", "chan-b", "agenty", 1_000, "hello", &sig
1403 ));
1404 }
1405
1406 #[test]
1407 fn lan_and_channel_signatures_are_not_interchangeable_in_either_direction() {
1408 let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1414 let lan_sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1415 let channel_sig =
1416 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1417 .unwrap();
1418
1419 assert_ne!(lan_sig, channel_sig, "the two schemes must not produce identical bytes");
1420
1421 assert!(!verify_channel_jekt(
1423 &public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &lan_sig
1424 ));
1425 assert!(!verify_lan_jekt(
1427 &public_key, "msg-1", "agentx", "agenty", 1_000, "hello", &channel_sig
1428 ));
1429 }
1430
1431 #[test]
1432 fn channel_malformed_inputs_fail_rather_than_panic() {
1433 let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1434 let sig =
1435 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1436 .unwrap();
1437 assert!(sign_channel_jekt(&[1u8; 16], "m", "a", "c", "b", 1, "x").is_none());
1439 assert!(!verify_channel_jekt(&[0u8; 16], "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig));
1441 assert!(!verify_channel_jekt(&public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", "!!not base64!!"));
1442 assert!(!verify_channel_jekt(&public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", ""));
1443 }
1444
1445 #[test]
1446 fn an_empty_source_channel_is_still_bound_not_ignored() {
1447 let (public_key, private_key) = generate_lan_keypair(lan_seed(9));
1450 let sig = sign_channel_jekt(&private_key, "msg-1", "agentx", "", "agenty", 1_000, "hello").unwrap();
1451 assert!(verify_channel_jekt(&public_key, "msg-1", "agentx", "", "agenty", 1_000, "hello", &sig));
1452 assert!(!verify_channel_jekt(&public_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello", &sig));
1453 }
1454
1455 #[test]
1458 fn a_correctly_signed_wan_message_verifies() {
1459 let (public_key, private_key) = generate_wan_keypair(lan_seed(11));
1460 let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1461 assert!(verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1462 }
1463
1464 #[test]
1465 fn a_wan_signature_does_not_verify_under_another_agents_key() {
1466 let (_, private_key) = generate_wan_keypair(lan_seed(11));
1467 let (other_public, _) = generate_wan_keypair(lan_seed(12));
1468 let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1469 assert!(!verify_wan_jekt(&other_public, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1470 }
1471
1472 #[test]
1473 fn tampering_with_any_wan_field_breaks_verification() {
1474 let (public_key, private_key) = generate_wan_keypair(lan_seed(11));
1475 let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1476 assert!(!verify_wan_jekt(&public_key, "msg-2", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1477 assert!(!verify_wan_jekt(&public_key, "msg-1", "someoneelse", "narko", "stable", "agenty", 1_000, "hello", &sig));
1478 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agentz", 1_000, "hello", &sig));
1479 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_001, "hello", &sig));
1480 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "goodbye", &sig));
1481 }
1482
1483 #[test]
1484 fn wan_malformed_inputs_fail_rather_than_panic() {
1485 let (public_key, private_key) = generate_wan_keypair(lan_seed(11));
1486 let sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1487 assert!(sign_wan_jekt(&[1u8; 16], "m", "a", "h", "c", "b", 1, "x").is_none());
1488 assert!(!verify_wan_jekt(&[0u8; 16], "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &sig));
1489 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", "!!not base64!!"));
1490 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", ""));
1491 }
1492
1493 #[test]
1494 fn a_wan_signature_is_bound_to_the_sending_instance() {
1495 let (public_key, private_key) = generate_wan_keypair(lan_seed(14));
1503 let sig =
1504 sign_wan_jekt(&private_key, "msg-1", "lark", "narko", "stable", "agenty", 1_000, "hello")
1505 .unwrap();
1506
1507 assert!(verify_wan_jekt(
1508 &public_key, "msg-1", "lark", "narko", "stable", "agenty", 1_000, "hello", &sig
1509 ));
1510 assert!(!verify_wan_jekt(
1512 &public_key, "msg-1", "lark", "area54", "stable", "agenty", 1_000, "hello", &sig
1513 ));
1514 assert!(!verify_wan_jekt(
1517 &public_key, "msg-1", "lark", "narko", "local-dev", "agenty", 1_000, "hello", &sig
1518 ));
1519 }
1520
1521 #[test]
1522 fn a_wan_signature_is_not_interchangeable_with_lan_or_channel() {
1523 let seed = lan_seed(13);
1530 let (public_key, private_key) = generate_wan_keypair(seed);
1531
1532 let wan_sig = sign_wan_jekt(&private_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello").unwrap();
1533 let lan_sig = sign_lan_jekt(&private_key, "msg-1", "agentx", "agenty", 1_000, "hello").unwrap();
1534 let channel_sig =
1535 sign_channel_jekt(&private_key, "msg-1", "agentx", "chan-a", "agenty", 1_000, "hello")
1536 .unwrap();
1537
1538 assert!(verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &wan_sig));
1540 assert!(!verify_lan_jekt(&public_key, "msg-1", "agentx", "agenty", 1_000, "hello", &wan_sig));
1541 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &lan_sig));
1542 assert!(!verify_wan_jekt(&public_key, "msg-1", "agentx", "narko", "stable", "agenty", 1_000, "hello", &channel_sig
1543 ));
1544 assert_ne!(wan_sig, lan_sig);
1545 }
1546
1547 const V_INSTANCE_PUBKEY: &str = "iojj3XQJ8ZX9UtstPLpdcspnCb8dlBIb83SIAbQPb1w=";
1557 const V_INSTANCE_ID: &str = "gr2q7gf5lh6pzfdnurnkvputhm";
1558 const V_AGENT_PUBKEY: &str = "gTl3Dqh9F19Wo1Rmw0x+zMuNipG07jeiXfYPW4/Js5Q=";
1559 const V_KEY_FP: &str = "ajgD1fBZkCocba-8m6RykhL3yqwIY0zDrnaydSnwOCc";
1560 const V_CERT_SIG: &str =
1561 "ighbn1y/kyisIolSHwNyRtBqC2u95zQEBtGc3wxwksBUBHrDoUvXmz8yEXY20TFzOBC6IDEsG51WzTNJCrgNAA==";
1562 const V_NEW_INSTANCE_ID: &str = "wyxim75c6m5p4ywv22ywilqwee";
1563 const V_REVOKE_SIG: &str =
1564 "DOQzFIVNGItL3mkN7IPlEk5lBrmM96Dgub7jbL5JmTR5oDnCwWlfKzuYsyvZcUJJMRs56glIBhrDVHIeclqEDg==";
1565 const V_MSG_SIG: &str =
1566 "49VODE2M7pORgWgcQPl2p+M8W2ccaXJ1O8Zu1XeR4qRiJDsshawzq8eMpSkKTz+CJE0CXz2oTlXsNDobpgsRBw==";
1567 const V_ISSUED_AT: i64 = 1_790_000_000;
1568 const V_MSG_TS: i64 = 1_790_000_100;
1569
1570 fn vector_record() -> WanKeyRecord {
1571 let (agent_public_key, _) = generate_wan_keypair([2; 32]);
1572 WanKeyRecord::certify(&[1; 32], "camper", "stable", &agent_public_key, "narko", V_ISSUED_AT).unwrap()
1573 }
1574
1575 fn vector_carried() -> WanCarried<'static> {
1576 WanCarried {
1577 sig: V_MSG_SIG,
1578 msg_id: "msg-1",
1579 ts_secs: V_MSG_TS,
1580 source_agent: "camper",
1581 target_agent: "agent2",
1582 source_host: V_INSTANCE_ID,
1583 source_channel: "stable",
1584 key_fp: V_KEY_FP,
1585 }
1586 }
1587
1588 #[test]
1589 fn w3s_identifiers_and_certificate_match_the_cross_language_vectors() {
1590 let record = vector_record();
1591 assert_eq!(record.instance_pubkey, V_INSTANCE_PUBKEY);
1592 assert_eq!(record.instance_id, V_INSTANCE_ID);
1593 assert_eq!(record.instance_id.len(), WAN_INSTANCE_ID_LEN);
1594 assert_eq!(record.agent_pubkey, V_AGENT_PUBKEY);
1595 assert_eq!(record.key_fp, V_KEY_FP);
1596 assert_eq!(record.key_fp.len(), WAN_KEY_FP_LEN);
1597 assert_eq!(record.cert_sig, V_CERT_SIG);
1598 assert!(record.check_chain().is_some());
1599
1600 let (new_instance_public_key, _) = generate_wan_keypair([3; 32]);
1601 assert_eq!(wan_instance_id(&new_instance_public_key), V_NEW_INSTANCE_ID);
1602 let revocation = WanRevocation::sign(&[1; 32], V_NEW_INSTANCE_ID, 1_790_000_500).unwrap();
1603 assert_eq!(revocation.sig, V_REVOKE_SIG);
1604 assert!(revocation.verify());
1605
1606 let (_, agent_private_key) = generate_wan_keypair([2; 32]);
1607 let msg_sig = sign_wan_jekt(&agent_private_key, "msg-1", "camper", V_INSTANCE_ID, "stable", "agent2", V_MSG_TS, "hello");
1608 assert_eq!(msg_sig.as_deref(), Some(V_MSG_SIG));
1609 }
1610
1611 #[test]
1612 fn base32_matches_rfc4648_lowercased() {
1613 for (input, expected) in
1615 [("", ""), ("f", "my"), ("fo", "mzxq"), ("foo", "mzxw6"), ("foob", "mzxw6yq"), ("fooba", "mzxw6ytb"), ("foobar", "mzxw6ytboi")]
1616 {
1617 assert_eq!(base32_lower_nopad(input.as_bytes()), expected, "input {input:?}");
1618 }
1619 }
1620
1621 #[test]
1622 fn a_certificate_chain_breaks_on_any_substituted_field() {
1623 let good = vector_record();
1624 let (other_public_key, _) = generate_wan_keypair([9; 32]);
1625 let other_b64 = BASE64.encode(other_public_key);
1626 type Mutation<'a> = (&'a str, Box<dyn Fn(&mut WanKeyRecord) + 'a>);
1627 let mutations: Vec<Mutation> = vec![
1628 ("agent key", Box::new(|r| {
1630 r.agent_pubkey = other_b64.clone();
1631 r.key_fp = wan_key_fingerprint(&other_public_key);
1632 })),
1633 ("instance key", Box::new(|r| r.instance_pubkey = other_b64.clone())),
1635 ("instance id + key", Box::new(|r| {
1637 r.instance_pubkey = other_b64.clone();
1638 r.instance_id = wan_instance_id(&other_public_key);
1639 })),
1640 ("fingerprint", Box::new(|r| r.key_fp = wan_key_fingerprint(&other_public_key))),
1641 ("agent id", Box::new(|r| r.agent_id = "lark".into())),
1642 ("uppercased agent id", Box::new(|r| r.agent_id = "Camper".into())),
1643 ("channel", Box::new(|r| r.channel = "beta".into())),
1644 ("host hint", Box::new(|r| r.host_hint = "area54".into())),
1645 ("issued_at", Box::new(|r| r.issued_at += 1)),
1646 ("cert sig", Box::new(|r| r.cert_sig = V_REVOKE_SIG.into())),
1647 ("garbage key", Box::new(|r| r.agent_pubkey = "not base64".into())),
1648 ];
1649 for (name, mutate) in mutations {
1650 let mut record = good.clone();
1651 mutate(&mut record);
1652 assert!(record.check_chain().is_none(), "{name} substitution passed the chain");
1653 }
1654 }
1655
1656 #[test]
1657 fn certify_lowercases_the_agent_id_and_rejects_a_bad_seed() {
1658 let (agent_public_key, _) = generate_wan_keypair([2; 32]);
1659 let record = WanKeyRecord::certify(&[1; 32], "Camper", "stable", &agent_public_key, "narko", V_ISSUED_AT).unwrap();
1660 assert_eq!(record, vector_record());
1661 assert!(WanKeyRecord::certify(&[1; 31], "camper", "stable", &agent_public_key, "narko", 0).is_none());
1662 }
1663
1664 #[test]
1665 fn a_revocation_verifies_only_as_signed_by_the_old_key() {
1666 let good = WanRevocation::sign(&[1; 32], V_NEW_INSTANCE_ID, 1_790_000_500).unwrap();
1667 let mut retargeted = good.clone();
1668 retargeted.new_instance_id = V_INSTANCE_ID.into();
1669 assert!(!retargeted.verify());
1670 let mut redated = good.clone();
1671 redated.revoked_at += 1;
1672 assert!(!redated.verify());
1673 let mut foreign = WanRevocation::sign(&[9; 32], V_NEW_INSTANCE_ID, 1_790_000_500).unwrap();
1675 foreign.old_instance_id = V_INSTANCE_ID.into();
1676 assert!(!foreign.verify());
1677 }
1678
1679 #[test]
1680 fn freshness_window_boundaries() {
1681 let now = 1_790_000_000;
1682 assert!(wan_sig_is_fresh(now, now));
1683 assert!(wan_sig_is_fresh(now - WAN_SIG_MAX_AGE_SECS, now));
1684 assert!(!wan_sig_is_fresh(now - WAN_SIG_MAX_AGE_SECS - 1, now));
1685 assert!(wan_sig_is_fresh(now + WAN_SIG_MAX_FUTURE_SKEW_SECS, now));
1686 assert!(!wan_sig_is_fresh(now + WAN_SIG_MAX_FUTURE_SKEW_SECS + 1, now));
1687 assert_eq!(WAN_SIG_MAX_AGE_SECS, 2100);
1688 assert!(!wan_sig_is_fresh(i64::MIN, now));
1690 assert!(!wan_sig_is_fresh(i64::MAX, now));
1691 }
1692
1693 #[test]
1694 fn the_envelope_binds_source_to_the_row_and_target_to_the_polled_agent() {
1695 let carried = vector_carried();
1696 assert_eq!(check_wan_envelope(&carried, "camper", "agent2", V_MSG_TS), Ok(()));
1697 assert_eq!(check_wan_envelope(&carried, " Camper ", "AGENT2", V_MSG_TS), Ok(()));
1699 assert_eq!(check_wan_envelope(&carried, "camper", "lark", V_MSG_TS), Err(WanCheckFailure::EnvelopeMismatch));
1701 assert_eq!(check_wan_envelope(&carried, "clamk", "agent2", V_MSG_TS), Err(WanCheckFailure::EnvelopeMismatch));
1702 assert_eq!(
1704 check_wan_envelope(&carried, "camper", "lark", V_MSG_TS + 10_000),
1705 Err(WanCheckFailure::EnvelopeMismatch)
1706 );
1707 assert_eq!(check_wan_envelope(&carried, "camper", "agent2", V_MSG_TS + 2_101), Err(WanCheckFailure::Stale));
1708 assert_eq!(WanCheckFailure::Stale.verdict(), None);
1709 assert_eq!(WanCheckFailure::EnvelopeMismatch.verdict(), Some(false));
1710 }
1711
1712 #[test]
1713 fn a_carried_message_verifies_against_its_certified_record() {
1714 assert_eq!(verify_wan_against_record(&vector_carried(), &vector_record(), "hello"), Ok(()));
1715 }
1716
1717 #[test]
1718 fn each_carried_field_altered_in_turn_fails() {
1719 let record = vector_record();
1720 let cases: Vec<(&str, WanCarried<'static>, WanCheckFailure)> = vec![
1721 ("msg_id", WanCarried { msg_id: "msg-2", ..vector_carried() }, WanCheckFailure::BadSignature),
1722 ("ts", WanCarried { ts_secs: V_MSG_TS + 1, ..vector_carried() }, WanCheckFailure::BadSignature),
1723 ("target", WanCarried { target_agent: "lark", ..vector_carried() }, WanCheckFailure::BadSignature),
1724 ("sig", WanCarried { sig: V_CERT_SIG, ..vector_carried() }, WanCheckFailure::BadSignature),
1725 ("source", WanCarried { source_agent: "lark", ..vector_carried() }, WanCheckFailure::RecordMismatch),
1726 ("host", WanCarried { source_host: V_NEW_INSTANCE_ID, ..vector_carried() }, WanCheckFailure::RecordMismatch),
1727 ("channel", WanCarried { source_channel: "beta", ..vector_carried() }, WanCheckFailure::RecordMismatch),
1728 ("key_fp", WanCarried { key_fp: "x", ..vector_carried() }, WanCheckFailure::RecordMismatch),
1729 ];
1730 for (name, carried, expected) in cases {
1731 assert_eq!(verify_wan_against_record(&carried, &record, "hello"), Err(expected), "altered {name}");
1732 }
1733 assert_eq!(
1734 verify_wan_against_record(&vector_carried(), &record, "hello!"),
1735 Err(WanCheckFailure::BadSignature)
1736 );
1737 }
1738
1739 #[test]
1740 fn the_record_match_is_ascii_case_insensitive_for_instance_channel_and_agent() {
1741 let (agent_public_key, agent_private_key) = generate_wan_keypair([2; 32]);
1745 let record = WanKeyRecord::certify(&[1; 32], "camper", "Stable", &agent_public_key, "narko", V_ISSUED_AT).unwrap();
1746 let sig = sign_wan_jekt(&agent_private_key, "msg-1", "Camper", V_INSTANCE_ID, "stable", "agent2", V_MSG_TS, "hello").unwrap();
1747 let carried = WanCarried {
1748 sig: &sig,
1749 source_agent: "Camper",
1750 source_channel: "stable",
1751 ..vector_carried()
1752 };
1753 assert_eq!(verify_wan_against_record(&carried, &record, "hello"), Ok(()));
1754 }
1755
1756 #[test]
1757 fn a_tampered_record_is_cert_invalid_before_anything_else() {
1758 let mut record = vector_record();
1759 record.host_hint = "evil".into();
1760 assert_eq!(
1761 verify_wan_against_record(&vector_carried(), &record, "hello"),
1762 Err(WanCheckFailure::CertInvalid)
1763 );
1764 }
1765
1766 #[test]
1767 fn host_hints_and_display_labels() {
1768 assert!(is_valid_wan_host_hint("narko"));
1769 assert!(is_valid_wan_host_hint("host-1.lan"));
1770 for bad in ["", "Narko", "narko~x", "has space", "ümlaut", &"a".repeat(49)] {
1771 assert!(!is_valid_wan_host_hint(bad), "{bad:?}");
1772 }
1773 assert_eq!(wan_instance_display_label("narko", V_INSTANCE_ID), "narko~gr2q7gf5");
1774 assert_eq!(wan_instance_display_label("narko~abcdefgh", V_INSTANCE_ID), "?~gr2q7gf5");
1776 }
1777}
1778
1779#[cfg(test)]
1787mod cross_version_goldens {
1788 use super::*;
1789
1790 const SEED: [u8; 32] = [42u8; 32];
1791 const TS: i64 = 1_790_000_000;
1792 const PK: &str = "GX9rI+FshTLGq8g4+s1ep4m+DHaykgM0A5v6iz02jWE=";
1793 const LAN: &str = "EXTYmZi7R4FlXQU5olRQng9i3JvoyPaNk+5hsgz/pwLVYBoIP5/fKSlN8YCAViv/j18Gr9fCp0W9fS2NYrFrAQ==";
1794 const CHAN: &str = "lSTM+FmFR/QsqzmTiVvO8KLa5jmFrnQhpuQ7i543/ezCoeOMaAll+N2NEs2wfL727/vwzOYFDt3Exkk6h7yKCw==";
1795 const WAN: &str = "IeNtT8Ivh6k84poizAS0a1oBzBQsDRteO72vlsviWieBDHu/ET8XZNwjLNlZGFyjMWA4GnbZbOn9S02pvbYjCw==";
1796
1797 fn hex(s: &str) -> Vec<u8> {
1798 (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
1799 }
1800
1801 #[test]
1802 fn rfc8032_test_1_matches() {
1803 let seed: [u8; 32] = hex("9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60")
1804 .try_into()
1805 .unwrap();
1806 let key = ed25519_dalek::SigningKey::from_bytes(&seed);
1807 assert_eq!(
1808 key.verifying_key().to_bytes().to_vec(),
1809 hex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
1810 );
1811 let sig = ed25519_dalek::Signer::sign(&key, b"");
1812 assert_eq!(
1813 sig.to_bytes().to_vec(),
1814 hex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e065224901555fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b")
1815 );
1816 }
1817
1818 #[test]
1819 fn public_key_matches_golden() {
1820 assert_eq!(BASE64.encode(generate_lan_keypair(SEED).0), PK);
1821 }
1822
1823 #[test]
1824 fn lan_signature_is_byte_identical_and_verifies() {
1825 let sig = sign_lan_jekt(&SEED, "msg-golden", "agentx", "manoz", TS, "hello fleet").unwrap();
1826 assert_eq!(sig, LAN);
1827 let pk = BASE64.decode(PK).unwrap();
1828 assert!(verify_lan_jekt(&pk, "msg-golden", "agentx", "manoz", TS, "hello fleet", LAN));
1829 assert!(!verify_lan_jekt(&pk, "msg-golden", "agentx", "manoz", TS, "hello fleet!", LAN));
1830 }
1831
1832 #[test]
1833 fn channel_signature_is_byte_identical_and_verifies() {
1834 let sig = sign_channel_jekt(&SEED, "msg-golden", "agentx", "local-main", "manoz", TS, "hello fleet").unwrap();
1835 assert_eq!(sig, CHAN);
1836 let pk = BASE64.decode(PK).unwrap();
1837 assert!(verify_channel_jekt(&pk, "msg-golden", "agentx", "local-main", "manoz", TS, "hello fleet", CHAN));
1838 }
1839
1840 #[test]
1841 fn wan_signature_is_byte_identical_and_verifies() {
1842 let sig =
1843 sign_wan_jekt(&SEED, "msg-golden", "agentx", "narko", "stable", "manoz", TS, "hello fleet").unwrap();
1844 assert_eq!(sig, WAN);
1845 let pk = BASE64.decode(PK).unwrap();
1846 assert!(verify_wan_jekt(&pk, "msg-golden", "agentx", "narko", "stable", "manoz", TS, "hello fleet", WAN));
1847 }
1848}