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