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}