signstar_crypto/
openpgp.rs

1//! OpenPGP functionality for Signstar.
2
3use std::{
4    borrow::Borrow,
5    collections::HashSet,
6    fmt::{Debug, Display},
7    str::FromStr,
8    string::FromUtf8Error,
9};
10
11use email_address::{EmailAddress, Options};
12use log::error;
13use pgp::{
14    packet::KeyFlags,
15    types::{KeyVersion, SignedUser},
16};
17use serde::Deserialize;
18use strum::{EnumIter, IntoStaticStr};
19
20/// An error that may occur when working with OpenPGP data.
21#[derive(Debug, thiserror::Error)]
22pub enum Error {
23    /// Duplicate OpenPGP User ID
24    #[error("The OpenPGP User ID {user_id} is used more than once!")]
25    DuplicateUserId {
26        /// The duplicate OpenPGP User ID.
27        user_id: OpenPgpUserId,
28    },
29
30    /// Provided OpenPGP version is invalid
31    #[error("Invalid OpenPGP version: {0}")]
32    InvalidOpenPgpVersion(String),
33
34    /// The User ID is too large
35    #[error("The OpenPGP User ID is too large: {user_id}")]
36    UserIdTooLarge {
37        /// The string that is too long to be used as an OpenPGP User ID.
38        user_id: String,
39    },
40
41    /// A UTF-8 error when trying to create a string from bytes.
42    #[error("Creating a valid UTF-8 string from bytes failed while {context}:\n{source}")]
43    FromUtf8 {
44        /// The context in which a UTF-8 error occurred.
45        ///
46        /// This is meant to complete the sentence "Creating a valid UTF-8 string from bytes failed
47        /// while ".
48        context: &'static str,
49        /// The source error.
50        source: FromUtf8Error,
51    },
52}
53
54/// The OpenPGP version
55#[derive(
56    Clone,
57    Copy,
58    Debug,
59    Default,
60    Deserialize,
61    strum::Display,
62    EnumIter,
63    Hash,
64    IntoStaticStr,
65    Eq,
66    PartialEq,
67    serde::Serialize,
68)]
69#[serde(into = "String", try_from = "String")]
70pub enum OpenPgpVersion {
71    /// OpenPGP version 4 as defined in [RFC 4880]
72    ///
73    /// [RFC 4880]: https://www.rfc-editor.org/rfc/rfc4880.html
74    #[default]
75    #[strum(to_string = "4")]
76    V4,
77
78    /// OpenPGP version 6 as defined in [RFC 9580]
79    ///
80    /// [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html
81    #[strum(to_string = "6")]
82    V6,
83}
84
85impl AsRef<str> for OpenPgpVersion {
86    fn as_ref(&self) -> &str {
87        match self {
88            Self::V4 => "4",
89            Self::V6 => "6",
90        }
91    }
92}
93
94impl FromStr for OpenPgpVersion {
95    type Err = Error;
96
97    /// Creates an [`OpenPgpVersion`] from a string slice
98    ///
99    /// Only valid OpenPGP versions are considered:
100    /// * [RFC 4880] aka "v4"
101    /// * [RFC 9580] aka "v6"
102    ///
103    /// # Errors
104    ///
105    /// Returns an error if the provided string slice does not represent a valid OpenPGP version.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use std::str::FromStr;
111    ///
112    /// use signstar_crypto::openpgp::OpenPgpVersion;
113    ///
114    /// # fn main() -> testresult::TestResult {
115    /// assert_eq!(OpenPgpVersion::from_str("4")?, OpenPgpVersion::V4);
116    /// assert_eq!(OpenPgpVersion::from_str("6")?, OpenPgpVersion::V6);
117    ///
118    /// assert!(OpenPgpVersion::from_str("5").is_err());
119    /// # Ok(())
120    /// # }
121    /// ```
122    /// [RFC 4880]: https://www.rfc-editor.org/rfc/rfc4880.html
123    /// [RFC 9580]: https://www.rfc-editor.org/rfc/rfc9580.html
124    fn from_str(s: &str) -> Result<Self, Self::Err> {
125        match s {
126            "4" | "v4" | "V4" | "OpenPGPv4" => Ok(Self::V4),
127            "5" | "v5" | "V5" | "OpenPGPv5" => Err(Error::InvalidOpenPgpVersion(format!(
128                "{s} (\"we don't do these things around here\")"
129            ))),
130            "6" | "v6" | "V6" | "OpenPGPv6" => Ok(Self::V6),
131            _ => Err(Error::InvalidOpenPgpVersion(s.to_string())),
132        }
133    }
134}
135
136impl From<OpenPgpVersion> for String {
137    fn from(value: OpenPgpVersion) -> Self {
138        value.to_string()
139    }
140}
141
142impl TryFrom<KeyVersion> for OpenPgpVersion {
143    type Error = Error;
144
145    /// Creates an [`OpenPgpVersion`] from a [`KeyVersion`].
146    ///
147    /// # Errors
148    ///
149    /// Returns an error if an invalid OpenPGP version is encountered.
150    fn try_from(value: KeyVersion) -> Result<Self, Self::Error> {
151        Ok(match value {
152            KeyVersion::V4 => Self::V4,
153            KeyVersion::V6 => Self::V6,
154            _ => {
155                return Err(Error::InvalidOpenPgpVersion(
156                    Into::<u8>::into(value).to_string(),
157                ));
158            }
159        })
160    }
161}
162
163impl TryFrom<String> for OpenPgpVersion {
164    type Error = Error;
165
166    fn try_from(value: String) -> Result<Self, Self::Error> {
167        Self::from_str(&value)
168    }
169}
170
171/// A distinction between types of OpenPGP User IDs
172#[derive(Clone, Debug, Eq, Hash, PartialEq)]
173enum OpenPgpUserIdType {
174    /// An OpenPGP User ID that contains a valid e-mail address (e.g. "John Doe
175    /// <john@example.org>")
176    ///
177    /// The e-mail address must use a top-level domain (TLD) and no domain literal (e.g. an IP
178    /// address) is allowed.
179    Email(EmailAddress),
180
181    /// A plain OpenPGP User ID
182    ///
183    /// The User ID may contain any UTF-8 character, but does not represent a valid e-mail address.
184    Plain(String),
185}
186
187/// A basic representation of a User ID for OpenPGP
188///
189/// While [OpenPGP User IDs] are loosely defined to be UTF-8 strings, they do not enforce
190/// particular rules around the use of e-mail addresses or their general length.
191/// This type allows to distinguish between plain UTF-8 strings and valid e-mail addresses.
192/// Valid e-mail addresses must provide a display part, use a top-level domain (TLD) and not rely on
193/// domain literals (e.g. IP address).
194/// The length of a User ID is implicitly limited by the maximum length of an OpenPGP packet (8192
195/// bytes).
196/// As such, this type only allows a maximum length of 4096 bytes as middle ground.
197///
198/// [OpenPGP User IDs]: https://www.rfc-editor.org/rfc/rfc9580.html#name-user-id-packet-type-id-13
199#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)]
200#[serde(into = "String", try_from = "String")]
201pub struct OpenPgpUserId(OpenPgpUserIdType);
202
203impl OpenPgpUserId {
204    /// Creates a new [`OpenPgpUserId`] from a String
205    ///
206    /// # Errors
207    ///
208    /// Returns an [`Error::UserIdTooLarge`] if the chars of the provided String exceed
209    /// 4096 bytes. This ensures to stay below the valid upper limit defined by the maximum OpenPGP
210    /// packet size of 8192 bytes.
211    ///
212    /// # Examples
213    ///
214    /// ```
215    /// use std::str::FromStr;
216    ///
217    /// use signstar_crypto::openpgp::OpenPgpUserId;
218    ///
219    /// # fn main() -> testresult::TestResult {
220    /// assert!(!OpenPgpUserId::new("🤡".to_string())?.is_email());
221    ///
222    /// assert!(OpenPgpUserId::new("🤡 <foo@xn--rl8h.org>".to_string())?.is_email());
223    ///
224    /// // an e-mail without a display name is not considered a valid e-mail
225    /// assert!(!OpenPgpUserId::new("<foo@xn--rl8h.org>".to_string())?.is_email());
226    ///
227    /// // this fails because the provided String is too long
228    /// assert!(OpenPgpUserId::new("U".repeat(4097)).is_err());
229    /// # Ok(())
230    /// # }
231    /// ```
232    pub fn new(user_id: String) -> Result<Self, Error> {
233        if user_id.len() > 4096 {
234            return Err(Error::UserIdTooLarge { user_id });
235        }
236        if let Ok(email) = EmailAddress::parse_with_options(
237            &user_id,
238            Options::default()
239                .with_required_tld()
240                .without_domain_literal(),
241        ) {
242            Ok(Self(OpenPgpUserIdType::Email(email)))
243        } else {
244            Ok(Self(OpenPgpUserIdType::Plain(user_id)))
245        }
246    }
247
248    /// Returns whether the [`OpenPgpUserId`] is a valid e-mail address
249    ///
250    /// # Examples
251    ///
252    /// ```
253    /// use signstar_crypto::openpgp::OpenPgpUserId;
254    ///
255    /// # fn main() -> testresult::TestResult {
256    /// assert!(!OpenPgpUserId::new("🤡".to_string())?.is_email());
257    ///
258    /// assert!(OpenPgpUserId::new("🤡 <foo@xn--rl8h.org>".to_string())?.is_email());
259    /// # Ok(())
260    /// # }
261    /// ```
262    pub fn is_email(&self) -> bool {
263        matches!(self.0, OpenPgpUserIdType::Email(..))
264    }
265}
266
267impl AsRef<str> for OpenPgpUserId {
268    fn as_ref(&self) -> &str {
269        match self.0.borrow() {
270            OpenPgpUserIdType::Email(user_id) => user_id.as_str(),
271            OpenPgpUserIdType::Plain(user_id) => user_id.as_str(),
272        }
273    }
274}
275
276impl Display for OpenPgpUserId {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        write!(f, "{}", self.as_ref())
279    }
280}
281
282impl FromStr for OpenPgpUserId {
283    type Err = Error;
284
285    fn from_str(s: &str) -> Result<Self, Self::Err> {
286        Self::new(s.to_string())
287    }
288}
289
290impl From<OpenPgpUserId> for String {
291    fn from(value: OpenPgpUserId) -> Self {
292        value.to_string()
293    }
294}
295
296impl TryFrom<&SignedUser> for OpenPgpUserId {
297    type Error = Error;
298
299    /// Creates an [`OpenPgpUserId`] from [`SignedUser`].
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if the [`SignedUser`]'s User ID can not be converted to a valid UTF-8
304    /// string.
305    fn try_from(value: &SignedUser) -> Result<Self, Self::Error> {
306        Self::new(
307            String::from_utf8(value.id.id().to_vec()).map_err(|source| Error::FromUtf8 {
308                context: "converting an OpenPGP UserID",
309                source,
310            })?,
311        )
312    }
313}
314
315impl TryFrom<String> for OpenPgpUserId {
316    type Error = Error;
317
318    fn try_from(value: String) -> Result<Self, Self::Error> {
319        Self::new(value)
320    }
321}
322
323/// A list of [`OpenPgpUserId`]
324///
325/// The items of the list are guaranteed to be unique.
326#[derive(Clone, Debug, serde::Deserialize, Eq, Hash, PartialEq, serde::Serialize)]
327#[serde(into = "Vec<String>", try_from = "Vec<String>")]
328pub struct OpenPgpUserIdList(Vec<OpenPgpUserId>);
329
330impl OpenPgpUserIdList {
331    /// Creates a new [`OpenPgpUserIdList`]
332    ///
333    /// # Errors
334    ///
335    /// Returns an error, if one of the provided [`OpenPgpUserId`]s is a duplicate.
336    ///
337    /// # Examples
338    ///
339    /// ```
340    /// use signstar_crypto::openpgp::OpenPgpUserIdList;
341    ///
342    /// # fn main() -> testresult::TestResult {
343    /// OpenPgpUserIdList::new(vec![
344    ///     "🤡 <foo@xn--rl8h.org>".parse()?,
345    ///     "🤡 <bar@xn--rl8h.org>".parse()?,
346    /// ])?;
347    ///
348    /// // this fails because the two OpenPgpUserIds are the same
349    /// assert!(
350    ///     OpenPgpUserIdList::new(vec![
351    ///         "🤡 <foo@xn--rl8h.org>".parse()?,
352    ///         "🤡 <foo@xn--rl8h.org>".parse()?,
353    ///     ])
354    ///     .is_err()
355    /// );
356    /// # Ok(())
357    /// # }
358    /// ```
359    pub fn new(user_ids: Vec<OpenPgpUserId>) -> Result<Self, Error> {
360        let mut set = HashSet::new();
361        for user_id in user_ids.iter() {
362            if !set.insert(user_id) {
363                return Err(Error::DuplicateUserId {
364                    user_id: user_id.to_owned(),
365                });
366            }
367        }
368        Ok(Self(user_ids))
369    }
370
371    /// Iterator for OpenPGP User IDs contained in this list.
372    pub fn iter(&self) -> impl Iterator<Item = &OpenPgpUserId> {
373        self.0.iter()
374    }
375
376    /// Returns a reference to the first [`OpenPgpUserId`] if there is one.
377    pub fn first(&self) -> Option<&OpenPgpUserId> {
378        self.0.first()
379    }
380}
381
382impl AsRef<[OpenPgpUserId]> for OpenPgpUserIdList {
383    fn as_ref(&self) -> &[OpenPgpUserId] {
384        &self.0
385    }
386}
387
388impl From<OpenPgpUserIdList> for Vec<String> {
389    fn from(value: OpenPgpUserIdList) -> Self {
390        value
391            .iter()
392            .map(|user_id| user_id.to_string())
393            .collect::<Vec<String>>()
394    }
395}
396
397impl TryFrom<Vec<String>> for OpenPgpUserIdList {
398    type Error = Error;
399
400    fn try_from(value: Vec<String>) -> Result<Self, Self::Error> {
401        let user_ids = {
402            let mut user_ids: Vec<OpenPgpUserId> = vec![];
403            for user_id in value {
404                user_ids.push(OpenPgpUserId::new(user_id)?)
405            }
406            user_ids
407        };
408        OpenPgpUserIdList::new(user_ids)
409    }
410}
411
412/// Key usage flags that can be set on the generated certificate.
413#[derive(Debug, Default)]
414pub struct OpenPgpKeyUsageFlags(KeyFlags);
415
416impl OpenPgpKeyUsageFlags {
417    /// Makes it possible for this key to issue data signatures.
418    pub fn set_sign(&mut self) {
419        self.0.set_sign(true);
420    }
421
422    /// Makes it impossible for this key to issue data signatures.
423    pub fn clear_sign(&mut self) {
424        self.0.set_sign(false);
425    }
426}
427
428impl AsRef<KeyFlags> for OpenPgpKeyUsageFlags {
429    fn as_ref(&self) -> &KeyFlags {
430        &self.0
431    }
432}
433
434impl From<OpenPgpKeyUsageFlags> for KeyFlags {
435    fn from(value: OpenPgpKeyUsageFlags) -> Self {
436        value.0
437    }
438}