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 Some(id.address.as_str()) == ctx.recipient_address {
509 return Err(ParseError::RecipientAddressInHeader);
510 }
511 if !addresses.insert(id.address.as_str()) {
512 return Err(ParseError::DuplicateAddress);
513 }
514 if id.timestamp > ctx.now {
515 return Err(ParseError::FutureTimestamp);
516 }
517 if idx > 0 && id.timestamp >= header[idx - 1].timestamp {
518 return Err(ParseError::NonDescendingTimestamps);
519 }
520 }
521 Ok(())
522}
523
524fn take_crlf_line(mut input: &[u8]) -> Result<(&[u8], &[u8]), ParseError> {
525 let mut i = 0;
526 while i + 1 < input.len() {
527 if input[i] == b'\r' {
528 if input[i + 1] != b'\n' {
529 return Err(ParseError::MissingCrlf);
530 }
531 let line = &input[..i];
532 input = &input[(i + 2)..];
533 return Ok((line, input));
534 }
535 i += 1;
536 }
537 Err(ParseError::MissingCrlf)
538}
539
540fn parse_dec_u16(input: &str, _field: &str, full: &str) -> Result<u16, ParseError> {
541 if input.len() != 4 || !input.bytes().all(|b| b.is_ascii_digit()) {
542 return Err(ParseError::InvalidTimestamp(full.to_owned()));
543 }
544 input
545 .parse::<u16>()
546 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))
547}
548
549fn parse_dec_u8(input: &str, _field: &str, full: &str) -> Result<u8, ParseError> {
550 if input.len() != 2 || !input.bytes().all(|b| b.is_ascii_digit()) {
551 return Err(ParseError::InvalidTimestamp(full.to_owned()));
552 }
553 input
554 .parse::<u8>()
555 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))
556}
557
558fn validate_calendar_parts(
559 year: u16,
560 month: u8,
561 day: u8,
562 hour: u8,
563 minute: u8,
564 second: u8,
565 full: &str,
566) -> Result<(), ParseError> {
567 if hour > 23 || minute > 59 || second > 59 {
568 return Err(ParseError::InvalidTimestamp(full.to_owned()));
569 }
570 let month =
571 Month::try_from(month).map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))?;
572 let date = time::Date::from_calendar_date(i32::from(year), month, day)
573 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))?;
574 let _ = date
575 .with_hms(hour, minute, second)
576 .map_err(|_| ParseError::InvalidTimestamp(full.to_owned()))?;
577 Ok(())
578}
579
580fn compare_fractional_decimal(a: &str, b: &str) -> Ordering {
581 let max_len = a.len().max(b.len());
582 for i in 0..max_len {
583 let ad = a.as_bytes().get(i).copied().unwrap_or(b'0');
584 let bd = b.as_bytes().get(i).copied().unwrap_or(b'0');
585 match ad.cmp(&bd) {
586 Ordering::Equal => {}
587 non_eq => return non_eq,
588 }
589 }
590 Ordering::Equal
591}
592
593fn is_lower_hex(s: &str) -> bool {
594 s.bytes()
595 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
596}
597
598#[cfg(test)]
599mod tests {
600 use super::*;
601
602 fn ctx<'a>(recipient: Option<&'a str>) -> ParseContext<'a> {
603 ParseContext::secure(
604 CmrTimestamp::parse("2030/01/01 00:00:00").expect("valid time"),
605 recipient,
606 )
607 }
608
609 #[test]
610 fn parse_round_trip_unsigned() {
611 let raw = b"0\r\n2029/12/31 23:59:59 http://alice\r\n\r\n5\r\nhello";
612 let parsed = parse_message(raw, &ctx(Some("http://bob"))).expect("parse");
613 assert_eq!(parsed.signature, Signature::Unsigned);
614 assert_eq!(parsed.header.len(), 1);
615 assert_eq!(parsed.body, b"hello");
616 assert_eq!(parsed.to_bytes(), raw);
617 }
618
619 #[test]
620 fn signed_verification_matches() {
621 let mut m = CmrMessage {
622 signature: Signature::Unsigned,
623 header: vec![MessageId::parse("2029/01/01 00:00:00 http://alice").expect("id")],
624 body: b"abc".to_vec(),
625 };
626 m.sign_with_key(b"secret");
627 let payload = m.payload_without_signature_line();
628 assert!(m.signature.verifies(&payload, Some(b"secret")));
629 assert!(!m.signature.verifies(&payload, Some(b"wrong")));
630 }
631
632 #[test]
633 fn rejects_recipient_in_header() {
634 let raw = b"0\r\n2029/12/31 23:59:59 http://bob\r\n\r\n0\r\n";
635 let err = parse_message(raw, &ctx(Some("http://bob"))).expect_err("must fail");
636 assert!(matches!(err, ParseError::RecipientAddressInHeader));
637 }
638}