pgp_cleartext/lib.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5/*! PGP cleartext framework
6
7The PGP cleartext framework is a mechanism to store PGP signatures inline with
8the cleartext data that is being signed.
9
10The cleartext framework is defined by
11[RFC 4880 Section 7](https://datatracker.ietf.org/doc/html/rfc4880.html#section-7)
12and this implementation aims to be conformant with the specification.
13
14PGP cleartext signatures are text documents beginning with
15`-----BEGIN PGP SIGNED MESSAGE-----`. They have the form:
16
17```text
18-----BEGIN PGP SIGNED MESSAGE-----
19Hash: <digest>
20
21<normalized signed content>
22-----BEGIN PGP SIGNATURE-----
23<headers>
24
25<signature data>
26-----END PGP SIGNATURE-----
27```
28*/
29
30use {
31 chrono::SubsecRound,
32 digest::Digest,
33 pgp::{
34 crypto::hash::{HashAlgorithm, Hasher},
35 packet::{Packet, SignatureConfig, SignatureType, Subpacket, SubpacketData},
36 types::{PublicKeyTrait, SecretKeyTrait},
37 Signature,
38 },
39 std::{
40 cmp::Ordering,
41 collections::HashMap,
42 io::{self, BufRead, Cursor, Read},
43 },
44};
45
46const HEADER: &str = "-----BEGIN PGP SIGNED MESSAGE-----";
47const HEADER_LF: &str = "-----BEGIN PGP SIGNED MESSAGE-----\n";
48const HEADER_CRLF: &str = "-----BEGIN PGP SIGNED MESSAGE-----\r\n";
49
50const SIGNATURE_ARMOR_LF: &str = "-----BEGIN PGP SIGNATURE-----\n";
51const SIGNATURE_ARMOR_CRLF: &str = "-----BEGIN PGP SIGNATURE-----\r\n";
52
53/// Wrapper around content digesting to work around lack of clone() in pgp crate.
54#[derive(Clone)]
55pub enum CleartextHasher {
56 Md5(md5::Md5),
57 Sha1(sha1::Sha1),
58 Sha256(sha2::Sha256),
59 Sha384(sha2::Sha384),
60 Sha512(sha2::Sha512),
61}
62
63impl CleartextHasher {
64 pub fn md5() -> Self {
65 Self::Md5(md5::Md5::new())
66 }
67
68 pub fn sha1() -> Self {
69 Self::Sha1(sha1::Sha1::new())
70 }
71
72 pub fn sha256() -> Self {
73 Self::Sha256(sha2::Sha256::new())
74 }
75
76 pub fn sha384() -> Self {
77 Self::Sha384(sha2::Sha384::new())
78 }
79
80 pub fn sha512() -> Self {
81 Self::Sha512(sha2::Sha512::new())
82 }
83
84 pub fn algorithm(&self) -> HashAlgorithm {
85 match self {
86 Self::Md5(_) => HashAlgorithm::MD5,
87 Self::Sha1(_) => HashAlgorithm::SHA1,
88 Self::Sha256(_) => HashAlgorithm::SHA2_256,
89 Self::Sha384(_) => HashAlgorithm::SHA2_384,
90 Self::Sha512(_) => HashAlgorithm::SHA2_512,
91 }
92 }
93}
94
95impl std::io::Write for CleartextHasher {
96 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
97 self.update(buf);
98 Ok(buf.len())
99 }
100
101 fn flush(&mut self) -> io::Result<()> {
102 Ok(())
103 }
104}
105
106impl Hasher for CleartextHasher {
107 fn update(&mut self, data: &[u8]) {
108 match self {
109 Self::Md5(digest) => digest.update(data),
110 Self::Sha1(digest) => digest.update(data),
111 Self::Sha256(digest) => digest.update(data),
112 Self::Sha384(digest) => digest.update(data),
113 Self::Sha512(digest) => digest.update(data),
114 }
115 }
116
117 fn finish(self: Box<Self>) -> Vec<u8> {
118 match *self {
119 Self::Md5(digest) => digest.finalize().to_vec(),
120 CleartextHasher::Sha1(digest) => digest.finalize().to_vec(),
121 CleartextHasher::Sha256(digest) => digest.finalize().to_vec(),
122 CleartextHasher::Sha384(digest) => digest.finalize().to_vec(),
123 CleartextHasher::Sha512(digest) => digest.finalize().to_vec(),
124 }
125 }
126
127 fn finish_reset_into(&mut self, out: &mut [u8]) {
128 let res = match self {
129 Self::Md5(ref mut digest) => digest.finalize_reset().to_vec(),
130 CleartextHasher::Sha1(ref mut digest) => digest.finalize_reset().to_vec(),
131 CleartextHasher::Sha256(ref mut digest) => digest.finalize_reset().to_vec(),
132 CleartextHasher::Sha384(ref mut digest) => digest.finalize_reset().to_vec(),
133 CleartextHasher::Sha512(ref mut digest) => digest.finalize_reset().to_vec(),
134 };
135 out.copy_from_slice(&res.as_slice()[..out.len()]);
136 }
137}
138
139enum ReaderState {
140 /// Instance construction.
141 Initial,
142
143 /// In `Hashes: ` headers section following cleartext armor header.
144 Hashes,
145
146 /// Reading the inline cleartext message.
147 ///
148 /// No buffered data available to send to client.
149 ///
150 /// The inner bool tracks whether we have consumed content yet.
151 CleartextEmpty(bool),
152
153 /// Reading the inline cleartext message.
154 ///
155 /// Buffered data available to send to client.
156 CleartextBuffered(String),
157
158 /// In the signatures section after the cleartext message.
159 Signatures,
160
161 /// End of file reached.
162 Eof,
163}
164
165/// A reader capable of extracting PGP cleartext signatures as defined by RFC 4880 Section 7.
166///
167/// <https://datatracker.ietf.org/doc/html/rfc4880.html#section-7>.
168///
169/// The source reader is expected to initially emit a
170/// `'-----BEGIN PGP SIGNED MESSAGE-----` line.
171///
172/// This type is effectively a filtering [Read] implementation. Given a source reader
173/// that will emit bytes constituting cleartext signature data, this reader will parse
174/// the special syntax defining the cleartext signature and store state in the instance.
175/// Only the original / signed cleartext bytes will be returned by `read()` calls.
176///
177/// Once EOF is reached, call [Self::finalize()] to consume the reader and return a
178/// [CleartextSignatures] holding parsed cleartext signature state.
179///
180/// Important: reading does not validate signatures. Use [CleartextSignatures] after
181/// parsing/reading to validate signatures.
182pub struct CleartextSignatureReader<R: BufRead> {
183 reader: R,
184 state: ReaderState,
185
186 /// Hash types as advertised by the `Hash: ` header.
187 hashers: HashMap<u8, CleartextHasher>,
188
189 /// Parsed PGP signatures.
190 signatures: Vec<Signature>,
191}
192
193impl<R: BufRead> CleartextSignatureReader<R> {
194 /// Construct a new instance from a reader.
195 pub fn new(reader: R) -> Self {
196 Self {
197 state: ReaderState::Initial,
198 reader,
199 hashers: HashMap::new(),
200 signatures: vec![],
201 }
202 }
203
204 /// Finalize this reader, returning an object with signature state.
205 pub fn finalize(self) -> CleartextSignatures {
206 CleartextSignatures {
207 hashers: self.hashers,
208 signatures: self.signatures,
209 }
210 }
211}
212
213impl<R: BufRead> Read for CleartextSignatureReader<R> {
214 fn read(&mut self, dest: &mut [u8]) -> std::io::Result<usize> {
215 loop {
216 match &mut self.state {
217 ReaderState::Initial => {
218 let mut line = String::with_capacity(HEADER_CRLF.len());
219 self.reader.read_line(&mut line)?;
220
221 if !matches!(line.as_str(), HEADER_LF | HEADER_CRLF) {
222 return Err(std::io::Error::new(
223 std::io::ErrorKind::InvalidData,
224 format!(
225 "bad PGP cleartext header; expected `{}`; got `{}`",
226 HEADER, line
227 ),
228 ));
229 }
230
231 self.state = ReaderState::Hashes;
232 // Fall through to next loop.
233 }
234 ReaderState::Hashes => {
235 // Following the cleartext header armor are 1 or more `Hash: ` armor headers.
236 // These are terminated by an empty line.
237
238 let mut line = String::with_capacity(16);
239 self.reader.read_line(&mut line)?;
240
241 if let Some(hash) = line.strip_prefix("Hash: ") {
242 // Comma delimited list.
243 for hash in hash.split(',') {
244 let hash = hash.trim();
245
246 if !hash.is_empty() {
247 let hasher = match hash {
248 "MD5" => CleartextHasher::md5(),
249 "SHA1" => CleartextHasher::sha1(),
250 "SHA256" => CleartextHasher::sha256(),
251 "SHA384" => CleartextHasher::sha384(),
252 "SHA512" => CleartextHasher::sha512(),
253 _ => {
254 return Err(io::Error::new(
255 io::ErrorKind::InvalidData,
256 format!("unsupported PGP hash type: {}", hash),
257 ));
258 }
259 };
260
261 self.hashers
262 .entry(u8::from(hasher.algorithm()))
263 .or_insert(hasher);
264 }
265 }
266 } else if line.trim().is_empty() {
267 if self.hashers.is_empty() {
268 return Err(io::Error::new(
269 io::ErrorKind::InvalidData,
270 "bad PGP cleartext signature; no Hash headers",
271 ));
272 }
273
274 self.state = ReaderState::CleartextEmpty(false);
275 // Fall through to next read.
276 } else {
277 return Err(io::Error::new(
278 io::ErrorKind::InvalidData,
279 format!(
280 "bad PGP cleartext signature; expected Hash: header; got {}",
281 line.trim_end()
282 ),
283 ));
284 }
285 }
286
287 // We want to actually return the cleartext data to the caller.
288 // However, we can't just proxy things through because this section
289 // uses dash escaping.
290 //
291 // From RFC 4880 Section 7.1:
292 //
293 // When reversing dash-escaping, an implementation MUST strip the string
294 // "- " if it occurs at the beginning of a line, and SHOULD warn on "-"
295 // and any character other than a space at the beginning of a line.
296 //
297 // (We do not warn.)
298 //
299 // In addition, we need to feed the cleartext data into registered hashers
300 // as we read so we can possibly verify the signatures later without
301 // access to the original content. This is subtly complex. Again per
302 // RFC 4880 Section 7.1:
303 //
304 // As with binary signatures on text documents, a cleartext signature is
305 // calculated on the text using canonical <CR><LF> line endings. The
306 // line ending (i.e., the <CR><LF>) before the '-----BEGIN PGP
307 // SIGNATURE-----' line that terminates the signed text is not
308 // considered part of the signed text.
309 //
310 // That CRLF before the `-----BEGIN PGP SIGNATURE----` line not being part
311 // of the digested content is a super annoying constraint because it forces
312 // us to maintain more state.
313 ReaderState::CleartextEmpty(previous_read) => {
314 let mut line = String::with_capacity(128);
315 self.reader.read_line(&mut line)?;
316
317 let emit = if let Some(stripped) = line.strip_prefix("- ") {
318 stripped
319 } else if matches!(line.as_str(), SIGNATURE_ARMOR_LF | SIGNATURE_ARMOR_CRLF) {
320 // Fall through to continue reading signature data.
321 self.state = ReaderState::Signatures;
322 continue;
323 } else {
324 line.as_str()
325 };
326
327 let no_eol = emit.trim_end_matches(|c| c == '\r' || c == '\n');
328
329 for hasher in self.hashers.values_mut() {
330 // On non-initial reads, feed in CRLF from last line, since we know this
331 // line isn't the end of the cleartext.
332 if *previous_read {
333 hasher.update(b"\r\n");
334 }
335
336 hasher.update(no_eol.as_bytes());
337 }
338
339 // We could continue reading to fill the destination buffer. But that is
340 // more complex.
341 return match dest.len().cmp(&emit.as_bytes().len()) {
342 Ordering::Equal | Ordering::Greater => {
343 // Destination buffer is large enough to hold the line/content we just
344 // read. Just copy it over and return how many bytes we copied.
345 let count = emit.as_bytes().len();
346 let dest = &mut dest[0..count];
347 dest.copy_from_slice(emit.as_bytes());
348 self.state = ReaderState::CleartextEmpty(true);
349
350 Ok(count)
351 }
352 Ordering::Less => {
353 // We read more data than we have an output buffer to write. Copy what
354 // we can then set up the next read to come from the buffer.
355 let (to_copy, remaining) = emit.split_at(dest.len());
356 dest.copy_from_slice(to_copy.as_bytes());
357 self.state = ReaderState::CleartextBuffered(remaining.to_string());
358
359 Ok(to_copy.as_bytes().len())
360 }
361 };
362 }
363
364 ReaderState::CleartextBuffered(ref mut remaining) => {
365 return match dest.len().cmp(&remaining.as_bytes().len()) {
366 Ordering::Equal | Ordering::Greater => {
367 // The destination buffer has enough capacity to hold what we have.
368 // Write it out and revert to clean read mode.
369 let count = remaining.as_bytes().len();
370 let dest = &mut dest[0..count];
371
372 dest.copy_from_slice(remaining.as_bytes());
373 self.state = ReaderState::CleartextEmpty(true);
374
375 Ok(count)
376 }
377 Ordering::Less => {
378 // Write what we can.
379 let count = dest.len();
380
381 let (to_copy, remaining) = remaining.split_at(count);
382
383 dest.copy_from_slice(to_copy.as_bytes());
384 self.state = ReaderState::CleartextBuffered(remaining.to_string());
385
386 Ok(count)
387 }
388 };
389 }
390 ReaderState::Signatures => {
391 // We should only get into this state immediately after reading the
392 // SIGNATURE_ARMOR line.
393
394 // We can conveniently use the pgp crate's armor reader to decode this
395 // data until EOF.
396
397 // Ownership of the reader is a bit wonky. We make life easy by building
398 // a new one. This is inefficient. But meh.
399 let mut buffer = SIGNATURE_ARMOR_LF.as_bytes().to_vec();
400 self.reader.read_to_end(&mut buffer)?;
401
402 let mut dearmor = pgp::armor::Dearmor::new(io::Cursor::new(buffer));
403 dearmor
404 .read_header()
405 .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
406
407 if !matches!(dearmor.typ, Some(pgp::armor::BlockType::Signature)) {
408 return Err(io::Error::new(
409 io::ErrorKind::InvalidData,
410 "failed to parse PGP signature armor",
411 ));
412 }
413
414 for packet in pgp::packet::PacketParser::new(dearmor) {
415 match packet {
416 Ok(Packet::Signature(signature)) => {
417 self.signatures.push(signature);
418 }
419 Ok(packet) => {
420 return Err(io::Error::new(
421 io::ErrorKind::InvalidData,
422 format!(
423 "unexpected PGP packet seen; expected Signature; got {:?}",
424 packet.tag()
425 ),
426 ));
427 }
428 Err(e) => {
429 return Err(io::Error::new(
430 io::ErrorKind::InvalidData,
431 format!("PGP packet parsing error: {:?}", e),
432 ));
433 }
434 }
435 }
436
437 self.state = ReaderState::Eof;
438 return Ok(0);
439 }
440 ReaderState::Eof => {
441 return Ok(0);
442 }
443 }
444 }
445 }
446}
447
448/// Parsed cleartext signatures data.
449///
450/// This type represents the results of parsing cleartext signature data.
451///
452/// When a document containing PGP cleartext signatures is parsed, [CleartextSignatureReader]
453/// derives hashers of the signed content as well as the parsed PGP signature packets. This
454/// data is held by this type to facilitate signature verification.
455pub struct CleartextSignatures {
456 hashers: HashMap<u8, CleartextHasher>,
457 signatures: Vec<Signature>,
458}
459
460impl CleartextSignatures {
461 /// Iterate over signatures in this instance.
462 ///
463 /// This obtains the parsed signature packets as derived from
464 /// `-----BEGIN PGP SIGNATURE-----` sections in the source document.
465 pub fn iter_signatures(&self) -> impl Iterator<Item = &Signature> {
466 self.signatures.iter()
467 }
468
469 /// Iterate over signatures made by a specific key.
470 ///
471 /// This is a convenience wrapper for [Self::iter_signatures()] that filters based on the
472 /// signature's issuer matching the key ID of the specified key.
473 pub fn iter_signatures_from_key<'slf, 'key: 'slf>(
474 &'slf self,
475 key: &'key impl PublicKeyTrait,
476 ) -> impl Iterator<Item = &'slf Signature> {
477 self.signatures
478 .iter()
479 .filter(|sig| sig.issuer().iter().any(|issuer| &key.key_id() == *issuer))
480 }
481
482 /// Verify a signature made from a known key.
483 ///
484 /// Returns the numbers of signatures verified against this key.
485 ///
486 /// If there are no signatures at all or no signatures from the specified key, an error is
487 /// returned.
488 ///
489 /// Errors also occur if a signature could not be verified (possibly due to implementation
490 /// bugs) or if the signature is invalid.
491 pub fn verify(&self, key: &impl PublicKeyTrait) -> pgp::errors::Result<usize> {
492 if self.signatures.is_empty() {
493 return Err(pgp::errors::Error::Message(
494 "no PGP signatures present".to_string(),
495 ));
496 }
497
498 let mut valid_signatures = 0;
499
500 for sig in self.iter_signatures_from_key(key) {
501 // We need to feed signature-specific state into the hasher (which was previously
502 // fed the cleartext) to verify the signature. Fortunately we can clone hashers.
503 let mut hasher = Box::new(
504 self.hashers
505 .get(&(u8::from(sig.config.hash_alg)))
506 .ok_or_else(|| {
507 pgp::errors::Error::Message(format!(
508 "could not find hasher matching signature hash algorithm ({:?})",
509 sig.config.hash_alg
510 ))
511 })?
512 .clone(),
513 );
514
515 let len = sig.config.hash_signature_data(&mut *hasher)?;
516 hasher.update(&sig.config.trailer(len)?);
517
518 let digest = hasher.finish();
519
520 if digest[0..2] != sig.signed_hash_value {
521 return Err(pgp::errors::Error::Message(
522 "invalid signed hash value".into(),
523 ));
524 }
525
526 key.verify_signature(sig.config.hash_alg, &digest, &sig.signature)?;
527 valid_signatures += 1;
528 }
529
530 match valid_signatures {
531 0 => Err(pgp::errors::Error::Message(
532 "no signatures signed by provided key".into(),
533 )),
534 _ => Ok(valid_signatures),
535 }
536 }
537}
538
539/// Produce a cleartext signature over data.
540///
541/// The original cleartext data to be signed is provided by a reader.
542///
543/// The returned value is a multiline string with LF line endings containing the PGP
544/// cleartext framework encoded cleartext and signature. The signature is produced by
545/// the provided key using the specified hashing algorithm.
546///
547/// Normalizing the line endings to a different format (e.g. `\r\n` is allowed, as
548/// cleartext signature framework readers should properly recognize alternate line
549/// endings.
550pub fn cleartext_sign<PW, R>(
551 key: &impl SecretKeyTrait,
552 key_pw: PW,
553 hash_algorithm: HashAlgorithm,
554 data: R,
555) -> pgp::errors::Result<String>
556where
557 PW: FnOnce() -> String,
558 R: BufRead,
559{
560 if !matches!(
561 hash_algorithm,
562 HashAlgorithm::MD5
563 | HashAlgorithm::SHA1
564 | HashAlgorithm::RIPEMD160
565 | HashAlgorithm::SHA2_256
566 | HashAlgorithm::SHA2_384
567 | HashAlgorithm::SHA2_512
568 | HashAlgorithm::SHA2_224,
569 ) {
570 return Err(pgp::errors::Error::Unsupported(
571 "hash algorithm unsupported for cleartext signatures".to_string(),
572 ));
573 }
574
575 // The message digest is computed using the source data. The emitted cleartext
576 // signature contains the dash-escaped normalization of the source data. Furthermore,
577 // line endings in the source data are normalized to CRLF for signature creation.
578
579 let mut dashed_lines = vec![];
580 let mut source_lines = vec![];
581
582 for line in data.lines() {
583 let line = line?;
584
585 // From https://datatracker.ietf.org/doc/html/rfc4880.html#section-7.1:
586 //
587 // Dash-escaped cleartext is the ordinary cleartext where every line
588 // starting with a dash '-' (0x2D) is prefixed by the sequence dash '-'
589 // (0x2D) and space ' ' (0x20). ... An implementation MAY dash-escape any
590 // line, SHOULD dash-escape lines commencing "From" followed by a space, and
591 // MUST dash-escape any line commencing in a dash. ... Also, any trailing
592 // whitespace -- spaces (0x20) and tabs (0x09) -- at the end of any line is
593 // removed when the cleartext signature is generated.
594 dashed_lines.push(if line.starts_with('-') || line.starts_with("From ") {
595 format!("- {}", line.trim_end())
596 } else {
597 line.trim_end().to_string()
598 });
599
600 source_lines.push(line.trim_end().to_string());
601 }
602
603 let cleartext = source_lines.join("\r\n").into_bytes();
604
605 // TODO these sets should be audited by someone who knows PGP.
606
607 let hashed_subpackets = vec![
608 Subpacket::regular(SubpacketData::IssuerFingerprint(key.fingerprint())),
609 Subpacket::regular(SubpacketData::SignatureCreationTime(
610 chrono::Utc::now().trunc_subsecs(0),
611 )),
612 ];
613 let unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(key.key_id()))];
614
615 let mut config = SignatureConfig::v4(SignatureType::Text, key.algorithm(), hash_algorithm);
616 config.hashed_subpackets = hashed_subpackets;
617 config.unhashed_subpackets = unhashed_subpackets;
618
619 let signature = config.sign(key, key_pw, Cursor::new(cleartext))?;
620
621 // The armoring consists of a signature packet.
622 let packet = Packet::Signature(signature);
623 let mut writer = Cursor::new(Vec::<u8>::new());
624 pgp::armor::write(
625 &packet,
626 pgp::armor::BlockType::Signature,
627 &mut writer,
628 None,
629 true,
630 )?;
631
632 // The armoring should always produce valid UTF-8. But we are careful.
633 let signature_string = String::from_utf8(writer.into_inner())
634 .map_err(|e| pgp::errors::Error::Utf8Error(e.utf8_error()))?;
635
636 // The cleartext consists of the header, the hash identifier, an empty line, the
637 // dash-escaped lines, and finally the signature armor.
638 let lines = vec![
639 HEADER.to_string(),
640 format!(
641 "Hash: {}",
642 match hash_algorithm {
643 HashAlgorithm::MD5 => "MD5",
644 HashAlgorithm::SHA1 => "SHA1",
645 HashAlgorithm::RIPEMD160 => "RIPEMD160",
646 HashAlgorithm::SHA2_256 => "SHA256",
647 HashAlgorithm::SHA2_384 => "SHA384",
648 HashAlgorithm::SHA2_512 => "SHA512",
649 HashAlgorithm::SHA2_224 => "SHA224",
650 _ => panic!("hash algorithm should have been validated above"),
651 }
652 ),
653 "".to_string(),
654 ]
655 .into_iter()
656 .chain(dashed_lines)
657 .chain(std::iter::once(signature_string))
658 .collect::<Vec<_>>();
659
660 // We could potentially make the line ending configurable, as a cleartext reader
661 // must normalize lines.
662 Ok(lines.join("\n"))
663}