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}