ssh_key/algorithm/
name.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
use alloc::string::String;
use core::str::{self, FromStr};
use encoding::LabelError;

/// The suffix added to the `name` in a `name@domainname` algorithm string identifier.
const CERT_STR_SUFFIX: &str = "-cert-v01";

/// According to [RFC4251 § 6], algorithm names are ASCII strings that are at most 64
/// characters long.
///
/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
const MAX_ALGORITHM_NAME_LEN: usize = 64;

/// The maximum length of the certificate string identifier is [`MAX_ALGORITHM_NAME_LEN`] +
/// `"-cert-v01".len()` (the certificate identifier is obtained by inserting `"-cert-v01"` in the
/// algorithm name).
const MAX_CERT_STR_LEN: usize = MAX_ALGORITHM_NAME_LEN + CERT_STR_SUFFIX.len();

/// A string representing an additional algorithm name in the `name@domainname` format (see
/// [RFC4251 § 6]).
///
/// Additional algorithm names must be non-empty printable ASCII strings no longer than 64
/// characters.
///
/// This also provides a `name-cert-v01@domainnname` string identifier for the corresponding
/// OpenSSH certificate format, derived from the specified `name@domainname` string.
///
/// NOTE: RFC4251 specifies additional validation criteria for algorithm names, but we do not
/// implement all of them here.
///
/// [RFC4251 § 6]: https://www.rfc-editor.org/rfc/rfc4251.html#section-6
#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub struct AlgorithmName {
    /// The string identifier which corresponds to this algorithm.
    id: String,
}

impl AlgorithmName {
    /// Create a new algorithm identifier.
    pub fn new(id: impl Into<String>) -> Result<Self, LabelError> {
        let id = id.into();
        validate_algorithm_id(&id, MAX_ALGORITHM_NAME_LEN)?;
        split_algorithm_id(&id)?;
        Ok(Self { id })
    }

    /// Get the string identifier which corresponds to this algorithm name.
    pub fn as_str(&self) -> &str {
        &self.id
    }

    /// Get the string identifier which corresponds to the OpenSSH certificate format.
    pub fn certificate_type(&self) -> String {
        let (name, domain) = split_algorithm_id(&self.id).expect("format checked in constructor");
        format!("{name}{CERT_STR_SUFFIX}@{domain}")
    }

    /// Create a new [`AlgorithmName`] from an OpenSSH certificate format string identifier.
    pub fn from_certificate_type(id: &str) -> Result<Self, LabelError> {
        validate_algorithm_id(id, MAX_CERT_STR_LEN)?;

        // Derive the algorithm name from the certificate format string identifier:
        let (name, domain) = split_algorithm_id(id)?;
        let name = name
            .strip_suffix(CERT_STR_SUFFIX)
            .ok_or_else(|| LabelError::new(id))?;

        let algorithm_name = format!("{name}@{domain}");

        Ok(Self { id: algorithm_name })
    }
}

impl FromStr for AlgorithmName {
    type Err = LabelError;

    fn from_str(id: &str) -> Result<Self, LabelError> {
        Self::new(id)
    }
}

/// Check if the length of `id` is at most `n`, and that `id` only consists of ASCII characters.
fn validate_algorithm_id(id: &str, n: usize) -> Result<(), LabelError> {
    if id.len() > n || !id.is_ascii() {
        return Err(LabelError::new(id));
    }

    Ok(())
}

/// Split a `name@domainname` algorithm string identifier into `(name, domainname)`.
fn split_algorithm_id(id: &str) -> Result<(&str, &str), LabelError> {
    let (name, domain) = id.split_once('@').ok_or_else(|| LabelError::new(id))?;

    // TODO: validate name and domain_name according to the criteria from RFC4251
    if name.is_empty() || domain.is_empty() || domain.contains('@') {
        return Err(LabelError::new(id));
    }

    Ok((name, domain))
}