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}