Skip to main content

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