Skip to main content

ssh_key/algorithm/
name.rs

1use alloc::string::String;
2use core::str::{self, FromStr};
3use encoding::LabelError;
4
5/// The suffix added to the `name` in a `name@domainname` algorithm string identifier.
6const CERT_STR_SUFFIX: &str = "-cert-v01";
7
8/// According to [RFC4251 § 6], algorithm names are ASCII strings that are at most 64
9/// characters long.
10///
11/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
12const MAX_ALGORITHM_NAME_LEN: usize = 64;
13
14/// The maximum length of the certificate string identifier is [`MAX_ALGORITHM_NAME_LEN`] +
15/// `"-cert-v01".len()` (the certificate identifier is obtained by inserting `"-cert-v01"` in the
16/// algorithm name).
17const MAX_CERT_STR_LEN: usize = MAX_ALGORITHM_NAME_LEN + CERT_STR_SUFFIX.len();
18
19/// A string representing an additional algorithm name in the `name@domainname` format (see
20/// [RFC4251 § 6]).
21///
22/// Additional algorithm names must be non-empty printable ASCII strings no longer than 64
23/// characters.
24///
25/// This also provides a `name-cert-v01@domainnname` string identifier for the corresponding
26/// OpenSSH certificate format, derived from the specified `name@domainname` string.
27///
28/// NOTE: RFC4251 specifies additional validation criteria for algorithm names, but we do not
29/// implement all of them here.
30///
31/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
32#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
33pub struct AlgorithmName {
34    /// The string identifier which corresponds to this algorithm.
35    id: String,
36}
37
38impl AlgorithmName {
39    /// Create a new algorithm identifier.
40    ///
41    /// # Errors
42    /// Returns [`LabelError`] in the event the identifier is invalid.
43    pub fn new(id: impl Into<String>) -> Result<Self, LabelError> {
44        let id = id.into();
45        validate_algorithm_id(&id, MAX_ALGORITHM_NAME_LEN)?;
46        split_algorithm_id(&id)?;
47        Ok(Self { id })
48    }
49
50    /// Get the string identifier which corresponds to this algorithm name.
51    #[must_use]
52    pub fn as_str(&self) -> &str {
53        &self.id
54    }
55
56    /// Get the string identifier which corresponds to the OpenSSH certificate format.
57    #[must_use]
58    #[allow(clippy::missing_panics_doc, reason = "should not panic")]
59    pub fn certificate_type(&self) -> String {
60        let (name, domain) = split_algorithm_id(&self.id).expect("format checked in constructor");
61        format!("{name}{CERT_STR_SUFFIX}@{domain}")
62    }
63
64    /// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier.
65    ///
66    /// # Errors
67    /// Returns [`LabelError`] in the event the identifier is invalid.
68    pub fn from_certificate_type(id: &str) -> Result<Self, LabelError> {
69        validate_algorithm_id(id, MAX_CERT_STR_LEN)?;
70
71        // Derive the algorithm name from the certificate format string identifier:
72        let (name, domain) = split_algorithm_id(id)?;
73        let name = name
74            .strip_suffix(CERT_STR_SUFFIX)
75            .ok_or_else(|| LabelError::new(id))?;
76
77        Ok(Self {
78            id: format!("{name}@{domain}"),
79        })
80    }
81}
82
83impl FromStr for AlgorithmName {
84    type Err = LabelError;
85
86    fn from_str(id: &str) -> Result<Self, LabelError> {
87        Self::new(id)
88    }
89}
90
91/// Check if the length of `id` is at most `n`, and that `id` only consists of ASCII characters.
92fn validate_algorithm_id(id: &str, n: usize) -> Result<(), LabelError> {
93    if id.len() > n || !id.is_ascii() {
94        return Err(LabelError::new(id));
95    }
96
97    Ok(())
98}
99
100/// Split a `name@domainname` algorithm string identifier into `(name, domainname)`.
101fn split_algorithm_id(id: &str) -> Result<(&str, &str), LabelError> {
102    let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?;
103
104    // TODO: validate name and domain_name according to the criteria from RFC4251
105    if name.is_empty() || domain.is_empty() || domain.contains('@') {
106        return Err(LabelError::new(id));
107    }
108
109    Ok((name, domain))
110}