Skip to content

Commit 7a414e2

Browse files
committed
feat: add SshPublicKey newtype with comprehensive IANA-compliant validation
- Create SshPublicKey newtype in src/shared/ssh with validation and serialization - Replace String type in CloudInitContext with SshPublicKey for type safety - Add comprehensive validation for all IANA SSH Parameters registry key types - Implement parameterized tests for all supported SSH key prefixes - Add maintainer documentation with link to IANA source of truth - Support wildcard patterns for ecdsa-sha2-* and x509v3-ecdsa-sha2-* variants - Ensure backward compatibility with existing SSH key formats
1 parent 29c8bbd commit 7a414e2

File tree

5 files changed

+444
-19
lines changed

5 files changed

+444
-19
lines changed

project-words.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
AAAAB
12
architecting
23
autorestart
34
buildx
@@ -12,6 +13,7 @@ debootstrap
1213
distutils
1314
Dockerfiles
1415
dtolnay
16+
EAAAADAQABAAABAQC
1517
ehthumbs
1618
elif
1719
endfor
@@ -29,18 +31,21 @@ larstobi
2931
logfile
3032
loglevel
3133
lxdbr
34+
MAAACBA
3235
maxbytes
3336
mgmt
3437
MVVM
3538
myenv
3639
newgrp
3740
newtype
41+
nistp
3842
noconfirm
3943
nodaemon
4044
noninteractive
4145
NOPASSWD
4246
nslookup
4347
nullglob
48+
OAAAAN
4449
oneline
4550
pacman
4651
parameterizing
@@ -66,6 +71,7 @@ serverurl
6671
shellcheck
6772
Silverlight
6873
smorimoto
74+
spki
6975
sshpass
7076
startretries
7177
subshell
@@ -91,6 +97,7 @@ tmpfs
9197
usermod
9298
usize
9399
utmp
100+
vbqajnc
94101
writeln
95102
значение
96103
ключ

src/infrastructure/template/wrappers/tofu/lxd/cloud_init/context.rs

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::fs;
88
use std::path::Path;
99
use thiserror::Error;
1010

11+
use crate::shared::ssh::SshPublicKey;
1112
use crate::shared::Username;
1213

1314
/// Errors that can occur when creating a `CloudInitContext`
@@ -30,24 +31,31 @@ pub enum CloudInitContextError {
3031
#[derive(Debug, Clone, Serialize)]
3132
pub struct CloudInitContext {
3233
/// SSH public key content to be injected into cloud-init configuration
33-
pub ssh_public_key: String,
34+
pub ssh_public_key: SshPublicKey,
3435
/// Username to be created in the cloud-init configuration
3536
pub username: Username,
3637
}
3738

3839
/// Builder for `CloudInitContext` with fluent interface
3940
#[derive(Debug, Default)]
4041
pub struct CloudInitContextBuilder {
41-
ssh_public_key: Option<String>,
42+
ssh_public_key: Option<SshPublicKey>,
4243
username: Option<Username>,
4344
}
4445

4546
impl CloudInitContextBuilder {
4647
/// Set the SSH public key content directly
47-
#[must_use]
48-
pub fn with_ssh_public_key(mut self, ssh_public_key: String) -> Self {
49-
self.ssh_public_key = Some(ssh_public_key);
50-
self
48+
///
49+
/// # Errors
50+
/// Returns an error if the SSH public key is invalid
51+
pub fn with_ssh_public_key<S: Into<String>>(
52+
mut self,
53+
ssh_public_key: S,
54+
) -> Result<Self, CloudInitContextError> {
55+
let key = SshPublicKey::new(ssh_public_key)
56+
.map_err(|e| CloudInitContextError::SshPublicKeyReadError(e.to_string()))?;
57+
self.ssh_public_key = Some(key);
58+
Ok(self)
5159
}
5260

5361
/// Set the username for the cloud-init configuration
@@ -67,7 +75,7 @@ impl CloudInitContextBuilder {
6775
/// Set the SSH public key by reading from a file path
6876
///
6977
/// # Errors
70-
/// Returns an error if the file cannot be read
78+
/// Returns an error if the file cannot be read or the SSH public key is invalid
7179
pub fn with_ssh_public_key_from_file<P: AsRef<Path>>(
7280
mut self,
7381
ssh_public_key_path: P,
@@ -80,8 +88,10 @@ impl CloudInitContextBuilder {
8088
))
8189
})?;
8290

83-
// Trim any trailing newlines or whitespace from the SSH key
84-
self.ssh_public_key = Some(content.trim().to_string());
91+
// Trim any trailing newlines or whitespace from the SSH key and create SshPublicKey
92+
let key = SshPublicKey::new(content.trim())
93+
.map_err(|e| CloudInitContextError::SshPublicKeyReadError(e.to_string()))?;
94+
self.ssh_public_key = Some(key);
8595
Ok(self)
8696
}
8797

@@ -110,14 +120,17 @@ impl CloudInitContext {
110120
///
111121
/// # Errors
112122
/// Returns an error if the username is invalid according to Linux naming requirements
123+
/// or if the SSH public key is invalid
113124
pub fn new<S: Into<String>>(
114-
ssh_public_key: String,
125+
ssh_public_key: S,
115126
username: S,
116127
) -> Result<Self, CloudInitContextError> {
128+
let key = SshPublicKey::new(ssh_public_key)
129+
.map_err(|e| CloudInitContextError::SshPublicKeyReadError(e.to_string()))?;
117130
let username = Username::new(username)
118131
.map_err(|e| CloudInitContextError::InvalidUsername(e.to_string()))?;
119132
Ok(Self {
120-
ssh_public_key,
133+
ssh_public_key: key,
121134
username,
122135
})
123136
}
@@ -131,7 +144,7 @@ impl CloudInitContext {
131144
/// Get the SSH public key content
132145
#[must_use]
133146
pub fn ssh_public_key(&self) -> &str {
134-
&self.ssh_public_key
147+
self.ssh_public_key.as_str()
135148
}
136149

137150
/// Get the username
@@ -151,7 +164,7 @@ mod tests {
151164
fn it_should_create_cloud_init_context_with_ssh_key() {
152165
let ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]";
153166
let username = "testuser";
154-
let context = CloudInitContext::new(ssh_key.to_string(), username).unwrap();
167+
let context = CloudInitContext::new(ssh_key, username).unwrap();
155168

156169
assert_eq!(context.ssh_public_key(), ssh_key);
157170
assert_eq!(context.username(), username);
@@ -162,7 +175,8 @@ mod tests {
162175
let ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]";
163176
let username = "testuser";
164177
let context = CloudInitContext::builder()
165-
.with_ssh_public_key(ssh_key.to_string())
178+
.with_ssh_public_key(ssh_key)
179+
.unwrap()
166180
.with_username(username)
167181
.unwrap()
168182
.build()
@@ -209,7 +223,8 @@ mod tests {
209223
fn it_should_fail_when_username_is_missing() {
210224
let ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]";
211225
let result = CloudInitContext::builder()
212-
.with_ssh_public_key(ssh_key.to_string())
226+
.with_ssh_public_key(ssh_key)
227+
.unwrap()
213228
.build();
214229

215230
assert!(result.is_err());
@@ -235,7 +250,7 @@ mod tests {
235250
fn it_should_serialize_to_json() {
236251
let ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]";
237252
let username = "testuser";
238-
let context = CloudInitContext::new(ssh_key.to_string(), username).unwrap();
253+
let context = CloudInitContext::new(ssh_key, username).unwrap();
239254

240255
let json = serde_json::to_value(&context).unwrap();
241256
assert_eq!(json["ssh_public_key"], ssh_key);
@@ -247,7 +262,7 @@ mod tests {
247262
let ssh_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]";
248263
let invalid_username = "123invalid"; // starts with digit
249264

250-
let result = CloudInitContext::new(ssh_key.to_string(), invalid_username);
265+
let result = CloudInitContext::new(ssh_key, invalid_username);
251266
assert!(result.is_err());
252267
assert!(matches!(
253268
result.unwrap_err(),
@@ -261,7 +276,8 @@ mod tests {
261276
let invalid_username = "@invalid"; // contains @ symbol
262277

263278
let result = CloudInitContext::builder()
264-
.with_ssh_public_key(ssh_key.to_string())
279+
.with_ssh_public_key(ssh_key)
280+
.unwrap()
265281
.with_username(invalid_username);
266282

267283
assert!(result.is_err());

src/infrastructure/template/wrappers/tofu/lxd/cloud_init/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@ mod tests {
7474
/// Helper function to create a `CloudInitContext` with given SSH key
7575
fn create_cloud_init_context(ssh_key: &str) -> CloudInitContext {
7676
CloudInitContext::builder()
77-
.with_ssh_public_key(ssh_key.to_string())
77+
.with_ssh_public_key(ssh_key)
78+
.unwrap()
7879
.with_username("testuser")
7980
.unwrap()
8081
.build()

src/shared/ssh/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//! - `client` - SSH client implementation for remote command execution
1010
//! - `connection` - SSH connection configuration and management
1111
//! - `credentials` - SSH authentication credentials and key management
12+
//! - `public_key` - SSH public key representation and validation
1213
//! - `service_checker` - SSH service availability testing without authentication
1314
//!
1415
//! ## Key Features
@@ -25,11 +26,13 @@
2526
pub mod client;
2627
pub mod connection;
2728
pub mod credentials;
29+
pub mod public_key;
2830
pub mod service_checker;
2931

3032
pub use client::SshClient;
3133
pub use connection::SshConnection;
3234
pub use credentials::SshCredentials;
35+
pub use public_key::SshPublicKey;
3336
pub use service_checker::SshServiceChecker;
3437

3538
use thiserror::Error;

0 commit comments

Comments
 (0)