umsh_cli/peer_ref.rs
1//! `<peer-ref>` token resolution used by every peer-naming command.
2//!
3//! Accepts any of:
4//! - a registered alias (ASCII, up to 16 chars),
5//! - the canonical fixed-width base58 address (exactly 44 chars),
6//! - hex-encoded 32-byte pubkey (64 hex chars, optional `0x` prefix),
7//! - base64-encoded 32-byte pubkey (standard or URL-safe alphabet).
8//!
9//! 3-byte hints are NOT accepted — they aren't unique and the CLI refuses
10//! to guess.
11
12use umsh_core::PublicKey;
13
14/// Decode a `<peer-ref>` token into a `PublicKey`. Alias lookup is the
15/// caller's concern; this helper only handles the encoded forms.
16///
17/// The canonical forms (44-char base58 and 64-char hex, distinguished by
18/// length) are tried first via [`PublicKey::from_str`], then base64.
19///
20/// After successful decoding the bytes are validated as a well-formed
21/// Ed25519 compressed public-key point on the curve (when the
22/// `software-crypto` feature is enabled). Bytes that decode to 32 bytes but
23/// do not lie on the curve return `None` so a typo'd hex string can never
24/// be accepted as a peer key.
25///
26/// Returns `None` if the token doesn't decode to a full 32-byte key in any
27/// of the supported encodings, or if the decoded bytes are not a valid
28/// Ed25519 point.
29pub fn try_parse_pubkey(token: &str) -> Option<PublicKey> {
30 let canonical = token.strip_prefix("0x").unwrap_or(token);
31 let key = canonical
32 .parse::<PublicKey>()
33 .ok()
34 .or_else(|| try_b64(token))?;
35 #[cfg(feature = "software-crypto")]
36 {
37 if !umsh_crypto::is_valid_ed25519_public_key(&key) {
38 return None;
39 }
40 }
41 Some(key)
42}
43
44fn try_b64(token: &str) -> Option<PublicKey> {
45 // Minimal base64 decoder for 32-byte keys. Supports both standard and
46 // URL-safe alphabets; padding is optional.
47 let bytes = token.as_bytes();
48 if bytes.is_empty() {
49 return None;
50 }
51 // Strip trailing '=' padding.
52 let end = bytes
53 .iter()
54 .rposition(|&b| b != b'=')
55 .map(|i| i + 1)
56 .unwrap_or(0);
57 let src = &bytes[..end];
58
59 let mut out = [0u8; 32];
60 let mut buf: u32 = 0;
61 let mut bits: u32 = 0;
62 let mut oi = 0;
63
64 for &b in src {
65 let v: u32 = match b {
66 b'A'..=b'Z' => (b - b'A') as u32,
67 b'a'..=b'z' => (b - b'a') as u32 + 26,
68 b'0'..=b'9' => (b - b'0') as u32 + 52,
69 b'+' | b'-' => 62,
70 b'/' | b'_' => 63,
71 _ => return None,
72 };
73 buf = (buf << 6) | v;
74 bits += 6;
75 if bits >= 8 {
76 bits -= 8;
77 if oi >= 32 {
78 return None;
79 }
80 out[oi] = ((buf >> bits) & 0xFF) as u8;
81 oi += 1;
82 }
83 }
84 if oi == 32 { Some(PublicKey(out)) } else { None }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use umsh_crypto::NodeIdentity;
91 use umsh_crypto::software::SoftwareIdentity;
92
93 /// A real Ed25519 public key derived from a known seed. Generated at
94 /// test time so the constant cannot silently drift away from a
95 /// valid point on the curve.
96 fn valid_key() -> [u8; 32] {
97 SoftwareIdentity::from_secret_bytes(&[0x11; 32])
98 .public_key()
99 .0
100 }
101
102 #[test]
103 fn decodes_hex() {
104 let key = valid_key();
105 let hex = std::format!("{:x}", PublicKey(key));
106 assert_eq!(try_parse_pubkey(&hex).unwrap().0, key);
107 let prefixed = std::format!("0x{}", hex.to_uppercase());
108 assert_eq!(try_parse_pubkey(&prefixed).unwrap().0, key);
109 }
110
111 #[test]
112 fn decodes_base58() {
113 let key = valid_key();
114 let b58 = PublicKey(key).to_string();
115 assert_eq!(b58.len(), 44);
116 assert_eq!(try_parse_pubkey(&b58).unwrap().0, key);
117 }
118
119 #[test]
120 fn rejects_short_or_junk() {
121 assert!(try_parse_pubkey("").is_none());
122 assert!(try_parse_pubkey("hello").is_none());
123 assert!(try_parse_pubkey("0x01").is_none());
124 }
125
126 #[test]
127 fn rejects_non_curve_point() {
128 // Y = 2 (little-endian) is well-formed hex but does not lie on
129 // the Ed25519 curve. Without curve validation this would have
130 // been blindly accepted as a peer key and only failed later at
131 // ECDH time with `IdentityAgreementFailed`.
132 let bogus = "0200000000000000000000000000000000000000000000000000000000000000";
133 assert!(try_parse_pubkey(bogus).is_none());
134 }
135}