voa_config/config/technology/openpgp/
filter.rs

1//! Types that can be used for filtering OpenPGP packets.
2
3use std::{fmt::Display, str::FromStr};
4
5use addr::parse_domain_name;
6use const_hex::{decode_to_slice, encode};
7use serde::{Deserialize, Serialize};
8
9use crate::Error;
10
11/// The match for an OpenPGP User ID.
12#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
13pub struct DomainName(String);
14
15impl FromStr for DomainName {
16    type Err = Error;
17
18    fn from_str(s: &str) -> Result<Self, Self::Err> {
19        let name = parse_domain_name(s).map_err(|source| {
20            crate::config::technology::openpgp::Error::InvalidDomainName {
21                domain_name: s.to_string(),
22                message: source.to_string(),
23            }
24        })?;
25        // Fail if the domain name does not have a root (e.g. only "example" instead of
26        // "example.org").
27        if name.root().is_none() {
28            return Err(
29                crate::config::technology::openpgp::Error::InvalidDomainName {
30                    domain_name: s.to_string(),
31                    message: "The domain cannot be a top-level domain".to_string(),
32                }
33                .into(),
34            );
35        }
36
37        Ok(Self(name.to_string()))
38    }
39}
40
41impl Display for DomainName {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        write!(f, "{}", self.0)
44    }
45}
46
47/// An OpenPGP fingerprint.
48#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
49#[serde(into = "String", try_from = "String")]
50pub enum OpenpgpFingerprint {
51    /// An OpenPGP V4 fingerprint.
52    V4([u8; 20]),
53
54    /// An OpenPGP V6 fingerprint.
55    V6([u8; 32]),
56}
57
58impl AsRef<[u8]> for OpenpgpFingerprint {
59    fn as_ref(&self) -> &[u8] {
60        match self {
61            Self::V4(bytes) => bytes,
62            Self::V6(bytes) => bytes,
63        }
64    }
65}
66
67impl From<OpenpgpFingerprint> for String {
68    fn from(value: OpenpgpFingerprint) -> Self {
69        value.to_string()
70    }
71}
72
73impl FromStr for OpenpgpFingerprint {
74    type Err = Error;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        match s.len() {
78            40 => {
79                let mut output = [0u8; 20];
80                decode_to_slice(s, &mut output).map_err(|source| {
81                    crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint {
82                        fingerprint: s.into(),
83                        context: "creating an OpenPGP v4 fingerprint".to_string(),
84                        source,
85                    }
86                })?;
87                Ok(Self::V4(output))
88            }
89            64 => {
90                let mut output = [0u8; 32];
91                decode_to_slice(s, &mut output).map_err(|source| {
92                    crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint {
93                        fingerprint: s.into(),
94                        context: "creating an OpenPGP v6 fingerprint".to_string(),
95                        source,
96                    }
97                })?;
98                Ok(Self::V6(output))
99            }
100            _ => Err(
101                crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprintLength {
102                    fingerprint: s.into(),
103                }
104                .into(),
105            ),
106        }
107    }
108}
109
110impl TryFrom<String> for OpenpgpFingerprint {
111    type Error = Error;
112
113    fn try_from(value: String) -> Result<Self, Self::Error> {
114        Self::from_str(&value)
115    }
116}
117
118impl Display for OpenpgpFingerprint {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        write!(
121            f,
122            "{}",
123            match self {
124                Self::V4(bytes) => encode(bytes),
125                Self::V6(bytes) => encode(bytes),
126            }
127        )
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use rstest::rstest;
134    use testresult::TestResult;
135
136    use super::*;
137
138    #[rstest]
139    #[case("example.org")]
140    #[case("sub.example.org")]
141    fn domain_name_from_str_succeeds(#[case] name: &str) -> TestResult {
142        let _domain = DomainName::from_str(name)?;
143        Ok(())
144    }
145
146    #[rstest]
147    #[case("example foo")]
148    #[case("fail")]
149    #[case("_")]
150    fn domain_name_from_str_fails(#[case] name: &str) -> TestResult {
151        match DomainName::from_str(name) {
152            Err(Error::OpenpgpTechnology(
153                crate::config::technology::openpgp::Error::InvalidDomainName { .. },
154            )) => {}
155            Err(error) => panic!(
156                "Expected to fail with an Error::InvalidDomainName, but failed with another error instead: {error}"
157            ),
158            Ok(name) => panic!(
159                "Expected to fail with an Error::InvalidDomainName, but succeeded instead: {name:?}"
160            ),
161        }
162
163        Ok(())
164    }
165
166    #[rstest]
167    #[case::v4("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15")]
168    #[case::v6("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c")]
169    fn lowercase_openpgp_fingerprint_roundtrip_succeeds(#[case] input: &str) -> TestResult {
170        let fingerprint = OpenpgpFingerprint::from_str(input)?;
171        assert_eq!(fingerprint.to_string(), input);
172        Ok(())
173    }
174
175    #[rstest]
176    #[case::v4(
177        "f1d2d2f924e986ac86fdf7b36c94bcdf32beec15",
178        &[241, 210, 210, 249, 36, 233, 134, 172, 134, 253, 247, 179, 108, 148, 188, 223, 50, 190, 236, 21]
179    )]
180    #[case::v6(
181        "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c",
182        &[
183            181, 187, 157, 128, 20, 160, 249, 177, 214, 30, 33, 231, 150, 215, 141, 204, 223, 19,
184            82, 242, 60, 211, 40, 18, 244, 133, 11, 135, 138, 228, 148, 76,
185        ]
186    )]
187    fn openpgp_fingerprint_as_ref_matches(
188        #[case] input: &str,
189        #[case] expected: &[u8],
190    ) -> TestResult {
191        let fingerprint = OpenpgpFingerprint::from_str(input)?;
192        assert_eq!(fingerprint.as_ref(), expected);
193        Ok(())
194    }
195
196    #[rstest]
197    #[case::wrong_chars_for_v4("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")]
198    #[case::wrong_chars_for_v6("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX")]
199    fn openpgp_fingerprint_from_str_fails_on_chars(#[case] input: &str) -> TestResult {
200        match OpenpgpFingerprint::from_str(input) {
201            Err(Error::OpenpgpTechnology(
202                crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprint { .. },
203            )) => {}
204            Err(error) => panic!(
205                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but failed with another error instead: {error}"
206            ),
207            Ok(name) => panic!(
208                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but succeeded instead: {name:?}"
209            ),
210        }
211
212        Ok(())
213    }
214
215    #[rstest]
216    #[case::too_long_for_v4("f1d2d2f924e986ac86fdf7b36c94bcdf32beec15234234")]
217    #[case::too_short_for_v4("f1d2d2f924e986ac86fdf7b36c94bcdf32")]
218    #[case::too_long_for_v6(
219        "b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c2234234"
220    )]
221    #[case::too_short_for_v6("b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878")]
222    fn openpgp_fingerprint_from_str_fails_on_length(#[case] input: &str) -> TestResult {
223        match OpenpgpFingerprint::from_str(input) {
224            Err(Error::OpenpgpTechnology(
225                crate::config::technology::openpgp::Error::InvalidOpenpgpFingerprintLength {
226                    ..
227                },
228            )) => {}
229            Err(error) => panic!(
230                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but failed with another error instead: {error}"
231            ),
232            Ok(name) => panic!(
233                "Expected to fail with an Error::InvalidOpenpgpFingerprint, but succeeded instead: {name:?}"
234            ),
235        }
236
237        Ok(())
238    }
239}