1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
    MissingPgpSignature,
    MissingPayload,
    TruncatedPgpSignature,
    JunkAfterPgpSignature,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Error::MissingPgpSignature => write!(f, "missing PGP signature"),
            Error::TruncatedPgpSignature => write!(f, "truncated PGP signature"),
            Error::JunkAfterPgpSignature => write!(f, "junk after PGP signature"),
            Error::MissingPayload => write!(f, "missing payload"),
        }
    }
}

impl std::error::Error for Error {}

/// Strip a PGP signature from a signed message.
///
/// This function takes a signed message and returns the payload and the PGP signature.
/// If the input is not a signed message, the function returns the input as the payload and `None`
/// as the signature.
///
/// # Arguments
/// * `input` - The signed message.
///
/// # Errors
/// This function returns an error if the input is a signed message but the payload, PGP signature,
/// or the PGP signature metadata is missing, or if there is junk after the PGP signature.
/// The error indicates the reason for the failure.
///
/// # Returns
/// A tuple containing the payload and the PGP signature, if present.
///
/// # Examples
/// ```
/// let input = "-----BEGIN PGP SIGNED MESSAGE-----
/// Hash: SHA256
///
/// Hello, world!
/// -----BEGIN PGP SIGNATURE-----
/// iQIzBAEBCAAdFiEEpyNohvPMyq0Uiif4DphATThvodkFAmbJ6swACgkQDphATThv
/// odkUiw//VDVOwHGRVxpvyIjSvH0AMQmANOvolJ5EoCu1I5UG2x98UPiMV5oTNv1r
/// ...
/// =olY7
/// -----END PGP SIGNATURE-----
/// ";
/// let (output, signature) = debian_control::pgp::strip_pgp_signature(input).unwrap();
/// assert_eq!(output, "Hello, world!\n");
/// assert_eq!(signature.unwrap().len(), 136);
/// ```
pub fn strip_pgp_signature(input: &str) -> Result<(String, Option<String>), Error> {
    let mut lines = input.lines();
    let first_line = if let Some(line) = lines.next() {
        line
    } else {
        return Ok((input.to_string(), None));
    };
    if first_line != "-----BEGIN PGP SIGNED MESSAGE-----" {
        return Ok((input.to_string(), None));
    }

    // Read the metadata
    let mut metadata = String::new();
    loop {
        let line = if let Some(line) = lines.next() {
            line
        } else {
            return Err(Error::MissingPayload);
        };
        if line.is_empty() {
            break;
        }
        metadata.push_str(line);
        metadata.push('\n');
    }

    let mut payload = String::new();
    loop {
        let line = if let Some(line) = lines.next() {
            line
        } else {
            return Err(Error::MissingPgpSignature);
        };
        if line == "-----BEGIN PGP SIGNATURE-----" {
            break;
        }
        payload.push_str(line);
        payload.push('\n');
    }

    let mut signature = String::new();
    loop {
        let line = if let Some(line) = lines.next() {
            line
        } else {
            return Err(Error::TruncatedPgpSignature);
        };
        if line == "-----END PGP SIGNATURE-----" {
            break;
        }
        signature.push_str(line);
    }

    if let Some(_line) = lines.next() {
        return Err(Error::JunkAfterPgpSignature);
    }

    Ok((payload, Some(signature)))
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_strip_pgp_wrapper() {
        let input = include_str!("testdata/InRelease");

        let (output, signature) = super::strip_pgp_signature(input).unwrap();

        assert_eq!(output, r###"Origin: Debian
Label: Debian
Suite: experimental
Codename: rc-buggy
Changelogs: https://metadata.ftp-master.debian.org/changelogs/@CHANGEPATH@_changelog
Date: Sat, 24 Aug 2024 14:13:49 UTC
Valid-Until: Sat, 31 Aug 2024 14:13:49 UTC
NotAutomatic: yes
Acquire-By-Hash: yes
No-Support-for-Architecture-all: Packages
Architectures: all amd64 arm64 armel armhf i386 mips64el ppc64el riscv64 s390x
Components: main contrib non-free-firmware non-free
Description: Experimental packages - not released; use at your own risk.
"###);

        assert_eq!(signature.as_deref(), Some(r###"iQIzBAEBCAAdFiEEpyNohvPMyq0Uiif4DphATThvodkFAmbJ6swACgkQDphATThv
odkUiw//VDVOwHGRVxpvyIjSvH0AMQmANOvolJ5EoCu1I5UG2x98UPiMV5oTNv1r
B79A3nb+FL2toeuHUJBN3G1WNg6xeH0vD43hGcxhCgVn6NADogv8pBEpyynn1qC0
iketp6kEiHvGMpEj4JqOUEcq2Mafq2TTf9zEqYuTr8NqL9hC/pG8YqPKT3rhPdc3
/D4/0dTT7L+wqLgVTjjNFNcmKU1ywvaWLF5b0VktZ1W6xIqnZYfHyP0iMqolrGqF
+NG+igpsMuLI6JtoqoE+yKtWlaQi7pY7VB+OFroywNxEobzPgwdqz0r8pdJq8S4V
CMXJqoY18KHdVd4qbU9yGr6qkqopHdMMcpvV9X1UG5xDKUb2OrdbBYttKFGuIuuM
S6ZzM+26bztVXLzSra/w7gn7Qm1GluT+EncYrleAAgUvruCRrLFptDpHMAuKWKs5
OyNLh1ZUe/TrkmYGhehsVEBNmG+/HnzS7VKzLfpANHLAXthoEF9Lzqe0lPETa9NZ
rSF/EfQwh8omsaBDfighU46fZJwKGSWOIz69jXrQ6YV9hBI/frUDHQUkMLBwjnVo
8hvr0s6/8hHRwNlLRW3XQuwL+wiz0qyk6u6RRudglqSyN1FwIAtTsGkERWN82au2
DY6KLpnfN7/0bIueDUWCP40Dib+eW5Y0/Z536WhNbp8C/OIKeVyJAjMEAQEIAB0W
IQRMtQGQIHtHWKP3Onlu0Oe4JkPhMQUCZsnq6QAKCRBu0Oe4JkPhMUJsD/0ZTGIM
oI9bzhP6NadhiNNruxLQfq/+fVx/oJbyOJy4IaYPOE0JVeqzZv/wFL/XOVXw6Gg2
V/SHe0cT+iuwdKd+8oMEYaOHQUeU8RhAguypeTdizZef3YjIL+2n4v0mLeq/jMHO
a6Hyd09eUQrHedmcgViwQYOX/9/oqls0j3OGtyx1gmpIsmCxJtqsWNsXEcBLaNlm
xSAp5YYa5USenFAph4VlR2sG+VdJrG/wtCj8TuDtJCA4tOML3JB5zwgnzfpLMVU6
l+WFKkSzl0f/dlMUYRtRoU9ccpWpyajMs968QsOp0lKLZ5Kq98fSXqOzKriDimpv
4WSmlLRptRgKL0J/Nc1eYRVEPnu+tBsitLdip52SLrqYcbCOErtxOLMIIbbC2HiR
Q0lYYgky2TwO8bbCWhTyQIznldnSRhNE1STf5bctphNeWQE6zRFmMGyHh9pQYVNF
KkmCbzHcv6EbUOp7Q7c5D/mijN8On/h9TEYU6EbbrQ1AEc+IulXukzlaLCMKJ0Tx
XqsogWqW/nbOxTdudMn+qjd7gVsLtNIDKA42Csyac5Hwl9YDqgicyOMGBY88gocV
8fDXnyUhX5Es35AgO25Sh8CbISC29479o4/MdZXCGMIJEocjPx46Dy+hP1sIcFyp
KYQwHDLf3TLHWF9z0lvGFYSAq1H8gOwchDISGA==
=olY7
"###.replace('\n', "").as_ref()));
    }

    #[test]
    fn test_strip_pgp_no_pgp_signature() {
        let input = "Hello, world!";
        let (output, signature) = super::strip_pgp_signature(input).unwrap();
        assert_eq!(output, input);
        assert_eq!(signature, None);
    }

    #[test]
    fn test_strip_pgp_missing_payload() {
        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
"###;
        let err = super::strip_pgp_signature(input).unwrap_err();
        assert_eq!(err, super::Error::MissingPayload);
    }

    #[test]
    fn test_strip_pgp_missing_pgp_signature() {
        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Hello, world!
"###;
        let err = super::strip_pgp_signature(input).unwrap_err();
        assert_eq!(err, super::Error::MissingPgpSignature);
    }

    #[test]
    fn test_strip_pgp_truncated_pgp_signature() {
        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Hello, world!

-----BEGIN PGP SIGNATURE-----
B79A3nb+FL2toeuHUJBN3G1WNg6xeH0vD43hGcxhCgVn6NADogv8pBEpyynn1qC0
"###;
        let err = super::strip_pgp_signature(input).unwrap_err();
        assert_eq!(err, super::Error::TruncatedPgpSignature);
    }

    #[test]
    fn test_strip_pgp_junk_after_pgp_signature() {
        let input = r###"-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

Hello, world!

-----BEGIN PGP SIGNATURE-----
B79A3nb+FL2toeuHUJBN3G1WNg6xeH0vD43hGcxhCgVn6NADogv8pBEpyynn1qC0
-----END PGP SIGNATURE-----
Junk after PGP signature
"###;
        let err = super::strip_pgp_signature(input).unwrap_err();
        assert_eq!(err, super::Error::JunkAfterPgpSignature);
    }
}