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