debian_control/
pgp.rs

1//! PGP signature parsing.
2
3/// Error during PGP signature parsing.
4#[derive(Debug, PartialEq, Eq)]
5pub enum Error {
6    /// Missing PGP signature.
7    MissingPgpSignature,
8
9    /// Payload missing in the signed message.
10    MissingPayload,
11
12    /// Truncated PGP signature.
13    TruncatedPgpSignature,
14
15    /// Junk after PGP signature.
16    JunkAfterPgpSignature,
17}
18
19impl std::fmt::Display for Error {
20    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
21        match self {
22            Error::MissingPgpSignature => write!(f, "missing PGP signature"),
23            Error::TruncatedPgpSignature => write!(f, "truncated PGP signature"),
24            Error::JunkAfterPgpSignature => write!(f, "junk after PGP signature"),
25            Error::MissingPayload => write!(f, "missing payload"),
26        }
27    }
28}
29
30impl std::error::Error for Error {}
31
32/// Strip a PGP signature from a signed message.
33///
34/// This function takes a signed message and returns the payload and the PGP signature.
35/// If the input is not a signed message, the function returns the input as the payload and `None`
36/// as the signature.
37///
38/// # Arguments
39/// * `input` - The signed message.
40///
41/// # Errors
42/// This function returns an error if the input is a signed message but the payload, PGP signature,
43/// or the PGP signature metadata is missing, or if there is junk after the PGP signature.
44/// The error indicates the reason for the failure.
45///
46/// # Returns
47/// A tuple containing the payload and the PGP signature, if present.
48///
49/// # Examples
50/// ```
51/// let input = "-----BEGIN PGP SIGNED MESSAGE-----
52/// Hash: SHA256
53///
54/// Hello, world!
55/// -----BEGIN PGP SIGNATURE-----
56/// iQIzBAEBCAAdFiEEpyNohvPMyq0Uiif4DphATThvodkFAmbJ6swACgkQDphATThv
57/// odkUiw//VDVOwHGRVxpvyIjSvH0AMQmANOvolJ5EoCu1I5UG2x98UPiMV5oTNv1r
58/// ...
59/// =olY7
60/// -----END PGP SIGNATURE-----
61/// ";
62/// let (output, signature) = debian_control::pgp::strip_pgp_signature(input).unwrap();
63/// assert_eq!(output, "Hello, world!\n");
64/// assert_eq!(signature.unwrap().len(), 136);
65/// ```
66pub fn strip_pgp_signature(input: &str) -> Result<(String, Option<String>), Error> {
67    let mut lines = input.lines();
68    let first_line = if let Some(line) = lines.next() {
69        line
70    } else {
71        return Ok((input.to_string(), None));
72    };
73    if first_line != "-----BEGIN PGP SIGNED MESSAGE-----" {
74        return Ok((input.to_string(), None));
75    }
76
77    // Read the metadata
78    let mut metadata = String::new();
79    loop {
80        let line = lines.next().ok_or(Error::MissingPayload)?;
81        if line.is_empty() {
82            break;
83        }
84        use std::fmt::Write;
85        writeln!(&mut metadata, "{}", line).unwrap();
86    }
87
88    let mut payload = String::new();
89    loop {
90        let line = lines.next().ok_or(Error::MissingPgpSignature)?;
91        if line == "-----BEGIN PGP SIGNATURE-----" {
92            break;
93        }
94        use std::fmt::Write;
95        writeln!(&mut payload, "{}", line).unwrap();
96    }
97
98    let mut signature = String::new();
99    loop {
100        let line = lines.next().ok_or(Error::TruncatedPgpSignature)?;
101        if line == "-----END PGP SIGNATURE-----" {
102            break;
103        }
104        signature.push_str(line);
105    }
106
107    if let Some(_line) = lines.next() {
108        return Err(Error::JunkAfterPgpSignature);
109    }
110
111    Ok((payload, Some(signature)))
112}
113
114#[cfg(test)]
115mod tests {
116    #[test]
117    fn test_strip_pgp_wrapper() {
118        let input = include_str!("testdata/InRelease");
119
120        let (output, signature) = super::strip_pgp_signature(input).unwrap();
121
122        assert_eq!(
123            output,
124            r###"Origin: Debian
125Label: Debian
126Suite: experimental
127Codename: rc-buggy
128Changelogs: https://metadata.ftp-master.debian.org/changelogs/@CHANGEPATH@_changelog
129Date: Sat, 24 Aug 2024 14:13:49 UTC
130Valid-Until: Sat, 31 Aug 2024 14:13:49 UTC
131NotAutomatic: yes
132Acquire-By-Hash: yes
133No-Support-for-Architecture-all: Packages
134Architectures: all amd64 arm64 armel armhf i386 mips64el ppc64el riscv64 s390x
135Components: main contrib non-free-firmware non-free
136Description: Experimental packages - not released; use at your own risk.
137"###
138        );
139
140        assert_eq!(
141            signature.as_deref(),
142            Some(
143                r###"iQIzBAEBCAAdFiEEpyNohvPMyq0Uiif4DphATThvodkFAmbJ6swACgkQDphATThv
144odkUiw//VDVOwHGRVxpvyIjSvH0AMQmANOvolJ5EoCu1I5UG2x98UPiMV5oTNv1r
145B79A3nb+FL2toeuHUJBN3G1WNg6xeH0vD43hGcxhCgVn6NADogv8pBEpyynn1qC0
146iketp6kEiHvGMpEj4JqOUEcq2Mafq2TTf9zEqYuTr8NqL9hC/pG8YqPKT3rhPdc3
147/D4/0dTT7L+wqLgVTjjNFNcmKU1ywvaWLF5b0VktZ1W6xIqnZYfHyP0iMqolrGqF
148+NG+igpsMuLI6JtoqoE+yKtWlaQi7pY7VB+OFroywNxEobzPgwdqz0r8pdJq8S4V
149CMXJqoY18KHdVd4qbU9yGr6qkqopHdMMcpvV9X1UG5xDKUb2OrdbBYttKFGuIuuM
150S6ZzM+26bztVXLzSra/w7gn7Qm1GluT+EncYrleAAgUvruCRrLFptDpHMAuKWKs5
151OyNLh1ZUe/TrkmYGhehsVEBNmG+/HnzS7VKzLfpANHLAXthoEF9Lzqe0lPETa9NZ
152rSF/EfQwh8omsaBDfighU46fZJwKGSWOIz69jXrQ6YV9hBI/frUDHQUkMLBwjnVo
1538hvr0s6/8hHRwNlLRW3XQuwL+wiz0qyk6u6RRudglqSyN1FwIAtTsGkERWN82au2
154DY6KLpnfN7/0bIueDUWCP40Dib+eW5Y0/Z536WhNbp8C/OIKeVyJAjMEAQEIAB0W
155IQRMtQGQIHtHWKP3Onlu0Oe4JkPhMQUCZsnq6QAKCRBu0Oe4JkPhMUJsD/0ZTGIM
156oI9bzhP6NadhiNNruxLQfq/+fVx/oJbyOJy4IaYPOE0JVeqzZv/wFL/XOVXw6Gg2
157V/SHe0cT+iuwdKd+8oMEYaOHQUeU8RhAguypeTdizZef3YjIL+2n4v0mLeq/jMHO
158a6Hyd09eUQrHedmcgViwQYOX/9/oqls0j3OGtyx1gmpIsmCxJtqsWNsXEcBLaNlm
159xSAp5YYa5USenFAph4VlR2sG+VdJrG/wtCj8TuDtJCA4tOML3JB5zwgnzfpLMVU6
160l+WFKkSzl0f/dlMUYRtRoU9ccpWpyajMs968QsOp0lKLZ5Kq98fSXqOzKriDimpv
1614WSmlLRptRgKL0J/Nc1eYRVEPnu+tBsitLdip52SLrqYcbCOErtxOLMIIbbC2HiR
162Q0lYYgky2TwO8bbCWhTyQIznldnSRhNE1STf5bctphNeWQE6zRFmMGyHh9pQYVNF
163KkmCbzHcv6EbUOp7Q7c5D/mijN8On/h9TEYU6EbbrQ1AEc+IulXukzlaLCMKJ0Tx
164XqsogWqW/nbOxTdudMn+qjd7gVsLtNIDKA42Csyac5Hwl9YDqgicyOMGBY88gocV
1658fDXnyUhX5Es35AgO25Sh8CbISC29479o4/MdZXCGMIJEocjPx46Dy+hP1sIcFyp
166KYQwHDLf3TLHWF9z0lvGFYSAq1H8gOwchDISGA==
167=olY7
168"###
169                .replace('\n', "")
170                .as_ref()
171            )
172        );
173    }
174
175    #[test]
176    fn test_strip_pgp_no_pgp_signature() {
177        let input = "Hello, world!";
178        let (output, signature) = super::strip_pgp_signature(input).unwrap();
179        assert_eq!(output, input);
180        assert_eq!(signature, None);
181    }
182
183    #[test]
184    fn test_strip_pgp_missing_payload() {
185        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
186Hash: SHA256
187"###;
188        let err = super::strip_pgp_signature(input).unwrap_err();
189        assert_eq!(err, super::Error::MissingPayload);
190    }
191
192    #[test]
193    fn test_strip_pgp_missing_pgp_signature() {
194        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
195Hash: SHA256
196
197Hello, world!
198"###;
199        let err = super::strip_pgp_signature(input).unwrap_err();
200        assert_eq!(err, super::Error::MissingPgpSignature);
201    }
202
203    #[test]
204    fn test_strip_pgp_truncated_pgp_signature() {
205        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
206Hash: SHA256
207
208Hello, world!
209
210-----BEGIN PGP SIGNATURE-----
211B79A3nb+FL2toeuHUJBN3G1WNg6xeH0vD43hGcxhCgVn6NADogv8pBEpyynn1qC0
212"###;
213        let err = super::strip_pgp_signature(input).unwrap_err();
214        assert_eq!(err, super::Error::TruncatedPgpSignature);
215    }
216
217    #[test]
218    fn test_strip_pgp_junk_after_pgp_signature() {
219        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
220Hash: SHA256
221
222Hello, world!
223
224-----BEGIN PGP SIGNATURE-----
225B79A3nb+FL2toeuHUJBN3G1WNg6xeH0vD43hGcxhCgVn6NADogv8pBEpyynn1qC0
226-----END PGP SIGNATURE-----
227Junk after PGP signature
228"###;
229        let err = super::strip_pgp_signature(input).unwrap_err();
230        assert_eq!(err, super::Error::JunkAfterPgpSignature);
231    }
232}