1use std::str::FromStr;
52
53use base64::engine::general_purpose::URL_SAFE_NO_PAD;
54use base64::Engine;
55use offline_protocol_core::{contains_control_or_format, Address};
56
57use crate::canonical::canonical_payload;
58use crate::error::{MlsError, Result};
59use crate::manager::MlsManager;
60
61pub const INVITE_SIGN_DOMAIN: &[u8] = b"offline-invite-v1";
66
67pub const INVITE_VERSION: u8 = 1;
69
70pub const MAX_PETNAME_BYTES: usize = 64;
76
77const PUBLIC_KEY_LEN: usize = 32;
79
80const SIGNATURE_LEN: usize = 64;
82
83const FLAG_PETNAME: u8 = 0b0000_0001;
85
86const FLAG_SIGNATURE: u8 = 0b0000_0010;
88
89const KNOWN_FLAGS: u8 = FLAG_PETNAME | FLAG_SIGNATURE;
95
96#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct Invite {
102 pub address: Address,
105 pub public_key: Vec<u8>,
107 pub petname: Option<String>,
112 pub signed: bool,
117}
118
119pub fn encode_invite(
130 address: &Address,
131 public_key: &[u8],
132 petname: Option<&str>,
133 signature: Option<&[u8]>,
134) -> Result<String> {
135 if public_key.len() != PUBLIC_KEY_LEN {
136 return Err(MlsError::InvalidPublicKey(format!(
137 "Ed25519 public key must be {} bytes, got {}",
138 PUBLIC_KEY_LEN,
139 public_key.len()
140 )));
141 }
142 let derived = MlsManager::derive_address(public_key)?;
146 if derived != *address {
147 return Err(MlsError::Serialization(
148 "Invite address is not the address this public key derives to".to_string(),
149 ));
150 }
151
152 let petname_bytes = match petname {
153 Some("") => None,
156 Some(name) => {
157 if name.len() > MAX_PETNAME_BYTES {
158 return Err(MlsError::Serialization(format!(
159 "Invite petname is {} bytes, maximum is {}",
160 name.len(),
161 MAX_PETNAME_BYTES
162 )));
163 }
164 if contains_control_or_format(name) {
168 return Err(MlsError::Serialization(
169 "Invite petname contains a control or format character".to_string(),
170 ));
171 }
172 Some(name.as_bytes())
173 }
174 None => None,
175 };
176
177 if let Some(sig) = signature {
178 if sig.len() != SIGNATURE_LEN {
179 return Err(MlsError::Serialization(format!(
180 "Invite signature must be {} bytes, got {}",
181 SIGNATURE_LEN,
182 sig.len()
183 )));
184 }
185 }
186
187 let address_string = address.to_string();
188 let address_bytes = address_string.as_bytes();
189
190 let mut flags = 0u8;
191 if petname_bytes.is_some() {
192 flags |= FLAG_PETNAME;
193 }
194 if signature.is_some() {
195 flags |= FLAG_SIGNATURE;
196 }
197
198 let mut blob = Vec::with_capacity(
199 2 + PUBLIC_KEY_LEN
200 + 1
201 + address_bytes.len()
202 + petname_bytes.map_or(0, |p| 1 + p.len())
203 + signature.map_or(0, |s| s.len()),
204 );
205 blob.push(INVITE_VERSION);
206 blob.push(flags);
207 blob.extend_from_slice(public_key);
208 blob.push(address_bytes.len() as u8);
211 blob.extend_from_slice(address_bytes);
212 if let Some(petname) = petname_bytes {
213 blob.push(petname.len() as u8);
214 blob.extend_from_slice(petname);
215 }
216 if let Some(sig) = signature {
217 blob.extend_from_slice(sig);
218 }
219
220 Ok(URL_SAFE_NO_PAD.encode(&blob))
221}
222
223pub fn invite_signing_payload(
231 address: &Address,
232 public_key: &[u8],
233 petname: Option<&str>,
234) -> Result<Vec<u8>> {
235 let address_string = address.to_string();
236 let petname_bytes = petname.unwrap_or("").as_bytes();
237 canonical_payload(
238 INVITE_SIGN_DOMAIN,
239 &[
240 &[INVITE_VERSION],
241 address_string.as_bytes(),
242 public_key,
243 petname_bytes,
244 ],
245 )
246}
247
248pub fn parse_invite(blob: &str) -> Result<Invite> {
266 let bytes = URL_SAFE_NO_PAD
267 .decode(blob.trim())
268 .map_err(|e| MlsError::InvalidMessage(format!("Invite is not base64url: {}", e)))?;
269
270 let mut cursor = Reader::new(&bytes);
271
272 let version = cursor.take_u8("version")?;
273 if version != INVITE_VERSION {
274 return Err(MlsError::InvalidMessage(format!(
275 "Unsupported invite version {}, expected {}",
276 version, INVITE_VERSION
277 )));
278 }
279
280 let flags = cursor.take_u8("flags")?;
281 if flags & !KNOWN_FLAGS != 0 {
282 return Err(MlsError::InvalidMessage(format!(
283 "Invite carries unknown flag bits: {:#010b}",
284 flags
285 )));
286 }
287
288 let public_key = cursor.take("public key", PUBLIC_KEY_LEN)?.to_vec();
289
290 let address_len = cursor.take_u8("address length")? as usize;
291 let address_bytes = cursor.take("address", address_len)?;
292 let address_str = std::str::from_utf8(address_bytes)
293 .map_err(|_| MlsError::InvalidMessage("Invite address is not UTF-8".to_string()))?;
294 let address = Address::from_str(address_str)
295 .map_err(|e| MlsError::InvalidMessage(format!("Invite address is invalid: {}", e)))?;
296
297 let petname = if flags & FLAG_PETNAME != 0 {
298 let len = cursor.take_u8("petname length")? as usize;
299 if len == 0 {
300 return Err(MlsError::InvalidMessage(
303 "Invite sets the petname flag but carries an empty petname".to_string(),
304 ));
305 }
306 if len > MAX_PETNAME_BYTES {
307 return Err(MlsError::InvalidMessage(format!(
308 "Invite petname is {} bytes, maximum is {}",
309 len, MAX_PETNAME_BYTES
310 )));
311 }
312 let bytes = cursor.take("petname", len)?;
313 let name = std::str::from_utf8(bytes)
314 .map_err(|_| MlsError::InvalidMessage("Invite petname is not UTF-8".to_string()))?;
315 if contains_control_or_format(name) {
323 return Err(MlsError::InvalidMessage(
324 "Invite petname contains a control or format character".to_string(),
325 ));
326 }
327 Some(name.to_string())
328 } else {
329 None
330 };
331
332 let signature = if flags & FLAG_SIGNATURE != 0 {
333 Some(cursor.take("signature", SIGNATURE_LEN)?.to_vec())
334 } else {
335 None
336 };
337
338 cursor.finish()?;
342
343 let derived = MlsManager::derive_address(&public_key)?;
346 if derived != address {
347 return Err(MlsError::VerificationFailed(
348 "Invite address is not the address its public key derives to".to_string(),
349 ));
350 }
351
352 let signed = match signature {
353 Some(sig) => {
354 let payload = invite_signing_payload(&address, &public_key, petname.as_deref())?;
355 if !MlsManager::verify_signature(&public_key, &payload, &sig)? {
356 return Err(MlsError::VerificationFailed(
357 "Invite signature does not verify under its public key".to_string(),
358 ));
359 }
360 true
361 }
362 None => false,
363 };
364
365 Ok(Invite {
366 address,
367 public_key,
368 petname,
369 signed,
370 })
371}
372
373struct Reader<'a> {
378 bytes: &'a [u8],
379 offset: usize,
380}
381
382impl<'a> Reader<'a> {
383 fn new(bytes: &'a [u8]) -> Self {
384 Self { bytes, offset: 0 }
385 }
386
387 fn take(&mut self, field: &str, len: usize) -> Result<&'a [u8]> {
388 let end = self.offset.checked_add(len).ok_or_else(|| {
389 MlsError::InvalidMessage(format!("Invite {} length overflows", field))
390 })?;
391 if end > self.bytes.len() {
392 return Err(MlsError::InvalidMessage(format!(
393 "Invite is truncated: {} needs {} bytes, {} remain",
394 field,
395 len,
396 self.bytes.len().saturating_sub(self.offset)
397 )));
398 }
399 let slice = &self.bytes[self.offset..end];
400 self.offset = end;
401 Ok(slice)
402 }
403
404 fn take_u8(&mut self, field: &str) -> Result<u8> {
405 Ok(self.take(field, 1)?[0])
406 }
407
408 fn finish(&self) -> Result<()> {
409 if self.offset != self.bytes.len() {
410 return Err(MlsError::InvalidMessage(format!(
411 "Invite has {} trailing bytes",
412 self.bytes.len() - self.offset
413 )));
414 }
415 Ok(())
416 }
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use ed25519_dalek::{Signer, SigningKey};
423
424 fn identity(seed: u8) -> (SigningKey, Vec<u8>, Address) {
426 let signing = SigningKey::from_bytes(&[seed; 32]);
427 let public = signing.verifying_key().to_bytes().to_vec();
428 let address = MlsManager::derive_address(&public).expect("derive");
429 (signing, public, address)
430 }
431
432 fn sign(signing: &SigningKey, payload: &[u8]) -> Vec<u8> {
433 signing.sign(payload).to_bytes().to_vec()
434 }
435
436 #[test]
437 fn invite_round_trips_unsigned_without_a_petname() {
438 let (_, public, address) = identity(1);
439 let blob = encode_invite(&address, &public, None, None).expect("encode");
440 let invite = parse_invite(&blob).expect("parse");
441 assert_eq!(invite.address, address);
442 assert_eq!(invite.public_key, public);
443 assert_eq!(invite.petname, None);
444 assert!(!invite.signed);
445 }
446
447 #[test]
448 fn invite_round_trips_with_a_petname() {
449 let (_, public, address) = identity(2);
450 let blob = encode_invite(&address, &public, Some("Alice"), None).expect("encode");
451 let invite = parse_invite(&blob).expect("parse");
452 assert_eq!(invite.petname.as_deref(), Some("Alice"));
453 assert!(!invite.signed);
454 }
455
456 #[test]
457 fn invite_round_trips_signed() {
458 let (signing, public, address) = identity(3);
459 let payload = invite_signing_payload(&address, &public, Some("Alice")).expect("payload");
460 let signature = sign(&signing, &payload);
461 let blob =
462 encode_invite(&address, &public, Some("Alice"), Some(&signature)).expect("encode");
463 let invite = parse_invite(&blob).expect("parse");
464 assert_eq!(invite.petname.as_deref(), Some("Alice"));
465 assert!(invite.signed);
466 }
467
468 #[test]
471 fn invite_treats_an_empty_petname_as_absent() {
472 let (_, public, address) = identity(4);
473 let with_empty = encode_invite(&address, &public, Some(""), None).expect("encode");
474 let without = encode_invite(&address, &public, None, None).expect("encode");
475 assert_eq!(with_empty, without);
476 assert_eq!(parse_invite(&with_empty).expect("parse").petname, None);
477 }
478
479 #[test]
482 fn invite_refuses_an_address_that_is_not_the_keys() {
483 let (_, public_a, _) = identity(5);
484 let (_, _, address_b) = identity(6);
485
486 assert!(encode_invite(&address_b, &public_a, None, None).is_err());
488
489 let address_bytes = address_b.to_string();
492 let mut blob = vec![INVITE_VERSION, 0u8];
493 blob.extend_from_slice(&public_a);
494 blob.push(address_bytes.len() as u8);
495 blob.extend_from_slice(address_bytes.as_bytes());
496 let encoded = URL_SAFE_NO_PAD.encode(&blob);
497
498 assert!(matches!(
499 parse_invite(&encoded),
500 Err(MlsError::VerificationFailed(_))
501 ));
502 }
503
504 #[test]
506 fn invite_refuses_a_petname_swapped_under_a_signature() {
507 let (signing, public, address) = identity(7);
508 let payload = invite_signing_payload(&address, &public, Some("Alice")).expect("payload");
509 let signature = sign(&signing, &payload);
510
511 let relabeled =
514 encode_invite(&address, &public, Some("Bob"), Some(&signature)).expect("encode");
515
516 assert!(matches!(
517 parse_invite(&relabeled),
518 Err(MlsError::VerificationFailed(_))
519 ));
520 }
521
522 #[test]
526 fn invite_refuses_a_signature_by_a_foreign_key() {
527 let (_, public, address) = identity(8);
528 let (foreign_signing, _, _) = identity(9);
529 let payload = invite_signing_payload(&address, &public, None).expect("payload");
530 let signature = sign(&foreign_signing, &payload);
531 let blob = encode_invite(&address, &public, None, Some(&signature)).expect("encode");
532 assert!(matches!(
533 parse_invite(&blob),
534 Err(MlsError::VerificationFailed(_))
535 ));
536 }
537
538 #[test]
539 fn invite_refuses_an_unknown_version() {
540 let (_, public, address) = identity(10);
541 let blob = encode_invite(&address, &public, None, None).expect("encode");
542 let mut bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
543 bytes[0] = 2;
544 assert!(matches!(
545 parse_invite(&URL_SAFE_NO_PAD.encode(&bytes)),
546 Err(MlsError::InvalidMessage(_))
547 ));
548 }
549
550 #[test]
553 fn invite_refuses_unknown_flag_bits() {
554 let (_, public, address) = identity(11);
555 let blob = encode_invite(&address, &public, None, None).expect("encode");
556 let mut bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
557 bytes[1] |= 0b1000_0000;
558 assert!(matches!(
559 parse_invite(&URL_SAFE_NO_PAD.encode(&bytes)),
560 Err(MlsError::InvalidMessage(_))
561 ));
562 }
563
564 #[test]
565 fn invite_refuses_truncation_at_every_length() {
566 let (signing, public, address) = identity(12);
567 let payload = invite_signing_payload(&address, &public, Some("Alice")).expect("payload");
568 let signature = sign(&signing, &payload);
569 let blob =
570 encode_invite(&address, &public, Some("Alice"), Some(&signature)).expect("encode");
571 let bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
572
573 for cut in 0..bytes.len() {
574 let truncated = URL_SAFE_NO_PAD.encode(&bytes[..cut]);
575 assert!(
576 parse_invite(&truncated).is_err(),
577 "a blob truncated to {} bytes must not parse",
578 cut
579 );
580 }
581 }
582
583 #[test]
584 fn invite_refuses_trailing_bytes() {
585 let (_, public, address) = identity(13);
586 let blob = encode_invite(&address, &public, None, None).expect("encode");
587 let mut bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
588 bytes.push(0);
589 assert!(matches!(
590 parse_invite(&URL_SAFE_NO_PAD.encode(&bytes)),
591 Err(MlsError::InvalidMessage(_))
592 ));
593 }
594
595 #[test]
596 fn invite_refuses_an_over_long_petname() {
597 let (_, public, address) = identity(14);
598 let long = "a".repeat(MAX_PETNAME_BYTES + 1);
599 assert!(encode_invite(&address, &public, Some(&long), None).is_err());
600 }
601
602 #[test]
612 fn invite_refuses_a_petname_that_renders_as_another_name() {
613 let (signing, public, address) = identity(15);
614
615 for (label, name) in [
616 ("right-to-left override", "ali\u{202E}ce"),
617 ("zero-width joiner", "ali\u{200D}ce"),
618 ("soft hyphen", "ali\u{00AD}ce"),
619 ("byte-order mark", "ali\u{FEFF}ce"),
620 ("newline", "ali\nce"),
621 ] {
622 assert!(
623 encode_invite(&address, &public, Some(name), None).is_err(),
624 "a {label} petname must not be mintable"
625 );
626
627 let payload = invite_signing_payload(&address, &public, Some(name)).expect("payload");
632 let signature = sign(&signing, &payload);
633 let address_bytes = address.to_string();
634 let mut blob = vec![INVITE_VERSION, FLAG_PETNAME | FLAG_SIGNATURE];
635 blob.extend_from_slice(&public);
636 blob.push(address_bytes.len() as u8);
637 blob.extend_from_slice(address_bytes.as_bytes());
638 blob.push(name.len() as u8);
639 blob.extend_from_slice(name.as_bytes());
640 blob.extend_from_slice(&signature);
641
642 assert!(
643 matches!(
644 parse_invite(&URL_SAFE_NO_PAD.encode(&blob)),
645 Err(MlsError::InvalidMessage(_))
646 ),
647 "a {label} petname must be refused at parse even when signed"
648 );
649 }
650 }
651
652 #[test]
654 fn invite_allows_ordinary_international_petnames() {
655 let (_, public, address) = identity(16);
656 for name in ["Alice", "José", "上田", "أحمد", "Ann Lee"] {
657 let blob = encode_invite(&address, &public, Some(name), None).expect("encode");
658 assert_eq!(
659 parse_invite(&blob).expect("parse").petname.as_deref(),
660 Some(name)
661 );
662 }
663 }
664
665 #[test]
679 fn invite_golden_vectors() {
680 let (signing, public, address) = identity(1);
681 assert_eq!(
682 address.to_string(),
683 "off1qy682ruch4vlely5dkj94247jva7z49yk5xpqee0",
684 "the golden identity's address changed"
685 );
686
687 let unsigned = encode_invite(&address, &public, None, None).expect("encode");
688 assert_eq!(
689 unsigned,
690 "AQCKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXCxvZmYxcXk2ODJydWNoNHZsZWx5NWRrajk0MjQ3anZhN3o0OXlrNXhwcWVlMA"
691 );
692
693 let named = encode_invite(&address, &public, Some("alice"), None).expect("encode");
694 assert_eq!(
695 named,
696 "AQGKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXCxvZmYxcXk2ODJydWNoNHZsZWx5NWRrajk0MjQ3anZhN3o0OXlrNXhwcWVlMAVhbGljZQ"
697 );
698
699 let payload = invite_signing_payload(&address, &public, Some("alice")).expect("payload");
700 assert_eq!(
701 base64::engine::general_purpose::STANDARD.encode(&payload),
702 "b2ZmbGluZS1pbnZpdGUtdjEAAAABAQAAACxvZmYxcXk2ODJydWNoNHZsZWx5NWRrajk0MjQ3anZhN3o0OXlrNXhwcWVlMAAAACCKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXAAAAAVhbGljZQ=="
703 );
704
705 let signature = sign(&signing, &payload);
706 let signed =
707 encode_invite(&address, &public, Some("alice"), Some(&signature)).expect("encode");
708 assert!(parse_invite(&signed).expect("parse").signed);
709 assert_eq!(signed.len(), 199, "signed invite length changed");
710 }
711}