1use std::cmp::Ordering;
4use std::fmt::{Display, Formatter};
5
6use hmac::{Hmac, Mac};
7use serde::{Deserialize, Serialize};
8use sha2::Sha256;
9use subtle::ConstantTimeEq;
10use thiserror::Error;
11use time::{Month, OffsetDateTime};
12use url::Url;
13
14#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
16pub enum TransportKind {
17 Http,
19 Https,
21 Smtp,
23 Udp,
25 Ssh,
27 Other(String),
29}
30
31impl TransportKind {
32 #[must_use]
34 pub fn is_secure_channel(&self) -> bool {
35 matches!(self, Self::Https | Self::Ssh)
36 }
37}
38
39#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
41pub enum Signature {
42 Unsigned,
44 Sha256([u8; 32]),
46}
47
48impl Signature {
49 #[must_use]
51 pub fn line_without_crlf(&self) -> String {
52 match self {
53 Self::Unsigned => "0".to_owned(),
54 Self::Sha256(digest) => {
55 let mut out = String::with_capacity(65);
56 out.push('1');
57 out.push_str(&hex::encode(digest));
58 out
59 }
60 }
61 }
62
63 #[must_use]
65 pub fn verifies(&self, payload_without_signature_line: &[u8], key: Option<&[u8]>) -> bool {
66 match self {
67 Self::Unsigned => true,
68 Self::Sha256(expected) => {
69 let Some(key) = key else {
70 return false;
71 };
72 let Ok(mut mac) = Hmac::<Sha256>::new_from_slice(key) else {
73 return false;
74 };
75 mac.update(payload_without_signature_line);
76 let digest = mac.finalize().into_bytes();
77 let mut actual = [0_u8; 32];
78 actual.copy_from_slice(&digest[..32]);
79 bool::from(actual.ct_eq(expected))
80 }
81 }
82 }
83
84 #[must_use]
86 pub fn sign(payload_without_signature_line: &[u8], key: &[u8]) -> Self {
87 let mut mac = Hmac::<Sha256>::new_from_slice(key).expect("HMAC supports all key lengths");
88 mac.update(payload_without_signature_line);
89 let digest = mac.finalize().into_bytes();
90 let mut out = [0_u8; 32];
91 out.copy_from_slice(&digest[..32]);
92 Self::Sha256(out)
93 }
94}
95
96#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
98pub struct CmrTimestamp {
99 year: u16,
100 month: u8,
101 day: u8,
102 hour: u8,
103 minute: u8,
104 second: u8,
105 fraction: String,
106}
107
108impl CmrTimestamp {
109 #[must_use]
111 pub fn now_utc() -> Self {
112 let now = OffsetDateTime::now_utc();
113 let fraction = format!("{:09}", now.nanosecond())
114 .trim_end_matches('0')
115 .to_owned();
116 Self {
117 year: u16::try_from(now.year()).unwrap_or(0),
118 month: now.month() as u8,
119 day: now.day(),
120 hour: now.hour(),
121 minute: now.minute(),
122 second: now.second(),
123 fraction,
124 }
125 }
126
127 pub fn parse(input: &str) -> Result<Self, ParseError> {
129 if input.len() < 19 {
130 return Err(ParseError::InvalidTimestamp(input.to_owned()));
131 }
132 let year = parse_dec_u16(&input[0..4], "year", input)?;
133 if input.as_bytes().get(4) != Some(&b'/') {
134 return Err(ParseError::InvalidTimestamp(input.to_owned()));
135 }
136 let month = parse_dec_u8(&input[5..7], "month", input)?;
137 if input.as_bytes().get(7) != Some(&b'/') {
138 return Err(ParseError::InvalidTimestamp(input.to_owned()));
139 }
140 let day = parse_dec_u8(&input[8..10], "day", input)?;
141 if input.as_bytes().get(10) != Some(&b' ') {
142 return Err(ParseError::InvalidTimestamp(input.to_owned()));
143 }
144 let hour = parse_dec_u8(&input[11..13], "hour", input)?;
145 if input.as_bytes().get(13) != Some(&b':') {
146 return Err(ParseError::InvalidTimestamp(input.to_owned()));
147 }
148 let minute = parse_dec_u8(&input[14..16], "minute", input)?;
149 if input.as_bytes().get(16) != Some(&b':') {
150 return Err(ParseError::InvalidTimestamp(input.to_owned()));
151 }
152 let second = parse_dec_u8(&input[17..19], "second", input)?;
153 let fraction = if input.len() == 19 {
154 String::new()
155 } else {
156 if input.as_bytes().get(19) != Some(&b'.') {
157 return Err(ParseError::InvalidTimestamp(input.to_owned()));
158 }
159 let frac = &input[20..];
160 if frac.is_empty() || !frac.bytes().all(|b| b.is_ascii_digit()) {
161 return Err(ParseError::InvalidTimestamp(input.to_owned()));
162 }
163 frac.to_owned()
164 };
165 validate_calendar_parts(year, month, day, hour, minute, second, input)?;
166 Ok(Self {
167 year,
168 month,
169 day,
170 hour,
171 minute,
172 second,
173 fraction,
174 })
175 }
176
177 #[must_use]
179 pub fn with_fraction(mut self, fraction: String) -> Self {
180 self.fraction = fraction;
181 self
182 }
183
184 #[must_use]
186 pub fn is_newer_than(&self, other: &Self) -> bool {
187 self > other
188 }
189}
190
191impl Display for CmrTimestamp {
192 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
193 write!(
194 f,
195 "{:04}/{:02}/{:02} {:02}:{:02}:{:02}",
196 self.year, self.month, self.day, self.hour, self.minute, self.second
197 )?;
198 if !self.fraction.is_empty() {
199 write!(f, ".{}", self.fraction)?;
200 }
201 Ok(())
202 }
203}
204
205impl Ord for CmrTimestamp {
206 fn cmp(&self, other: &Self) -> Ordering {
207 let cmp_tuple = (
208 self.year,
209 self.month,
210 self.day,
211 self.hour,
212 self.minute,
213 self.second,
214 )
215 .cmp(&(
216 other.year,
217 other.month,
218 other.day,
219 other.hour,
220 other.minute,
221 other.second,
222 ));
223 if cmp_tuple != Ordering::Equal {
224 return cmp_tuple;
225 }
226 compare_fractional_decimal(&self.fraction, &other.fraction)
227 }
228}
229
230impl PartialOrd for CmrTimestamp {
231 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
232 Some(self.cmp(other))
233 }
234}
235
236#[derive(Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
238pub struct MessageId {
239 pub timestamp: CmrTimestamp,
241 pub address: String,
243}
244
245impl MessageId {
246 pub fn parse(input: &str) -> Result<Self, ParseError> {
248 let split_at = input
249 .char_indices()
250 .skip(19)
251 .find_map(|(idx, ch)| (ch == ' ').then_some(idx))
252 .ok_or_else(|| ParseError::InvalidMessageId(input.to_owned()))?;
253 let ts = &input[..split_at];
254 let address = &input[(split_at + 1)..];
255 if address.is_empty() || address.contains('\r') || address.contains('\n') {
256 return Err(ParseError::InvalidMessageId(input.to_owned()));
257 }
258 if let Some(parsed) = address
259 .contains("://")
260 .then(|| Url::parse(address))
261 .transpose()
262 .map_err(|_| ParseError::InvalidAddress(address.to_owned()))?
263 && parsed.scheme().is_empty()
264 {
265 return Err(ParseError::InvalidAddress(address.to_owned()));
266 }
267 Ok(Self {
268 timestamp: CmrTimestamp::parse(ts)?,
269 address: address.to_owned(),
270 })
271 }
272
273 #[must_use]
275 pub fn line_without_crlf(&self) -> String {
276 format!("{} {}", self.timestamp, self.address)
277 }
278}
279
280impl Display for MessageId {
281 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
282 write!(f, "{} {}", self.timestamp, self.address)
283 }
284}
285
286#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
288pub struct CmrMessage {
289 pub signature: Signature,
291 pub header: Vec<MessageId>,
293 pub body: Vec<u8>,
295}
296
297impl CmrMessage {
298 #[must_use]
300 pub fn immediate_sender(&self) -> &str {
301 self.header.first().map_or("", |id| id.address.as_str())
302 }
303
304 #[must_use]
306 pub fn origin_id(&self) -> Option<&MessageId> {
307 self.header.last()
308 }
309
310 #[must_use]
312 pub fn payload_without_signature_line(&self) -> Vec<u8> {
313 let mut out = Vec::with_capacity(self.encoded_len().saturating_sub(4));
314 for id in &self.header {
315 out.extend_from_slice(id.line_without_crlf().as_bytes());
316 out.extend_from_slice(b"\r\n");
317 }
318 out.extend_from_slice(b"\r\n");
319 out.extend_from_slice(self.body.len().to_string().as_bytes());
320 out.extend_from_slice(b"\r\n");
321 out.extend_from_slice(&self.body);
322 out
323 }
324
325 #[must_use]
327 pub fn to_bytes(&self) -> Vec<u8> {
328 let mut out = Vec::with_capacity(self.encoded_len());
329 out.extend_from_slice(self.signature.line_without_crlf().as_bytes());
330 out.extend_from_slice(b"\r\n");
331 out.extend_from_slice(&self.payload_without_signature_line());
332 out
333 }
334
335 #[must_use]
337 pub fn encoded_len(&self) -> usize {
338 let header_len: usize = self
339 .header
340 .iter()
341 .map(|id| id.line_without_crlf().len() + 2)
342 .sum();
343 self.signature.line_without_crlf().len()
344 + 2
345 + header_len
346 + 2
347 + self.body.len().to_string().len()
348 + 2
349 + self.body.len()
350 }
351
352 pub fn make_unsigned(&mut self) {
354 self.signature = Signature::Unsigned;
355 }
356
357 pub fn sign_with_key(&mut self, key: &[u8]) {
359 self.signature = Signature::sign(&self.payload_without_signature_line(), key);
360 }
361
362 pub fn prepend_hop(&mut self, hop: MessageId) {
364 self.header.insert(0, hop);
365 }
366}
367
368#[derive(Clone, Debug)]
370pub struct ParseContext<'a> {
371 pub now: CmrTimestamp,
373 pub recipient_address: Option<&'a str>,
375 pub max_message_bytes: usize,
377 pub max_header_ids: usize,
379}
380
381impl<'a> ParseContext<'a> {
382 #[must_use]
384 pub fn secure(now: CmrTimestamp, recipient_address: Option<&'a str>) -> Self {
385 Self {
386 now,
387 recipient_address,
388 max_message_bytes: 4 * 1024 * 1024,
389 max_header_ids: 1024,
390 }
391 }
392}
393
394#[derive(Debug, Error)]
396pub enum ParseError {
397 #[error("message exceeds configured size limit")]
399 TooLarge,
400 #[error("line is not valid utf-8")]
402 NonUtf8Line,
403 #[error("invalid signature line")]
405 InvalidSignature,
406 #[error("invalid timestamp `{0}`")]
408 InvalidTimestamp(String),
409 #[error("invalid message id `{0}`")]
411 InvalidMessageId(String),
412 #[error("invalid address `{0}`")]
414 InvalidAddress(String),
415 #[error("recipient address appears in routing header")]
417 RecipientAddressInHeader,
418 #[error("duplicate address in routing header")]
420 DuplicateAddress,
421 #[error("routing header timestamps are not strictly descending")]
423 NonDescendingTimestamps,
424 #[error("routing header contains future timestamp")]
426 FutureTimestamp,
427 #[error("routing header is empty")]
429 EmptyHeader,
430 #[error("malformed CRLF sequence")]
432 MissingCrlf,
433 #[error("invalid body length field")]
435 InvalidBodyLength,
436 #[error("body length mismatch")]
438 BodyLengthMismatch,
439 #[error("too many header entries")]
441 TooManyHeaderIds,
442}
443
444pub fn parse_message(input: &[u8], ctx: &ParseContext<'_>) -> Result<CmrMessage, ParseError> {
446 if input.len() > ctx.max_message_bytes {
447 return Err(ParseError::TooLarge);
448 }
449 let (sig_line, mut rest) = take_crlf_line(input)?;
450 let sig_line = std::str::from_utf8(sig_line).map_err(|_| ParseError::NonUtf8Line)?;
451 let signature = parse_signature_line(sig_line)?;
452
453 let mut header = Vec::new();
454 loop {
455 let (line, r) = take_crlf_line(rest)?;
456 rest = r;
457 if line.is_empty() {
458 break;
459 }
460 if header.len() >= ctx.max_header_ids {
461 return Err(ParseError::TooManyHeaderIds);
462 }
463 let line = std::str::from_utf8(line).map_err(|_| ParseError::NonUtf8Line)?;
464 header.push(MessageId::parse(line)?);
465 }
466 if header.is_empty() {
467 return Err(ParseError::EmptyHeader);
468 }
469 validate_header(&header, ctx)?;
470
471 let (len_line, body_bytes) = take_crlf_line(rest)?;
472 let len_line = std::str::from_utf8(len_line).map_err(|_| ParseError::NonUtf8Line)?;
473 if len_line.is_empty() || !len_line.bytes().all(|b| b.is_ascii_digit()) {
474 return Err(ParseError::InvalidBodyLength);
475 }
476 let body_len = len_line
477 .parse::<usize>()
478 .map_err(|_| ParseError::InvalidBodyLength)?;
479 if body_len > ctx.max_message_bytes {
480 return Err(ParseError::TooLarge);
481 }
482 if body_bytes.len() != body_len {
483 return Err(ParseError::BodyLengthMismatch);
484 }
485
486 Ok(CmrMessage {
487 signature,
488 header,
489 body: body_bytes.to_vec(),
490 })
491}
492
493fn parse_signature_line(line: &str) -> Result<Signature, ParseError> {
494 if line == "0" {
495 return Ok(Signature::Unsigned);
496 }
497 if line.len() == 65 && line.starts_with('1') && is_lower_hex(&line[1..]) {
498 let mut digest = [0_u8; 32];
499 hex::decode_to_slice(&line[1..], &mut digest).map_err(|_| ParseError::InvalidSignature)?;
500 return Ok(Signature::Sha256(digest));
501 }
502 Err(ParseError::InvalidSignature)
503}
504
505fn validate_header(header: &[MessageId], ctx: &ParseContext<'_>) -> Result<(), ParseError> {
506 let mut addresses = std::collections::HashSet::<&str>::with_capacity(header.len());
507 for (idx, id) in header.iter().enumerate() {
508 if ctx
509 .recipient_address
510 .is_some_and(|recipient| same_address_alias(id.address.as_str(), recipient))
511 {
512 return Err(ParseError::RecipientAddressInHeader);
513 }
514 if !addresses.insert(id.address.as_str()) {
515 return Err(ParseError::DuplicateAddress);
516 }
517 if id.timestamp > ctx.now {
518 return Err(ParseError::FutureTimestamp);
519 }
520 if idx > 0 && id.timestamp >= header[idx - 1].timestamp {
521 return Err(ParseError::NonDescendingTimestamps);
522 }
523 }
524 Ok(())
525}
526
527fn same_address_alias(left: &str, right: &str) -> bool {
528 left == right || left.trim_end_matches('/') == right.trim_end_matches('/')
529}
530
531fn take_crlf_line(mut input: &[u8]) -> Result<(&[u8], &[u8]), ParseError> {
532 let mut i = 0;
533 while i + 1 < input.len() {
534 if input[i] == b'\r' {
535 if input[i + 1] != b'\n' {
536 return Err(ParseError::MissingCrlf);
537 }
538 let line = &input[..i];
539 input = &input[(i + 2)..];
540 return Ok((line, input));
541 }
542 i += 1;
543 }
544 Err(ParseError::MissingCrlf)
545}
546
547fn parse_dec_u16(input: &str, _field: &str, full: &str) -> Result<u16, ParseError> {
548 if input.len() != 4 || !input.bytes().all(|b| b.is_ascii_digit()) {
549 return Err(ParseError::InvalidTimestamp(full.to_owned()));
550 }
551 input
552 .parse::<u16>()
553 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))
554}
555
556fn parse_dec_u8(input: &str, _field: &str, full: &str) -> Result<u8, ParseError> {
557 if input.len() != 2 || !input.bytes().all(|b| b.is_ascii_digit()) {
558 return Err(ParseError::InvalidTimestamp(full.to_owned()));
559 }
560 input
561 .parse::<u8>()
562 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))
563}
564
565fn validate_calendar_parts(
566 year: u16,
567 month: u8,
568 day: u8,
569 hour: u8,
570 minute: u8,
571 second: u8,
572 full: &str,
573) -> Result<(), ParseError> {
574 if hour > 23 || minute > 59 || second > 59 {
575 return Err(ParseError::InvalidTimestamp(full.to_owned()));
576 }
577 let month =
578 Month::try_from(month).map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))?;
579 let date = time::Date::from_calendar_date(i32::from(year), month, day)
580 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))?;
581 let _ = date
582 .with_hms(hour, minute, second)
583 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))?;
584 Ok(())
585}
586
587fn compare_fractional_decimal(a: &str, b: &str) -> Ordering {
588 let max_len = a.len().max(b.len());
589 for i in 0..max_len {
590 let ad = a.as_bytes().get(i).copied().unwrap_or(b'0');
591 let bd = b.as_bytes().get(i).copied().unwrap_or(b'0');
592 match ad.cmp(&bd) {
593 Ordering::Equal => {}
594 non_eq => return non_eq,
595 }
596 }
597 Ordering::Equal
598}
599
600fn is_lower_hex(s: &str) -> bool {
601 s.bytes()
602 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
603}
604
605#[cfg(test)]
606mod tests {
607 use super::*;
608
609 fn ctx<'a>(recipient: Option<&'a str>) -> ParseContext<'a> {
610 ParseContext::secure(
611 CmrTimestamp::parse("2030/01/01 00:00:00").expect("valid time"),
612 recipient,
613 )
614 }
615
616 #[test]
617 fn parse_round_trip_unsigned() {
618 let raw = b"0\r\n2029/12/31 23:59:59 http://alice\r\n\r\n5\r\nhello";
619 let parsed = parse_message(raw, &ctx(Some("http://bob"))).expect("parse");
620 assert_eq!(parsed.signature, Signature::Unsigned);
621 assert_eq!(parsed.header.len(), 1);
622 assert_eq!(parsed.body, b"hello");
623 assert_eq!(parsed.to_bytes(), raw);
624 }
625
626 #[test]
627 fn signed_verification_matches() {
628 let mut m = CmrMessage {
629 signature: Signature::Unsigned,
630 header: vec![MessageId::parse("2029/01/01 00:00:00 http://alice").expect("id")],
631 body: b"abc".to_vec(),
632 };
633 m.sign_with_key(b"secret");
634 let payload = m.payload_without_signature_line();
635 assert!(m.signature.verifies(&payload, Some(b"secret")));
636 assert!(!m.signature.verifies(&payload, Some(b"wrong")));
637 }
638
639 #[test]
640 fn rejects_recipient_in_header() {
641 let raw = b"0\r\n2029/12/31 23:59:59 http://bob\r\n\r\n0\r\n";
642 let err = parse_message(raw, &ctx(Some("http://bob"))).expect_err("must fail");
643 assert!(matches!(err, ParseError::RecipientAddressInHeader));
644 }
645
646 #[test]
647 fn rejects_recipient_in_header_when_only_trailing_slash_differs() {
648 let raw = b"0\r\n2029/12/31 23:59:59 http://bob/\r\n\r\n0\r\n";
649 let err = parse_message(raw, &ctx(Some("http://bob"))).expect_err("must fail");
650 assert!(matches!(err, ParseError::RecipientAddressInHeader));
651 }
652}