1use super::{DkimSigner, Done, Signature, canonicalize::BodyHasher, sign::SignableMessage};
10use crate::SystemTime;
11use crate::{
12 Error,
13 common::{
14 crypto::{HashContext, HashImpl, SigningKey},
15 headers::HeaderIterator,
16 },
17};
18use memchr::memmem;
19
20pub struct DkimSigningStream<'a, T: SigningKey> {
45 template: Signature,
46 key: &'a T,
47 state: SigningState<<<T as SigningKey>::Hasher as HashImpl>::Context>,
48}
49
50enum SigningState<H> {
51 ReadingHeaders { buffer: Vec<u8>, scanned: usize },
53 HashingBody {
55 header_section: Vec<u8>,
56 body_hasher: BodyHasher<H>,
57 },
58 Done,
60}
61
62impl<T: SigningKey> DkimSigner<T, Done> {
63 pub fn sign_streaming(&self) -> DkimSigningStream<'_, T> {
82 DkimSigningStream {
83 template: self.template.clone(),
84 key: &self.key,
85 state: SigningState::ReadingHeaders {
86 buffer: Vec::with_capacity(8192),
87 scanned: 0,
88 },
89 }
90 }
91}
92
93impl<T: SigningKey> DkimSigningStream<'_, T> {
94 pub fn write(&mut self, chunk: &[u8]) {
102 match &mut self.state {
103 SigningState::ReadingHeaders { buffer, scanned } => {
104 buffer.extend_from_slice(chunk);
105
106 let Some(boundary_pos) =
108 find_header_boundary(&buffer[*scanned..]).map(|pos| *scanned + pos)
109 else {
110 *scanned = buffer.len().saturating_sub(3);
111 return;
112 };
113
114 let mut header_section = std::mem::take(buffer);
115
116 let mut body_hasher = BodyHasher::new(
117 <T::Hasher as HashImpl>::hasher(),
118 self.template.cb,
119 if self.template.l > 0 { u64::MAX } else { 0 },
120 );
121
122 body_hasher.write(&header_section[boundary_pos..]);
124 header_section.truncate(boundary_pos - 2);
125
126 self.state = SigningState::HashingBody {
127 header_section,
128 body_hasher,
129 };
130 }
131 SigningState::HashingBody { body_hasher, .. } => {
132 body_hasher.write(chunk);
133 }
134 SigningState::Done => {
135 }
137 }
138 }
139
140 pub fn finish(mut self) -> crate::Result<Signature>
152 where
153 <<T as SigningKey>::Hasher as HashImpl>::Context: HashContext,
154 {
155 let now = SystemTime::now()
156 .duration_since(SystemTime::UNIX_EPOCH)
157 .map(|d| d.as_secs())
158 .unwrap_or(0);
159
160 match std::mem::replace(&mut self.state, SigningState::Done) {
161 SigningState::ReadingHeaders { mut buffer, .. } => {
162 let mut body_hasher = BodyHasher::new(
164 <T::Hasher as HashImpl>::hasher(),
165 self.template.cb,
166 if self.template.l > 0 { u64::MAX } else { 0 },
167 );
168
169 let header_len = match find_header_boundary(&buffer) {
172 Some(boundary_pos) => {
173 body_hasher.write(&buffer[boundary_pos..]);
174 boundary_pos - 2
175 }
176 None => {
177 buffer.extend_from_slice(b"\r\n");
179 buffer.len()
180 }
181 };
182
183 let (hasher, body_len) = body_hasher.finish();
184 let body_hash = hasher.complete();
185
186 self.finish_with_parsed_data(&buffer[..header_len], body_hash, body_len, now)
187 }
188 SigningState::HashingBody {
189 header_section,
190 body_hasher,
191 } => {
192 let (hasher, body_len) = body_hasher.finish();
193 let body_hash = hasher.complete();
194 self.finish_with_parsed_data(&header_section, body_hash, body_len, now)
195 }
196 SigningState::Done => Err(Error::NoHeadersFound),
197 }
198 }
199
200 fn finish_with_parsed_data(
201 &self,
202 header_section: &[u8],
203 body_hash: crate::common::crypto::HashOutput,
204 body_len: u64,
205 now: u64,
206 ) -> crate::Result<Signature> {
207 let mut headers = Vec::with_capacity(self.template.h.len());
209 let mut found_headers = vec![false; self.template.h.len()];
210 let mut signed_headers = Vec::with_capacity(self.template.h.len());
211
212 for (name, value) in HeaderIterator::new(header_section) {
213 if let Some(pos) = self
214 .template
215 .h
216 .iter()
217 .position(|header| name.eq_ignore_ascii_case(header.as_bytes()))
218 {
219 headers.push((name, value));
220 found_headers[pos] = true;
221 signed_headers.push(std::str::from_utf8(name).unwrap_or_default().to_string());
222 }
223 }
224
225 if signed_headers.is_empty() {
226 return Err(Error::NoHeadersFound);
227 }
228
229 signed_headers.reverse();
231 for (header, found) in self.template.h.iter().zip(found_headers) {
232 if !found {
233 signed_headers.push(header.to_string());
234 }
235 }
236
237 let canonical_headers = self.template.ch.canonical_headers(headers);
239
240 let mut signature = self.template.clone();
242 signature.bh = body_hash.as_ref().to_vec();
243 signature.t = now;
244 signature.x = if signature.x > 0 {
245 now + signature.x
246 } else {
247 0
248 };
249 signature.h = signed_headers;
250 if signature.l > 0 {
251 signature.l = body_len;
252 }
253
254 signature.b = self.key.sign(SignableMessage {
256 headers: canonical_headers,
257 signature: &signature,
258 })?;
259
260 Ok(signature)
261 }
262}
263
264pub(crate) fn find_header_boundary(data: &[u8]) -> Option<usize> {
266 memmem::find(data, b"\r\n\r\n").map(|p| p + 4)
267}
268
269#[cfg(test)]
270#[allow(unused)]
271mod test {
272 use crate::{
273 common::crypto::{RsaKey, Sha256},
274 dkim::{Canonicalization, DkimSigner},
275 };
276
277 use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, pem::PemObject};
278
279 const RSA_PRIVATE_KEY: &str = include_str!("../../resources/rsa-private.pem");
280
281 #[test]
282 fn streaming_sign_matches_regular_sign() {
283 let message = concat!(
285 "From: bill@example.com\r\n",
286 "To: jdoe@example.com\r\n",
287 "Subject: TPS Report\r\n",
288 "\r\n",
289 "I'm going to need those TPS reports ASAP. ",
290 "So, if you could do that, that'd be great.\r\n"
291 );
292
293 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
294 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
295 ))
296 .unwrap();
297
298 let signer = DkimSigner::from_key(pk_rsa)
299 .domain("example.com")
300 .selector("default")
301 .headers(["From", "To", "Subject"]);
302
303 let sig1 = signer.sign(message.as_bytes()).unwrap();
305
306 let mut stream = signer.sign_streaming();
308 stream.write(message.as_bytes());
309 let sig2 = stream.finish().unwrap();
310
311 assert_eq!(sig1.bh, sig2.bh, "Body hashes should match");
313 assert_eq!(sig1.h, sig2.h, "Signed headers should match");
315 assert_eq!(sig1.b, sig2.b, "Signatures should match");
317 }
318
319 #[test]
320 fn streaming_sign_multiple_chunks() {
321 let header = "From: bill@example.com\r\nTo: jdoe@example.com\r\nSubject: Test\r\n\r\n";
322 let body = "Hello World! This is the body.\r\n";
323
324 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
325 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
326 ))
327 .unwrap();
328
329 let signer = DkimSigner::from_key(pk_rsa)
330 .domain("example.com")
331 .selector("default")
332 .headers(["From", "To", "Subject"]);
333
334 let full_message = format!("{}{}", header, body);
336 let reference_sig = signer.sign(full_message.as_bytes()).unwrap();
337
338 let mut stream = signer.sign_streaming();
340 stream.write(header.as_bytes());
341 stream.write(body.as_bytes());
342 let streamed_sig = stream.finish().unwrap();
343
344 assert_eq!(
345 reference_sig.bh, streamed_sig.bh,
346 "Body hashes should match"
347 );
348 }
349
350 #[test]
351 fn streaming_sign_chunked_body() {
352 let message = concat!(
353 "From: test@example.com\r\n",
354 "Subject: Chunked Test\r\n",
355 "\r\n",
356 "Line 1\r\n",
357 "Line 2\r\n",
358 "Line 3\r\n",
359 );
360
361 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
362 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
363 ))
364 .unwrap();
365
366 let signer = DkimSigner::from_key(pk_rsa)
367 .domain("example.com")
368 .selector("default")
369 .headers(["From", "Subject"]);
370
371 let reference_sig = signer.sign(message.as_bytes()).unwrap();
373
374 for chunk_size in [1, 2, 5, 10, 20] {
376 let mut stream = signer.sign_streaming();
377 for chunk in message.as_bytes().chunks(chunk_size) {
378 stream.write(chunk);
379 }
380 let streamed_sig = stream.finish().unwrap();
381
382 assert_eq!(
383 reference_sig.bh, streamed_sig.bh,
384 "Body hash mismatch at chunk_size={}",
385 chunk_size
386 );
387 }
388 }
389
390 #[test]
391 fn streaming_sign_split_header_boundary() {
392 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
394 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
395 ))
396 .unwrap();
397
398 let signer = DkimSigner::from_key(pk_rsa)
399 .domain("example.com")
400 .selector("default")
401 .headers(["From", "Subject"]);
402
403 let message = "From: test@example.com\r\nSubject: Test\r\n\r\nBody";
405 let reference_sig = signer.sign(message.as_bytes()).unwrap();
406
407 let mut stream = signer.sign_streaming();
409 stream.write(b"From: test@example.com\r\n");
410 stream.write(b"Subject: Test\r\n");
411 stream.write(b"\r\n"); stream.write(b"Body");
413 let streamed_sig = stream.finish().unwrap();
414
415 assert_eq!(reference_sig.bh, streamed_sig.bh);
416 }
417
418 #[test]
419 fn streaming_sign_empty_body() {
420 let message = "From: test@example.com\r\nSubject: Empty\r\n\r\n";
421 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
422 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
423 ))
424 .unwrap();
425
426 let signer = DkimSigner::from_key(pk_rsa)
427 .domain("example.com")
428 .selector("default")
429 .headers(["From", "Subject"]);
430
431 let reference_sig = signer.sign(message.as_bytes()).unwrap();
432
433 let mut stream = signer.sign_streaming();
434 stream.write(message.as_bytes());
435 let streamed_sig = stream.finish().unwrap();
436
437 assert_eq!(reference_sig.bh, streamed_sig.bh);
438 }
439
440 #[test]
441 fn streaming_sign_simple_canonicalization() {
442 let message = concat!(
443 "From: test@example.com\r\n",
444 "Subject: Simple Canon Test\r\n",
445 "\r\n",
446 "Body with spaces\r\n",
447 );
448
449 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
450 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
451 ))
452 .unwrap();
453
454 let signer = DkimSigner::from_key(pk_rsa)
455 .domain("example.com")
456 .selector("default")
457 .headers(["From", "Subject"])
458 .header_canonicalization(Canonicalization::Simple)
459 .body_canonicalization(Canonicalization::Simple);
460
461 let reference_sig = signer.sign(message.as_bytes()).unwrap();
462
463 let mut stream = signer.sign_streaming();
464 stream.write(message.as_bytes());
465 let streamed_sig = stream.finish().unwrap();
466
467 assert_eq!(reference_sig.bh, streamed_sig.bh);
468 assert_eq!(reference_sig.b, streamed_sig.b);
469 }
470
471 #[test]
472 fn streaming_sign_folded_headers() {
473 let message = concat!(
475 "From: test@example.com\r\n",
476 "Subject: This is a very long subject line that\r\n",
477 " continues on the next line\r\n",
478 "\r\n",
479 "Body\r\n",
480 );
481 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
482 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
483 ))
484 .unwrap();
485
486 let signer = DkimSigner::from_key(pk_rsa)
487 .domain("example.com")
488 .selector("default")
489 .headers(["From", "Subject"]);
490
491 let reference_sig = signer.sign(message.as_bytes()).unwrap();
492
493 let mut stream = signer.sign_streaming();
494 stream.write(message.as_bytes());
495 let streamed_sig = stream.finish().unwrap();
496
497 assert_eq!(reference_sig.bh, streamed_sig.bh);
498 }
499
500 #[test]
501 fn streaming_sign_no_matching_headers_error() {
502 let message = "X-Custom: value\r\n\r\nBody\r\n";
503
504 let pk_rsa = RsaKey::<Sha256>::from_key_der(PrivateKeyDer::Pkcs1(
505 PrivatePkcs1KeyDer::from_pem_slice(RSA_PRIVATE_KEY.as_bytes()).unwrap(),
506 ))
507 .unwrap();
508
509 let signer = DkimSigner::from_key(pk_rsa)
510 .domain("example.com")
511 .selector("default")
512 .headers(["From", "Subject"]); let mut stream = signer.sign_streaming();
515 stream.write(message.as_bytes());
516 let result = stream.finish();
517
518 assert!(matches!(result, Err(crate::Error::NoHeadersFound)));
519 }
520}