1use std::collections::HashSet;
4use std::io::{BufRead, Read};
5
6use buffer_redux::BufReader;
7use chrono::SubsecRound;
8use log::debug;
9use nom::branch::alt;
10use nom::bytes::streaming::take_until1;
11use nom::character::streaming::line_ending;
12use nom::combinator::{complete, map_res};
13use nom::IResult;
14
15use crate::armor::{self, header_parser, read_from_buf, BlockType, Headers};
16use crate::crypto::hash::HashAlgorithm;
17use crate::errors::Result;
18use crate::line_writer::LineBreak;
19use crate::normalize_lines::Normalized;
20use crate::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
21use crate::types::{KeyVersion, PublicKeyTrait, SecretKeyTrait};
22use crate::{ArmorOptions, Deserializable, Signature, StandaloneSignature};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct CleartextSignedMessage {
29 csf_encoded_text: String,
34
35 hashes: Vec<HashAlgorithm>,
37
38 signatures: Vec<StandaloneSignature>,
40}
41
42impl CleartextSignedMessage {
43 pub fn new<F>(
45 text: &str,
46 config: SignatureConfig,
47 key: &impl SecretKeyTrait,
48 key_pw: F,
49 ) -> Result<Self>
50 where
51 F: FnOnce() -> String,
52 {
53 let signature_text: Vec<u8> = Normalized::new(text.bytes(), LineBreak::Crlf).collect();
54 let hash = config.hash_alg;
55 let signature = config.sign(key, key_pw, &signature_text[..])?;
56 let signature = StandaloneSignature::new(signature);
57
58 Ok(Self {
59 csf_encoded_text: dash_escape(text),
60 hashes: vec![hash],
61 signatures: vec![signature],
62 })
63 }
64
65 pub fn sign<R, F>(rng: R, text: &str, key: &impl SecretKeyTrait, key_pw: F) -> Result<Self>
67 where
68 R: rand::Rng + rand::CryptoRng,
69 F: FnOnce() -> String,
70 {
71 let key_id = key.key_id();
72 let algorithm = key.algorithm();
73 let hash_algorithm = key.hash_alg();
74 let hashed_subpackets = vec![
75 Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint())),
76 Subpacket::regular(SubpacketData::SignatureCreationTime(
77 chrono::Utc::now().trunc_subsecs(0),
78 )),
79 ];
80 let unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(key_id))];
81
82 let mut config = match key.version() {
83 KeyVersion::V4 => SignatureConfig::v4(SignatureType::Text, algorithm, hash_algorithm),
84 KeyVersion::V6 => {
85 SignatureConfig::v6(rng, SignatureType::Text, algorithm, hash_algorithm)?
86 }
87 v => bail!("unsupported key version {:?}", v),
88 };
89 config.hashed_subpackets = hashed_subpackets;
90 config.unhashed_subpackets = unhashed_subpackets;
91
92 Self::new(text, config, key, key_pw)
93 }
94
95 pub fn new_many<F>(text: &str, signer: F) -> Result<Self>
100 where
101 F: FnOnce(&[u8]) -> Result<Vec<Signature>>,
102 {
103 let signature_text: Vec<u8> = Normalized::new(text.bytes(), LineBreak::Crlf).collect();
104
105 let raw_signatures = signer(&signature_text[..])?;
106 let mut hashes = HashSet::new();
107 let mut signatures = Vec::new();
108
109 for signature in raw_signatures {
110 hashes.insert(signature.hash_alg());
111 let signature = StandaloneSignature::new(signature);
112 signatures.push(signature);
113 }
114
115 Ok(Self {
116 csf_encoded_text: dash_escape(text),
117 hashes: hashes.into_iter().collect(),
118 signatures,
119 })
120 }
121
122 pub fn signatures(&self) -> &[StandaloneSignature] {
124 &self.signatures
125 }
126
127 pub fn verify(&self, key: &impl PublicKeyTrait) -> Result<&StandaloneSignature> {
131 let nt = self.signed_text();
132 for signature in &self.signatures {
133 if signature.verify(key, nt.as_bytes()).is_ok() {
134 return Ok(signature);
135 }
136 }
137
138 bail!("No matching signature found")
139 }
140
141 pub fn verify_many<F>(&self, verifier: F) -> Result<()>
143 where
144 F: Fn(usize, &StandaloneSignature, &[u8]) -> Result<()>,
145 {
146 let nt = self.signed_text();
147 for (i, signature) in self.signatures.iter().enumerate() {
148 verifier(i, signature, nt.as_bytes())?;
149 }
150 Ok(())
151 }
152
153 pub fn signed_text(&self) -> String {
156 let unescaped = dash_unescape_and_trim(&self.csf_encoded_text);
157
158 let normalized: Vec<u8> = Normalized::new(unescaped.bytes(), LineBreak::Crlf).collect();
159
160 std::str::from_utf8(&normalized)
161 .map(str::to_owned)
162 .expect("csf_encoded_text is UTF8")
163 }
164
165 pub fn text(&self) -> &str {
167 &self.csf_encoded_text
168 }
169
170 pub fn from_armor<R: Read>(bytes: R) -> Result<(Self, Headers)> {
172 Self::from_armor_buf(BufReader::new(bytes))
173 }
174
175 pub fn from_string(input: &str) -> Result<(Self, Headers)> {
177 Self::from_armor_buf(input.as_bytes())
178 }
179
180 pub fn from_armor_buf<R: BufRead>(mut b: R) -> Result<(Self, Headers)> {
182 debug!("parsing cleartext message");
183 let (typ, headers, has_leading_data) =
185 read_from_buf(&mut b, "cleartext header", header_parser)?;
186 ensure_eq!(typ, BlockType::CleartextMessage, "unexpected block type");
187 ensure!(
188 !has_leading_data,
189 "must not have leading data for a cleartext message"
190 );
191
192 Self::from_armor_after_header(b, headers)
193 }
194
195 pub fn from_armor_after_header<R: BufRead>(
196 mut b: R,
197 headers: Headers,
198 ) -> Result<(Self, Headers)> {
199 let hashes = validate_headers(headers)?;
200
201 debug!("Found Hash headers: {:?}", hashes);
202
203 let csf_encoded_text = read_from_buf(&mut b, "cleartext body", cleartext_body)?;
205
206 let mut dearmor = armor::Dearmor::new(b);
208 dearmor.read_header()?;
209 let typ = dearmor
211 .typ
212 .ok_or_else(|| format_err!("dearmor failed to retrieve armor type"))?;
213
214 ensure_eq!(typ, BlockType::Signature, "invalid block type");
215
216 let signatures = StandaloneSignature::from_bytes_many(&mut dearmor);
217 let signatures = signatures.collect::<Result<_>>()?;
218
219 let (_, headers, _, b) = dearmor.into_parts();
220
221 if has_rest(b)? {
222 bail!("unexpected trailing data");
223 }
224
225 Ok((
226 Self {
227 csf_encoded_text,
228 hashes,
229 signatures,
230 },
231 headers,
232 ))
233 }
234
235 pub fn to_armored_writer(
236 &self,
237 writer: &mut impl std::io::Write,
238 opts: ArmorOptions<'_>,
239 ) -> Result<()> {
240 writer.write_all(HEADER_LINE.as_bytes())?;
242 writer.write_all(&[b'\n'])?;
243
244 for hash in &self.hashes {
246 writer.write_all(b"Hash: ")?;
247 writer.write_all(hash.to_string().as_bytes())?;
248 writer.write_all(&[b'\n'])?;
249 }
250 writer.write_all(&[b'\n'])?;
251
252 writer.write_all(self.csf_encoded_text.as_bytes())?;
254 writer.write_all(&[b'\n'])?;
255
256 armor::write(
257 &self.signatures,
258 armor::BlockType::Signature,
259 writer,
260 opts.headers,
261 opts.include_checksum,
262 )?;
263
264 Ok(())
265 }
266
267 pub fn to_armored_bytes(&self, opts: ArmorOptions<'_>) -> Result<Vec<u8>> {
268 let mut buf = Vec::new();
269 self.to_armored_writer(&mut buf, opts)?;
270 Ok(buf)
271 }
272
273 pub fn to_armored_string(&self, opts: ArmorOptions<'_>) -> Result<String> {
274 let res = String::from_utf8(self.to_armored_bytes(opts)?).map_err(|e| e.utf8_error())?;
275 Ok(res)
276 }
277}
278
279fn validate_headers(headers: Headers) -> Result<Vec<HashAlgorithm>> {
280 let mut hashes = Vec::new();
281 for (name, values) in headers {
282 ensure_eq!(name, "Hash", "unexpected header");
283 for value in values {
284 let h: HashAlgorithm = value.parse()?;
285 hashes.push(h);
286 }
287 }
288 Ok(hashes)
289}
290
291fn dash_escape(text: &str) -> String {
297 let mut out = String::new();
298 for line in text.split_inclusive('\n') {
299 if line.starts_with('-') {
300 out += "- ";
301 }
302 out.push_str(line);
303 }
304
305 out
306}
307
308fn dash_unescape_and_trim(text: &str) -> String {
312 let mut out = String::new();
313
314 for line in text.split_inclusive('\n') {
315 let line_end_len = if line.ends_with("\r\n") {
317 2
318 } else if line.ends_with("\n") {
319 1
320 } else {
321 0
322 };
323 let (content, end) = line.split_at(line.len() - line_end_len);
324
325 let trimmed = content.trim_end_matches([' ', '\t']);
327
328 if let Some(stripped) = trimmed.strip_prefix("- ") {
330 out += stripped;
331 } else {
332 out += trimmed;
333 }
334
335 out += end;
337 }
338
339 out
340}
341
342fn has_rest<R: BufRead>(mut b: R) -> Result<bool> {
344 let mut buf = [0u8; 64];
345 while b.read(&mut buf)? > 0 {
346 if buf.iter().any(|&c| !char::from(c).is_ascii_whitespace()) {
347 return Ok(true);
348 }
349 }
350
351 Ok(false)
352}
353
354const HEADER_LINE: &str = "-----BEGIN PGP SIGNED MESSAGE-----";
355
356fn to_string(b: &[u8]) -> std::result::Result<String, std::str::Utf8Error> {
357 std::str::from_utf8(b).map(|s| s.to_string())
358}
359
360fn cleartext_body(i: &[u8]) -> IResult<&[u8], String> {
361 let (i, lines) = map_res(
362 alt((
363 complete(take_until1("\r\n-----")),
364 complete(take_until1("\n-----")),
365 )),
366 to_string,
367 )(i)?;
368 let (i, _) = line_ending(i)?;
369
370 Ok((i, lines))
371}
372
373#[cfg(test)]
374mod tests {
375 #![allow(clippy::unwrap_used)]
376
377 use rand::SeedableRng;
378 use rand_chacha::ChaCha8Rng;
379
380 use super::*;
381 use crate::{Any, SignedPublicKey, SignedSecretKey};
382
383 #[test]
384 fn test_cleartext_openpgp_1() {
385 let _ = pretty_env_logger::try_init();
386
387 let data =
388 std::fs::read_to_string("./tests/openpgp/samplemsgs/clearsig-1-key-1.asc").unwrap();
389
390 let (msg, headers) = CleartextSignedMessage::from_string(&data).unwrap();
391
392 assert_eq!(normalize(msg.text()), normalize("You are scrupulously honest, frank, and straightforward. Therefore you\nhave few friends."));
393 assert_eq!(headers.len(), 1);
394 assert_eq!(
395 headers.get("Version").unwrap(),
396 &vec!["GnuPG v2".to_string()]
397 );
398
399 assert_eq!(msg.signatures().len(), 1);
400
401 roundtrip(&data, &msg, &headers);
402 }
403
404 #[test]
405 fn test_cleartext_openpgp_2() {
406 let _ = pretty_env_logger::try_init();
407
408 let data =
409 std::fs::read_to_string("./tests/openpgp/samplemsgs/clearsig-2-keys-1.asc").unwrap();
410
411 let (msg, headers) = CleartextSignedMessage::from_string(&data).unwrap();
412
413 assert_eq!(
414 normalize(msg.text()),
415 normalize("\"The geeks shall inherit the earth.\"\n -- Karl Lehenbauer")
416 );
417 assert_eq!(headers.len(), 1);
418 assert_eq!(
419 headers.get("Version").unwrap(),
420 &vec!["GnuPG v2".to_string()]
421 );
422
423 assert_eq!(msg.signatures().len(), 2);
424
425 roundtrip(&data, &msg, &headers);
426 }
427
428 #[test]
429 fn test_cleartext_openpgp_3() {
430 let _ = pretty_env_logger::try_init();
431
432 let data =
433 std::fs::read_to_string("./tests/openpgp/samplemsgs/clearsig-2-keys-2.asc").unwrap();
434
435 let (msg, headers) = CleartextSignedMessage::from_string(&data).unwrap();
436
437 assert_eq!(
438 normalize(msg.text()),
439 normalize("The very remembrance of my former misfortune proves a new one to me.\n -- Miguel de Cervantes")
440 );
441 assert_eq!(headers.len(), 1);
442 assert_eq!(
443 headers.get("Version").unwrap(),
444 &vec!["GnuPG v2".to_string()]
445 );
446
447 roundtrip(&data, &msg, &headers);
448 }
449
450 #[test]
451 fn test_cleartext_interop_testsuite_1_good() {
452 let _ = pretty_env_logger::try_init();
453
454 let data = std::fs::read_to_string("./tests/unit-tests/cleartext-msg-01.asc").unwrap();
455
456 let (msg, headers) = CleartextSignedMessage::from_string(&data).unwrap();
457
458 assert_eq!(
459 normalize(msg.text()),
460 normalize(
461 "- From the grocery store we need:\n\n- - tofu\n- - vegetables\n- - noodles\n\n"
462 )
463 );
464 assert!(headers.is_empty());
465
466 assert_eq!(
467 msg.signed_text(),
468 "From the grocery store we need:\r\n\r\n- tofu\r\n- vegetables\r\n- noodles\r\n\r\n"
469 );
470
471 let key_data = std::fs::read_to_string("./tests/unit-tests/cleartext-key-01.asc").unwrap();
472 let (key, _) = SignedSecretKey::from_string(&key_data).unwrap();
473
474 msg.verify(&key.public_key()).unwrap();
475 assert_eq!(msg.signatures().len(), 1);
476
477 roundtrip(&data, &msg, &headers);
478 }
479
480 #[test]
481 fn test_cleartext_interop_testsuite_1_any() {
482 let _ = pretty_env_logger::try_init();
483
484 let data = std::fs::read_to_string("./tests/unit-tests/cleartext-msg-01.asc").unwrap();
485
486 let (msg, headers) = CleartextSignedMessage::from_string(&data).unwrap();
487
488 let (any, headers2) = Any::from_string(&data).unwrap();
489 assert_eq!(headers, headers2);
490
491 if let Any::Cleartext(msg2) = any {
492 assert_eq!(msg, msg2);
493 } else {
494 panic!("got unexpected type of any: {:?}", any);
495 }
496 }
497
498 #[test]
499 fn test_cleartext_interop_testsuite_1_fail() {
500 let _ = pretty_env_logger::try_init();
501
502 let data = std::fs::read_to_string("./tests/unit-tests/cleartext-msg-01-fail.asc").unwrap();
503
504 let err = CleartextSignedMessage::from_string(&data).unwrap_err();
505 dbg!(err);
506
507 let err = Any::from_string(&data).unwrap_err();
508 dbg!(err);
509 }
510
511 #[test]
512 fn test_cleartext_interop_testsuite_2_fail() {
513 let _ = pretty_env_logger::try_init();
514
515 let data = std::fs::read_to_string("./tests/unit-tests/cleartext-msg-02-fail.asc").unwrap();
516
517 let err = CleartextSignedMessage::from_string(&data).unwrap_err();
518 dbg!(err);
519
520 let err = Any::from_string(&data).unwrap_err();
521 dbg!(err);
522 }
523
524 fn roundtrip(expected: &str, msg: &CleartextSignedMessage, headers: &Headers) {
525 let expected = normalize(expected);
526 let out = msg.to_armored_string(Some(headers).into()).unwrap();
527 let out = normalize(out);
528
529 assert_eq!(expected, out);
530 }
531
532 fn normalize(a: impl AsRef<str>) -> String {
533 a.as_ref().replace("\r\n", "\n").replace('\r', "\n")
534 }
535
536 #[test]
537 fn test_cleartext_body() {
538 assert_eq!(
539 cleartext_body(b"-- hello\n--world\n-----bla").unwrap(),
540 (&b"-----bla"[..], "-- hello\n--world".to_string())
541 );
542
543 assert_eq!(
544 cleartext_body(b"-- hello\r\n--world\r\n-----bla").unwrap(),
545 (&b"-----bla"[..], "-- hello\r\n--world".to_string())
546 );
547 }
548
549 #[test]
550 fn test_dash_escape() {
551 let input = "From the grocery store we need:
552
553- tofu
554- vegetables
555- noodles
556
557";
558 let expected = "From the grocery store we need:
559
560- - tofu
561- - vegetables
562- - noodles
563
564";
565
566 assert_eq!(dash_escape(input), expected);
567 }
568
569 #[test]
570 fn test_dash_unescape_and_trim() {
571 let input = "From the grocery store we need:
572
573- - tofu\u{20}\u{20}
574- - vegetables\t
575- - noodles
576
577";
578 let expected = "From the grocery store we need:
579
580- tofu
581- vegetables
582- noodles
583
584";
585
586 assert_eq!(dash_unescape_and_trim(input), expected);
587 }
588
589 #[test]
590 fn test_sign() {
591 let mut rng = ChaCha8Rng::seed_from_u64(0);
592
593 let key_data = std::fs::read_to_string("./tests/unit-tests/cleartext-key-01.asc").unwrap();
594 let (key, _) = SignedSecretKey::from_string(&key_data).unwrap();
595 let msg = CleartextSignedMessage::sign(
596 &mut rng,
597 "hello\n-world-what-\nis up\n",
598 &key,
599 String::new,
600 )
601 .unwrap();
602 msg.verify(&key.public_key()).unwrap();
603 }
604
605 #[test]
606 fn test_sign_no_newline() {
607 const MSG: &str = "message without newline at the end";
608 let mut rng = ChaCha8Rng::seed_from_u64(0);
609
610 let key_data = std::fs::read_to_string("./tests/unit-tests/cleartext-key-01.asc").unwrap();
611 let (key, _) = SignedSecretKey::from_string(&key_data).unwrap();
612 let msg = CleartextSignedMessage::sign(&mut rng, MSG, &key, String::new).unwrap();
613
614 assert_eq!(msg.signed_text(), MSG);
615
616 msg.verify(&key.public_key()).unwrap();
617 }
618
619 #[test]
620 fn test_verify_csf_puppet() {
621 let msg_data = std::fs::read_to_string("./tests/unit-tests/csf-puppet/InRelease").unwrap();
624 let (Any::Cleartext(msg), headers) = Any::from_string(&msg_data).unwrap() else {
625 panic!("couldn't read msg")
626 };
627
628 assert_eq!(headers.len(), 0);
630 assert_eq!(msg.signatures().len(), 1);
631 roundtrip(&msg_data, &msg, &headers);
632
633 let cert_data =
635 std::fs::read_to_string("./tests/unit-tests/csf-puppet/DEB-GPG-KEY-puppet-20250406")
636 .unwrap();
637 let (cert, _) = SignedPublicKey::from_string(&cert_data).unwrap();
638
639 msg.verify(&cert).expect("verify");
640 }
641}