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