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::error::{MlsError, Result};
58use crate::manager::MlsManager;
59use offline_protocol_sealed::canonical_payload;
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 .map_err(MlsError::from)
247}
248
249pub fn parse_invite(blob: &str) -> Result<Invite> {
267 let bytes = URL_SAFE_NO_PAD
268 .decode(blob.trim())
269 .map_err(|e| MlsError::InvalidMessage(format!("Invite is not base64url: {}", e)))?;
270
271 let mut cursor = Reader::new(&bytes);
272
273 let version = cursor.take_u8("version")?;
274 if version != INVITE_VERSION {
275 return Err(MlsError::InvalidMessage(format!(
276 "Unsupported invite version {}, expected {}",
277 version, INVITE_VERSION
278 )));
279 }
280
281 let flags = cursor.take_u8("flags")?;
282 if flags & !KNOWN_FLAGS != 0 {
283 return Err(MlsError::InvalidMessage(format!(
284 "Invite carries unknown flag bits: {:#010b}",
285 flags
286 )));
287 }
288
289 let public_key = cursor.take("public key", PUBLIC_KEY_LEN)?.to_vec();
290
291 let address_len = cursor.take_u8("address length")? as usize;
292 let address_bytes = cursor.take("address", address_len)?;
293 let address_str = std::str::from_utf8(address_bytes)
294 .map_err(|_| MlsError::InvalidMessage("Invite address is not UTF-8".to_string()))?;
295 let address = Address::from_str(address_str)
296 .map_err(|e| MlsError::InvalidMessage(format!("Invite address is invalid: {}", e)))?;
297
298 let petname = if flags & FLAG_PETNAME != 0 {
299 let len = cursor.take_u8("petname length")? as usize;
300 if len == 0 {
301 return Err(MlsError::InvalidMessage(
304 "Invite sets the petname flag but carries an empty petname".to_string(),
305 ));
306 }
307 if len > MAX_PETNAME_BYTES {
308 return Err(MlsError::InvalidMessage(format!(
309 "Invite petname is {} bytes, maximum is {}",
310 len, MAX_PETNAME_BYTES
311 )));
312 }
313 let bytes = cursor.take("petname", len)?;
314 let name = std::str::from_utf8(bytes)
315 .map_err(|_| MlsError::InvalidMessage("Invite petname is not UTF-8".to_string()))?;
316 if contains_control_or_format(name) {
324 return Err(MlsError::InvalidMessage(
325 "Invite petname contains a control or format character".to_string(),
326 ));
327 }
328 Some(name.to_string())
329 } else {
330 None
331 };
332
333 let signature = if flags & FLAG_SIGNATURE != 0 {
334 Some(cursor.take("signature", SIGNATURE_LEN)?.to_vec())
335 } else {
336 None
337 };
338
339 cursor.finish()?;
343
344 let derived = MlsManager::derive_address(&public_key)?;
347 if derived != address {
348 return Err(MlsError::VerificationFailed(
349 "Invite address is not the address its public key derives to".to_string(),
350 ));
351 }
352
353 let signed = match signature {
354 Some(sig) => {
355 let payload = invite_signing_payload(&address, &public_key, petname.as_deref())?;
356 if !MlsManager::verify_signature(&public_key, &payload, &sig)? {
357 return Err(MlsError::VerificationFailed(
358 "Invite signature does not verify under its public key".to_string(),
359 ));
360 }
361 true
362 }
363 None => false,
364 };
365
366 Ok(Invite {
367 address,
368 public_key,
369 petname,
370 signed,
371 })
372}
373
374struct Reader<'a> {
379 bytes: &'a [u8],
380 offset: usize,
381}
382
383impl<'a> Reader<'a> {
384 fn new(bytes: &'a [u8]) -> Self {
385 Self { bytes, offset: 0 }
386 }
387
388 fn take(&mut self, field: &str, len: usize) -> Result<&'a [u8]> {
389 let end = self.offset.checked_add(len).ok_or_else(|| {
390 MlsError::InvalidMessage(format!("Invite {} length overflows", field))
391 })?;
392 if end > self.bytes.len() {
393 return Err(MlsError::InvalidMessage(format!(
394 "Invite is truncated: {} needs {} bytes, {} remain",
395 field,
396 len,
397 self.bytes.len().saturating_sub(self.offset)
398 )));
399 }
400 let slice = &self.bytes[self.offset..end];
401 self.offset = end;
402 Ok(slice)
403 }
404
405 fn take_u8(&mut self, field: &str) -> Result<u8> {
406 Ok(self.take(field, 1)?[0])
407 }
408
409 fn finish(&self) -> Result<()> {
410 if self.offset != self.bytes.len() {
411 return Err(MlsError::InvalidMessage(format!(
412 "Invite has {} trailing bytes",
413 self.bytes.len() - self.offset
414 )));
415 }
416 Ok(())
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423 use ed25519_dalek::{Signer, SigningKey};
424
425 fn identity(seed: u8) -> (SigningKey, Vec<u8>, Address) {
427 let signing = SigningKey::from_bytes(&[seed; 32]);
428 let public = signing.verifying_key().to_bytes().to_vec();
429 let address = MlsManager::derive_address(&public).expect("derive");
430 (signing, public, address)
431 }
432
433 fn sign(signing: &SigningKey, payload: &[u8]) -> Vec<u8> {
434 signing.sign(payload).to_bytes().to_vec()
435 }
436
437 #[test]
438 fn invite_round_trips_unsigned_without_a_petname() {
439 let (_, public, address) = identity(1);
440 let blob = encode_invite(&address, &public, None, None).expect("encode");
441 let invite = parse_invite(&blob).expect("parse");
442 assert_eq!(invite.address, address);
443 assert_eq!(invite.public_key, public);
444 assert_eq!(invite.petname, None);
445 assert!(!invite.signed);
446 }
447
448 #[test]
449 fn invite_round_trips_with_a_petname() {
450 let (_, public, address) = identity(2);
451 let blob = encode_invite(&address, &public, Some("Alice"), None).expect("encode");
452 let invite = parse_invite(&blob).expect("parse");
453 assert_eq!(invite.petname.as_deref(), Some("Alice"));
454 assert!(!invite.signed);
455 }
456
457 #[test]
458 fn invite_round_trips_signed() {
459 let (signing, public, address) = identity(3);
460 let payload = invite_signing_payload(&address, &public, Some("Alice")).expect("payload");
461 let signature = sign(&signing, &payload);
462 let blob =
463 encode_invite(&address, &public, Some("Alice"), Some(&signature)).expect("encode");
464 let invite = parse_invite(&blob).expect("parse");
465 assert_eq!(invite.petname.as_deref(), Some("Alice"));
466 assert!(invite.signed);
467 }
468
469 #[test]
472 fn invite_treats_an_empty_petname_as_absent() {
473 let (_, public, address) = identity(4);
474 let with_empty = encode_invite(&address, &public, Some(""), None).expect("encode");
475 let without = encode_invite(&address, &public, None, None).expect("encode");
476 assert_eq!(with_empty, without);
477 assert_eq!(parse_invite(&with_empty).expect("parse").petname, None);
478 }
479
480 #[test]
483 fn invite_refuses_an_address_that_is_not_the_keys() {
484 let (_, public_a, _) = identity(5);
485 let (_, _, address_b) = identity(6);
486
487 assert!(encode_invite(&address_b, &public_a, None, None).is_err());
489
490 let address_bytes = address_b.to_string();
493 let mut blob = vec![INVITE_VERSION, 0u8];
494 blob.extend_from_slice(&public_a);
495 blob.push(address_bytes.len() as u8);
496 blob.extend_from_slice(address_bytes.as_bytes());
497 let encoded = URL_SAFE_NO_PAD.encode(&blob);
498
499 assert!(matches!(
500 parse_invite(&encoded),
501 Err(MlsError::VerificationFailed(_))
502 ));
503 }
504
505 #[test]
507 fn invite_refuses_a_petname_swapped_under_a_signature() {
508 let (signing, public, address) = identity(7);
509 let payload = invite_signing_payload(&address, &public, Some("Alice")).expect("payload");
510 let signature = sign(&signing, &payload);
511
512 let relabeled =
515 encode_invite(&address, &public, Some("Bob"), Some(&signature)).expect("encode");
516
517 assert!(matches!(
518 parse_invite(&relabeled),
519 Err(MlsError::VerificationFailed(_))
520 ));
521 }
522
523 #[test]
527 fn invite_refuses_a_signature_by_a_foreign_key() {
528 let (_, public, address) = identity(8);
529 let (foreign_signing, _, _) = identity(9);
530 let payload = invite_signing_payload(&address, &public, None).expect("payload");
531 let signature = sign(&foreign_signing, &payload);
532 let blob = encode_invite(&address, &public, None, Some(&signature)).expect("encode");
533 assert!(matches!(
534 parse_invite(&blob),
535 Err(MlsError::VerificationFailed(_))
536 ));
537 }
538
539 #[test]
540 fn invite_refuses_an_unknown_version() {
541 let (_, public, address) = identity(10);
542 let blob = encode_invite(&address, &public, None, None).expect("encode");
543 let mut bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
544 bytes[0] = 2;
545 assert!(matches!(
546 parse_invite(&URL_SAFE_NO_PAD.encode(&bytes)),
547 Err(MlsError::InvalidMessage(_))
548 ));
549 }
550
551 #[test]
554 fn invite_refuses_unknown_flag_bits() {
555 let (_, public, address) = identity(11);
556 let blob = encode_invite(&address, &public, None, None).expect("encode");
557 let mut bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
558 bytes[1] |= 0b1000_0000;
559 assert!(matches!(
560 parse_invite(&URL_SAFE_NO_PAD.encode(&bytes)),
561 Err(MlsError::InvalidMessage(_))
562 ));
563 }
564
565 #[test]
566 fn invite_refuses_truncation_at_every_length() {
567 let (signing, public, address) = identity(12);
568 let payload = invite_signing_payload(&address, &public, Some("Alice")).expect("payload");
569 let signature = sign(&signing, &payload);
570 let blob =
571 encode_invite(&address, &public, Some("Alice"), Some(&signature)).expect("encode");
572 let bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
573
574 for cut in 0..bytes.len() {
575 let truncated = URL_SAFE_NO_PAD.encode(&bytes[..cut]);
576 assert!(
577 parse_invite(&truncated).is_err(),
578 "a blob truncated to {} bytes must not parse",
579 cut
580 );
581 }
582 }
583
584 #[test]
585 fn invite_refuses_trailing_bytes() {
586 let (_, public, address) = identity(13);
587 let blob = encode_invite(&address, &public, None, None).expect("encode");
588 let mut bytes = URL_SAFE_NO_PAD.decode(&blob).expect("decode");
589 bytes.push(0);
590 assert!(matches!(
591 parse_invite(&URL_SAFE_NO_PAD.encode(&bytes)),
592 Err(MlsError::InvalidMessage(_))
593 ));
594 }
595
596 #[test]
597 fn invite_refuses_an_over_long_petname() {
598 let (_, public, address) = identity(14);
599 let long = "a".repeat(MAX_PETNAME_BYTES + 1);
600 assert!(encode_invite(&address, &public, Some(&long), None).is_err());
601 }
602
603 #[test]
613 fn invite_refuses_a_petname_that_renders_as_another_name() {
614 let (signing, public, address) = identity(15);
615
616 for (label, name) in [
617 ("right-to-left override", "ali\u{202E}ce"),
618 ("zero-width joiner", "ali\u{200D}ce"),
619 ("soft hyphen", "ali\u{00AD}ce"),
620 ("byte-order mark", "ali\u{FEFF}ce"),
621 ("newline", "ali\nce"),
622 ] {
623 assert!(
624 encode_invite(&address, &public, Some(name), None).is_err(),
625 "a {label} petname must not be mintable"
626 );
627
628 let payload = invite_signing_payload(&address, &public, Some(name)).expect("payload");
633 let signature = sign(&signing, &payload);
634 let address_bytes = address.to_string();
635 let mut blob = vec![INVITE_VERSION, FLAG_PETNAME | FLAG_SIGNATURE];
636 blob.extend_from_slice(&public);
637 blob.push(address_bytes.len() as u8);
638 blob.extend_from_slice(address_bytes.as_bytes());
639 blob.push(name.len() as u8);
640 blob.extend_from_slice(name.as_bytes());
641 blob.extend_from_slice(&signature);
642
643 assert!(
644 matches!(
645 parse_invite(&URL_SAFE_NO_PAD.encode(&blob)),
646 Err(MlsError::InvalidMessage(_))
647 ),
648 "a {label} petname must be refused at parse even when signed"
649 );
650 }
651 }
652
653 #[test]
655 fn invite_allows_ordinary_international_petnames() {
656 let (_, public, address) = identity(16);
657 for name in ["Alice", "José", "上田", "أحمد", "Ann Lee"] {
658 let blob = encode_invite(&address, &public, Some(name), None).expect("encode");
659 assert_eq!(
660 parse_invite(&blob).expect("parse").petname.as_deref(),
661 Some(name)
662 );
663 }
664 }
665
666 #[test]
680 fn invite_golden_vectors() {
681 let (signing, public, address) = identity(1);
682 assert_eq!(
683 address.to_string(),
684 "off1qy682ruch4vlely5dkj94247jva7z49yk5xpqee0",
685 "the golden identity's address changed"
686 );
687
688 let unsigned = encode_invite(&address, &public, None, None).expect("encode");
689 assert_eq!(
690 unsigned,
691 "AQCKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXCxvZmYxcXk2ODJydWNoNHZsZWx5NWRrajk0MjQ3anZhN3o0OXlrNXhwcWVlMA"
692 );
693
694 let named = encode_invite(&address, &public, Some("alice"), None).expect("encode");
695 assert_eq!(
696 named,
697 "AQGKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXCxvZmYxcXk2ODJydWNoNHZsZWx5NWRrajk0MjQ3anZhN3o0OXlrNXhwcWVlMAVhbGljZQ"
698 );
699
700 let payload = invite_signing_payload(&address, &public, Some("alice")).expect("payload");
701 assert_eq!(
702 base64::engine::general_purpose::STANDARD.encode(&payload),
703 "b2ZmbGluZS1pbnZpdGUtdjEAAAABAQAAACxvZmYxcXk2ODJydWNoNHZsZWx5NWRrajk0MjQ3anZhN3o0OXlrNXhwcWVlMAAAACCKiOPddAnxlf1S2y08ul1yymcJvx2UEhvzdIgBtA9vXAAAAAVhbGljZQ=="
704 );
705
706 let signature = sign(&signing, &payload);
707 let signed =
708 encode_invite(&address, &public, Some("alice"), Some(&signature)).expect("encode");
709 assert!(parse_invite(&signed).expect("parse").signed);
710 assert_eq!(signed.len(), 199, "signed invite length changed");
711 }
712}