-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpublic_key.rs
More file actions
398 lines (349 loc) · 14.8 KB
/
Copy pathpublic_key.rs
File metadata and controls
398 lines (349 loc) · 14.8 KB
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
//! SSH public key representation and validation
//!
//! This module provides the `SshPublicKey` type for handling SSH public key values
//! with proper validation and serialization support.
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;
use thiserror::Error;
/// Errors that can occur when working with SSH public keys
#[derive(Error, Debug, Clone)]
pub enum SshPublicKeyError {
#[error("SSH public key cannot be empty")]
Empty,
#[error("SSH public key format is invalid: {0}")]
InvalidFormat(String),
}
/// SSH public key representation using the newtype pattern
///
/// This type wraps a string containing a valid SSH public key and provides
/// validation to ensure the key follows basic SSH public key format requirements.
///
/// # Example
///
/// ```rust
/// use torrust_tracker_deployer_lib::adapters::ssh::SshPublicKey;
///
/// let key_str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCw16sai+XVnawp/P/Q23kcXKekygZ6ALmQAyslREo6kbG8s5RScsmbQqOQEcIwnV2Vo88eeWVzX0N0H1dIczRa/ezijBEsGefthzmz9Ix/vM4lodzTPQFtW8c2eYw7ESy12/2x5//UQQ3mxawEWsz5Ri8XuyBEy/Xh7xH/KpoektaocIOt2/WdCe8CvZdMLd7AviGcTdHFWRiOVrmHM1Pd8znqeA3/1KQP/M4Ae5q21oPjchGjVfPkGh/e62Wt+Wo/2lT30AyMO7JHA1tB1W4xANRQkOd1Kb/TrDLXfg0PaHQ+Irmycjp/H4KkcdB06nzYawXMN5csd/5TWKwkb9/vofp6GQNP731U8+JR4cxRfD107KoHroDSJpG2Fanb2PVBkSXAiJl29YrtoP9vUtSIemQCD/aXFtTcpSv7Y16bdp7v+0adCEHwBmodm9GzLL808FpI2ZCzCi+Ae98P3z+yPCxbrnVAahU8AM2NSbrfyH1w2eb4hJ22oPjdd//tBYtkE1TZBw+i3n0vRn04s5BfPRwwj5GISxacTOZm/YWvoE4UU9axtFXOtMUniVKL3ycA+LEfK7C4velOKbluyL8fYYu4pUxHnYOOkYYeRoi2jf3oagbABOpznloPd93wYP3NoUpIdtMZW+iCF0NnZkVLC9lm1FbTcnmrfNzFtGVKCQ== testing@torrust-testing-infra";
/// let public_key = SshPublicKey::new(key_str).unwrap();
/// println!("{}", public_key.as_str());
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct SshPublicKey(String);
impl SshPublicKey {
/// Creates a new `SshPublicKey` from a string
///
/// # Arguments
/// * `key` - The SSH public key string
///
/// # Errors
/// Returns an error if the key is empty or has an invalid format
pub fn new<S: Into<String>>(key: S) -> Result<Self, SshPublicKeyError> {
let key = key.into();
if key.trim().is_empty() {
return Err(SshPublicKeyError::Empty);
}
// Basic SSH public key format validation
// SSH public keys typically start with the key type (ssh-rsa, ssh-ed25519, etc.)
let trimmed = key.trim();
if !Self::is_valid_format(trimmed) {
return Err(SshPublicKeyError::InvalidFormat(
"SSH public key must start with a valid key type (ssh-rsa, ssh-dss, ssh-ed25519, ssh-ed448, rsa-sha2-256, rsa-sha2-512, ecdsa-sha2-*, etc.)".to_string()
));
}
Ok(Self(trimmed.to_string()))
}
/// Basic format validation for SSH public keys
///
/// Checks if the key starts with a recognized SSH key type and has the basic structure
/// Based on IANA SSH Parameters registry: <https://www.iana.org/assignments/ssh-parameters/ssh-parameters.xhtml#ssh-parameters-19>
///
/// **Note for maintainers**: When new SSH key types are added to the IANA registry,
/// update the `valid_prefixes` array below to include them. Always check the official
/// IANA SSH Parameters document for the most current list of registered key types.
fn is_valid_format(key: &str) -> bool {
let valid_prefixes = [
// Standard SSH key types
"ssh-rsa",
"ssh-dss",
"ssh-ed25519",
"ssh-ed448",
// RSA with specific hash algorithms
"rsa-sha2-256",
"rsa-sha2-512",
// ECDSA variants
"ssh-ecdsa",
"ecdsa-sha2-nistp256",
"ecdsa-sha2-nistp384",
"ecdsa-sha2-nistp521",
// SPKI signatures
"spki-sign-rsa",
"spki-sign-dss",
// PGP signatures
"pgp-sign-rsa",
"pgp-sign-dss",
// X.509 certificate types
"x509v3-ssh-dss",
"x509v3-ssh-rsa",
"x509v3-rsa2048-sha256",
// Note: x509v3-ecdsa-sha2-* handled by prefix matching below
// Note: ecdsa-sha2-* handled by prefix matching below
// Null key for testing
"null",
];
// Check if the key starts with a valid prefix
let has_valid_prefix = valid_prefixes.iter().any(|prefix| key.starts_with(prefix));
if has_valid_prefix {
// Basic structure check: should have at least 2 space-separated parts for most keys
// Format: <type> <key-data> [comment]
let parts: Vec<&str> = key.split_whitespace().collect();
return parts.len() >= 2 || key.starts_with("null"); // null key might be standalone
}
// Check for wildcard patterns not covered by exact prefixes
let wildcard_patterns = [
"ecdsa-sha2-", // Matches ecdsa-sha2-* variants
"x509v3-ecdsa-sha2-", // Matches x509v3-ecdsa-sha2-* variants
];
for pattern in &wildcard_patterns {
if key.starts_with(pattern) {
let parts: Vec<&str> = key.split_whitespace().collect();
return parts.len() >= 2;
}
}
false
}
/// Returns the SSH public key as a string slice
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
/// Consumes the `SshPublicKey` and returns the inner string
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl FromStr for SshPublicKey {
type Err = SshPublicKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
impl fmt::Display for SshPublicKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<SshPublicKey> for String {
fn from(key: SshPublicKey) -> String {
key.0
}
}
impl AsRef<str> for SshPublicKey {
fn as_ref(&self) -> &str {
&self.0
}
}
#[cfg(test)]
mod tests {
use super::*;
const VALID_RSA_KEY: &str = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCw16sai+XVnawp/P/Q23kcXKekygZ6ALmQAyslREo6kbG8s5RScsmbQqOQEcIwnV2Vo88eeWVzX0N0H1dIczRa/ezijBEsGefthzmz9Ix/vM4lodzTPQFtW8c2eYw7ESy12/2x5//UQQ3mxawEWsz5Ri8XuyBEy/Xh7xH/KpoektaocIOt2/WdCe8CvZdMLd7AviGcTdHFWRiOVrmHM1Pd8znqeA3/1KQP/M4Ae5q21oPjchGjVfPkGh/e62Wt+Wo/2lT30AyMO7JHA1tB1W4xANRQkOd1Kb/TrDLXfg0PaHQ+Irmycjp/H4KkcdB06nzYawXMN5csd/5TWKwkb9/vofp6GQNP731U8+JR4cxRfD107KoHroDSJpG2Fanb2PVBkSXAiJl29YrtoP9vUtSIemQCD/aXFtTcpSv7Y16bdp7v+0adCEHwBmodm9GzLL808FpI2ZCzCi+Ae98P3z+yPCxbrnVAahU8AM2NSbrfyH1w2eb4hJ22oPjdd//tBYtkE1TZBw+i3n0vRn04s5BfPRwwj5GISxacTOZm/YWvoE4UU9axtFXOtMUniVKL3ycA+LEfK7C4velOKbluyL8fYYu4pUxHnYOOkYYeRoi2jf3oagbABOpznloPd93wYP3NoUpIdtMZW+iCF0NnZkVLC9lm1FbTcnmrfNzFtGVKCQ== testing@torrust-testing-infra";
const VALID_ED25519_KEY: &str = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG4rT3vTt99Ox5kndS4HmgTrKBT8tOajsHpzHtRG testing@example.com";
const VALID_RSA_SHA2_256_KEY: &str =
"rsa-sha2-256 AAAAB3NzaC1yc2EAAAADAQABAAABAQC7vbqajnc testing@example.com";
const VALID_ECDSA_KEY: &str =
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTY testing@example.com";
#[test]
fn it_should_create_ssh_public_key_with_valid_rsa_key() {
let key = SshPublicKey::new(VALID_RSA_KEY).unwrap();
assert_eq!(key.as_str(), VALID_RSA_KEY);
}
#[test]
fn it_should_create_ssh_public_key_with_valid_ed25519_key() {
let key = SshPublicKey::new(VALID_ED25519_KEY).unwrap();
assert_eq!(key.as_str(), VALID_ED25519_KEY);
}
#[test]
fn it_should_create_ssh_public_key_with_rsa_sha2_256_key() {
let key = SshPublicKey::new(VALID_RSA_SHA2_256_KEY).unwrap();
assert_eq!(key.as_str(), VALID_RSA_SHA2_256_KEY);
}
#[test]
fn it_should_create_ssh_public_key_with_ecdsa_key() {
let key = SshPublicKey::new(VALID_ECDSA_KEY).unwrap();
assert_eq!(key.as_str(), VALID_ECDSA_KEY);
}
/// Parameterized test for all supported SSH key prefixes
///
/// This test validates that all SSH key types defined in the IANA SSH Parameters registry
/// are properly recognized by our validation logic. When new key types are added to the
/// registry, add them to this test data to ensure they're supported.
#[test]
fn it_should_support_all_iana_registered_ssh_key_types() {
// Test data: (prefix, description)
let supported_prefixes = [
// Standard SSH key types
("ssh-rsa", "RSA keys"),
("ssh-dss", "DSS/DSA keys"),
("ssh-ed25519", "Ed25519 keys"),
("ssh-ed448", "Ed448 keys"),
// RSA with specific hash algorithms
("rsa-sha2-256", "RSA with SHA-256"),
("rsa-sha2-512", "RSA with SHA-512"),
// ECDSA variants
("ssh-ecdsa", "ECDSA keys (generic)"),
("ecdsa-sha2-nistp256", "ECDSA P-256"),
("ecdsa-sha2-nistp384", "ECDSA P-384"),
("ecdsa-sha2-nistp521", "ECDSA P-521"),
// SPKI signatures
("spki-sign-rsa", "SPKI RSA signatures"),
("spki-sign-dss", "SPKI DSS signatures"),
// PGP signatures
("pgp-sign-rsa", "PGP RSA signatures"),
("pgp-sign-dss", "PGP DSS signatures"),
// X.509 certificate types
("x509v3-ssh-dss", "X.509v3 DSS certificates"),
("x509v3-ssh-rsa", "X.509v3 RSA certificates"),
("x509v3-rsa2048-sha256", "X.509v3 RSA 2048 SHA-256"),
// Null key for testing
("null", "Null key type"),
];
for (prefix, description) in supported_prefixes {
let test_key = if prefix == "null" {
// Null key might be standalone
prefix.to_string()
} else {
// Standard format: <type> <key-data> [comment]
format!("{prefix} AAAAB3NzaC1example_key_data test@example.com")
};
let result = SshPublicKey::new(&test_key);
assert!(
result.is_ok(),
"Failed to validate {description} with prefix '{prefix}': {test_key}"
);
let key = result.unwrap();
assert_eq!(key.as_str(), test_key);
}
}
/// Test wildcard ECDSA variants that use pattern matching
#[test]
fn it_should_support_wildcard_ecdsa_variants() {
let wildcard_variants = [
("ecdsa-sha2-custom", "Custom ECDSA SHA-2 variant"),
("ecdsa-sha2-nistp192", "ECDSA P-192"),
("x509v3-ecdsa-sha2-nistp256", "X.509v3 ECDSA P-256"),
("x509v3-ecdsa-sha2-custom", "X.509v3 ECDSA custom variant"),
];
for (prefix, description) in wildcard_variants {
let test_key = format!("{prefix} AAAAB3NzaC1example_key_data test@example.com");
let result = SshPublicKey::new(&test_key);
assert!(
result.is_ok(),
"Failed to validate {description} with prefix '{prefix}': {test_key}"
);
let key = result.unwrap();
assert_eq!(key.as_str(), test_key);
}
}
#[test]
fn it_should_support_ssh_dss_keys() {
let dss_key = "ssh-dss AAAAB3NzaC1kc3MAAACBAIr9... test@example.com";
let key = SshPublicKey::new(dss_key).unwrap();
assert_eq!(key.as_str(), dss_key);
}
#[test]
fn it_should_support_ssh_ed448_keys() {
let ed448_key = "ssh-ed448 AAAAGnNzaC1lZDQ0OAAAANLamVx1... test@example.com";
let key = SshPublicKey::new(ed448_key).unwrap();
assert_eq!(key.as_str(), ed448_key);
}
#[test]
fn it_should_support_rsa_sha2_512_keys() {
let rsa_sha2_512_key =
"rsa-sha2-512 AAAAB3NzaC1yc2EAAAADAQABAAABAQC7vbqajnc test@example.com";
let key = SshPublicKey::new(rsa_sha2_512_key).unwrap();
assert_eq!(key.as_str(), rsa_sha2_512_key);
}
#[test]
fn it_should_support_x509v3_keys() {
let x509_key = "x509v3-ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC7vbqajnc test@example.com";
let key = SshPublicKey::new(x509_key).unwrap();
assert_eq!(key.as_str(), x509_key);
}
#[test]
fn it_should_fail_with_empty_key() {
let result = SshPublicKey::new("");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SshPublicKeyError::Empty));
}
#[test]
fn it_should_fail_with_whitespace_only_key() {
let result = SshPublicKey::new(" \n \t ");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), SshPublicKeyError::Empty));
}
#[test]
fn it_should_fail_with_invalid_format() {
let result = SshPublicKey::new("invalid-key-format");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SshPublicKeyError::InvalidFormat(_)
));
}
#[test]
fn it_should_fail_with_incomplete_key() {
let result = SshPublicKey::new("ssh-rsa");
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
SshPublicKeyError::InvalidFormat(_)
));
}
#[test]
fn it_should_trim_whitespace() {
let key_with_whitespace = format!(" \n{VALID_RSA_KEY}\n ");
let key = SshPublicKey::new(key_with_whitespace).unwrap();
assert_eq!(key.as_str(), VALID_RSA_KEY);
}
#[test]
fn it_should_convert_from_str() {
let key: SshPublicKey = VALID_RSA_KEY.parse().unwrap();
assert_eq!(key.as_str(), VALID_RSA_KEY);
}
#[test]
fn it_should_display_correctly() {
let key = SshPublicKey::new(VALID_RSA_KEY).unwrap();
assert_eq!(format!("{key}"), VALID_RSA_KEY);
}
#[test]
fn it_should_convert_to_string() {
let key = SshPublicKey::new(VALID_RSA_KEY).unwrap();
let string_key: String = key.into();
assert_eq!(string_key, VALID_RSA_KEY);
}
#[test]
fn it_should_serialize_to_json() {
let key = SshPublicKey::new(VALID_RSA_KEY).unwrap();
let json = serde_json::to_string(&key).unwrap();
assert_eq!(json, format!("\"{VALID_RSA_KEY}\""));
}
#[test]
fn it_should_deserialize_from_json() {
let json = format!("\"{VALID_RSA_KEY}\"");
let key: SshPublicKey = serde_json::from_str(&json).unwrap();
assert_eq!(key.as_str(), VALID_RSA_KEY);
}
#[test]
fn it_should_be_equal_when_same_key() {
let key1 = SshPublicKey::new(VALID_RSA_KEY).unwrap();
let key2 = SshPublicKey::new(VALID_RSA_KEY).unwrap();
assert_eq!(key1, key2);
}
#[test]
fn it_should_work_as_reference() {
let key = SshPublicKey::new(VALID_RSA_KEY).unwrap();
let key_ref: &str = key.as_ref();
assert_eq!(key_ref, VALID_RSA_KEY);
}
}