1use ed25519;
2use pem_rfc7468::PemLabel;
3use pkcs8::{DecodePrivateKey, PrivateKeyInfo, SecretDocument, der::Decode, pkcs5};
4use ssh_key::{
5 Algorithm,
6 private::{Ed25519Keypair, RsaKeypair},
7 sec1,
8};
9
10use crate::{SshKeyData, error::SshKeyImportError, ssh_private_key_to_data};
11
12pub fn import_key(
21 encoded_key: String,
22 password: Option<String>,
23) -> Result<SshKeyData, SshKeyImportError> {
24 let label = pem_rfc7468::decode_label(encoded_key.as_bytes())
25 .map_err(|_| SshKeyImportError::Parsing)?;
26
27 match label {
28 pkcs8::PrivateKeyInfo::<(), (), ()>::PEM_LABEL => import_pkcs8_key(encoded_key, None),
29 pkcs8::EncryptedPrivateKeyInfo::<()>::PEM_LABEL => import_pkcs8_key(
30 encoded_key,
31 Some(password.ok_or(SshKeyImportError::PasswordRequired)?),
32 ),
33 ssh_key::PrivateKey::PEM_LABEL => import_openssh_key(encoded_key, password),
34 _ => Err(SshKeyImportError::UnsupportedKeyType),
35 }
36}
37
38fn import_pkcs8_key(
39 encoded_key: String,
40 password: Option<String>,
41) -> Result<SshKeyData, SshKeyImportError> {
42 match parse_pkcs8_pem(&encoded_key, password.as_deref()) {
43 Err(SshKeyImportError::Parsing) => {
47 let rewrapped = rewrap_pem(&encoded_key).ok_or(SshKeyImportError::Parsing)?;
48 parse_pkcs8_pem(&rewrapped, password.as_deref())
49 }
50 result => result,
51 }
52}
53
54fn parse_pkcs8_pem(
55 encoded_key: &str,
56 password: Option<&str>,
57) -> Result<SshKeyData, SshKeyImportError> {
58 let doc = if let Some(password) = password {
59 SecretDocument::from_pkcs8_encrypted_pem(encoded_key, password.as_bytes()).map_err(
60 |err| match err {
61 pkcs8::Error::EncryptedPrivateKey(pkcs5::Error::DecryptFailed) => {
62 SshKeyImportError::WrongPassword
63 }
64 _ => SshKeyImportError::Parsing,
65 },
66 )?
67 } else {
68 SecretDocument::from_pkcs8_pem(encoded_key).map_err(|_| SshKeyImportError::Parsing)?
69 };
70
71 import_pkcs8_der_key(doc.as_bytes())
72}
73
74fn rewrap_pem(pem: &str) -> Option<String> {
80 let mut lines = pem.lines();
81
82 let header = lines
83 .by_ref()
84 .find(|line| line.starts_with("-----BEGIN "))?;
85
86 let mut body = String::new();
88 let mut footer = None;
89 for line in lines.by_ref() {
90 if line.starts_with("-----END ") {
91 footer = Some(line);
92 break;
93 }
94 body.extend(line.split_whitespace());
95 }
96 let footer = footer?;
97
98 let mut out = String::with_capacity(body.len() + body.len() / 64 + header.len() + 16);
99 out.push_str(header);
100 out.push('\n');
101 let mut chars = body.chars();
103 loop {
104 let chunk: String = chars.by_ref().take(64).collect();
105 if chunk.is_empty() {
106 break;
107 }
108 out.push_str(&chunk);
109 out.push('\n');
110 }
111 out.push_str(footer);
112 out.push('\n');
113
114 Some(out)
115}
116
117pub fn import_pkcs8_der_key(encoded_key: &[u8]) -> Result<SshKeyData, SshKeyImportError> {
120 let private_key_info =
121 PrivateKeyInfo::from_der(encoded_key).map_err(|_| SshKeyImportError::Parsing)?;
122
123 let private_key = match private_key_info.algorithm.oid {
124 ed25519::pkcs8::ALGORITHM_OID => {
125 let private_key: ed25519::KeypairBytes = private_key_info
126 .try_into()
127 .map_err(|_| SshKeyImportError::Parsing)?;
128
129 ssh_key::private::PrivateKey::from(Ed25519Keypair::from(&private_key.secret_key.into()))
130 }
131 rsa::pkcs1::ALGORITHM_OID => {
132 let private_key: rsa::RsaPrivateKey = private_key_info
133 .try_into()
134 .map_err(|_| SshKeyImportError::Parsing)?;
135
136 ssh_key::private::PrivateKey::from(
137 RsaKeypair::try_from(private_key).map_err(|_| SshKeyImportError::Parsing)?,
138 )
139 }
140 sec1::ALGORITHM_OID => import_ecdsa_pkcs8_der(encoded_key)?,
141 _ => return Err(SshKeyImportError::UnsupportedKeyType),
142 };
143
144 ssh_private_key_to_data(private_key).map_err(|_| SshKeyImportError::Parsing)
145}
146
147fn import_openssh_key(
148 encoded_key: String,
149 password: Option<String>,
150) -> Result<SshKeyData, SshKeyImportError> {
151 let private_key =
152 ssh_key::private::PrivateKey::from_openssh(&encoded_key).map_err(|err| match err {
153 ssh_key::Error::AlgorithmUnknown | ssh_key::Error::AlgorithmUnsupported { .. } => {
154 SshKeyImportError::UnsupportedKeyType
155 }
156 _ => SshKeyImportError::Parsing,
157 })?;
158
159 if !is_supported_algorithm(&private_key.algorithm()) {
160 return Err(SshKeyImportError::UnsupportedKeyType);
161 }
162
163 let private_key = if private_key.is_encrypted() {
164 let password = password.ok_or(SshKeyImportError::PasswordRequired)?;
165 private_key
166 .decrypt(password.as_bytes())
167 .map_err(|_| SshKeyImportError::WrongPassword)?
168 } else {
169 private_key
170 };
171
172 ssh_private_key_to_data(private_key).map_err(|_| SshKeyImportError::Parsing)
173}
174
175fn is_supported_algorithm(algorithm: &Algorithm) -> bool {
177 matches!(
178 algorithm,
179 Algorithm::Ed25519 | Algorithm::Rsa { .. } | Algorithm::Ecdsa { .. }
180 )
181}
182
183fn import_ecdsa_pkcs8_der(encoded_key: &[u8]) -> Result<ssh_key::PrivateKey, SshKeyImportError> {
184 use pkcs8::DecodePrivateKey as _;
185
186 if let Ok(sk) = p256::SecretKey::from_pkcs8_der(encoded_key) {
187 let public_key = sk.public_key();
188 let keypair = ssh_key::private::EcdsaKeypair::NistP256 {
189 public: public_key.into(),
190 private: ssh_key::private::EcdsaPrivateKey::from(sk),
191 };
192 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
193 .map_err(|_| SshKeyImportError::Parsing);
194 }
195 if let Ok(sk) = p384::SecretKey::from_pkcs8_der(encoded_key) {
196 let public_key = sk.public_key();
197 let keypair = ssh_key::private::EcdsaKeypair::NistP384 {
198 public: public_key.into(),
199 private: ssh_key::private::EcdsaPrivateKey::from(sk),
200 };
201 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
202 .map_err(|_| SshKeyImportError::Parsing);
203 }
204 if let Ok(sk) = p521::SecretKey::from_pkcs8_der(encoded_key) {
205 let public_key = sk.public_key();
206 let keypair = ssh_key::private::EcdsaKeypair::NistP521 {
207 public: public_key.into(),
208 private: ssh_key::private::EcdsaPrivateKey::from(sk),
209 };
210 return ssh_key::PrivateKey::new(ssh_key::private::KeypairData::Ecdsa(keypair), "")
211 .map_err(|_| SshKeyImportError::Parsing);
212 }
213 Err(SshKeyImportError::UnsupportedKeyType)
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn import_key_ed25519_openssh_unencrypted() {
222 let private_key = include_str!("../resources/import/ed25519_openssh_unencrypted");
223 let public_key = include_str!("../resources/import/ed25519_openssh_unencrypted.pub").trim();
224 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
225 assert_eq!(result.public_key, public_key);
226 }
227
228 #[test]
229 fn import_key_ed25519_openssh_encrypted() {
230 let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
231 let public_key = include_str!("../resources/import/ed25519_openssh_encrypted.pub").trim();
232 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
233 assert_eq!(result.public_key, public_key);
234 }
235
236 #[test]
237 fn import_key_rsa_openssh_unencrypted() {
238 let private_key = include_str!("../resources/import/rsa_openssh_unencrypted");
239 let public_key = include_str!("../resources/import/rsa_openssh_unencrypted.pub").trim();
240 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
241 assert_eq!(result.public_key, public_key);
242 }
243
244 #[test]
245 fn import_key_rsa_openssh_encrypted() {
246 let private_key = include_str!("../resources/import/rsa_openssh_encrypted");
247 let public_key = include_str!("../resources/import/rsa_openssh_encrypted.pub").trim();
248 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
249 assert_eq!(result.public_key, public_key);
250 }
251
252 #[test]
253 fn import_key_ed25519_pkcs8_unencrypted() {
254 let private_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted");
255 let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub")
256 .replace("testkey", "");
257 let public_key = public_key.trim();
258 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
259 assert_eq!(result.public_key, public_key);
260 }
261
262 #[test]
263 fn import_key_rsa_pkcs8_unencrypted() {
264 let private_key = include_str!("../resources/import/rsa_pkcs8_unencrypted");
265 let public_key =
267 include_str!("../resources/import/rsa_pkcs8_unencrypted.pub").replace("testkey", "");
268 let public_key = public_key.trim();
269 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
270 assert_eq!(result.public_key, public_key);
271 }
272
273 #[test]
274 fn import_key_rsa_pkcs8_encrypted() {
275 let private_key = include_str!("../resources/import/rsa_pkcs8_encrypted");
276 let public_key =
277 include_str!("../resources/import/rsa_pkcs8_encrypted.pub").replace("testkey", "");
278 let public_key = public_key.trim();
279 let result = import_key(private_key.to_string(), Some("password".to_string())).unwrap();
280 assert_eq!(result.public_key, public_key);
281 }
282
283 #[test]
284 fn import_key_ed25519_openssh_encrypted_wrong_password() {
285 let private_key = include_str!("../resources/import/ed25519_openssh_encrypted");
286 let result = import_key(private_key.to_string(), Some("wrongpassword".to_string()));
287 assert_eq!(result.unwrap_err(), SshKeyImportError::WrongPassword);
288 }
289
290 #[test]
295 fn import_key_ed25519_pkcs8_unencrypted_single_line() {
296 let private_key =
299 include_str!("../resources/import/ed25519_pkcs8_1password_single_line_unencrypted");
300 let public_key = include_str!("../resources/import/ed25519_pkcs8_unencrypted.pub").trim();
301
302 let result = import_key(private_key.to_string(), None).unwrap();
303 assert_eq!(result.public_key, public_key);
304 }
305
306 #[test]
307 fn import_non_key_error() {
308 let result = import_key("not a key".to_string(), Some("".to_string()));
309 assert_eq!(result.unwrap_err(), SshKeyImportError::Parsing);
310 }
311
312 #[test]
313 fn import_wrong_label_error() {
314 let private_key = include_str!("../resources/import/wrong_label");
315 let result = import_key(private_key.to_string(), Some("".to_string()));
316 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
317 }
318
319 #[test]
320 fn import_ecdsa_p256_openssh_unencrypted() {
321 let private_key = include_str!("../resources/import/ecdsa_openssh_unencrypted");
322 let public_key = include_str!("../resources/import/ecdsa_openssh_unencrypted.pub").trim();
323 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
324 assert_eq!(result.public_key, public_key);
325 }
326
327 #[test]
328 fn import_ecdsa_p384_openssh_unencrypted() {
329 let private_key = include_str!("../resources/import/ecdsa_p384_openssh_unencrypted");
330 let public_key =
331 include_str!("../resources/import/ecdsa_p384_openssh_unencrypted.pub").trim();
332 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
333 assert_eq!(result.public_key, public_key);
334 }
335
336 #[test]
337 fn import_ecdsa_p521_openssh_unencrypted() {
338 let private_key = include_str!("../resources/import/ecdsa_p521_openssh_unencrypted");
339 let public_key =
340 include_str!("../resources/import/ecdsa_p521_openssh_unencrypted.pub").trim();
341 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
342 assert_eq!(result.public_key, public_key);
343 }
344
345 #[test]
346 fn import_key_ed25519_putty() {
347 let private_key = include_str!("../resources/import/ed25519_putty_openssh_unencrypted");
348 let public_key =
349 include_str!("../resources/import/ed25519_putty_openssh_unencrypted.pub").trim();
350 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
351 assert_eq!(result.public_key, public_key);
352 }
353
354 #[test]
355 fn import_key_rsa_openssh_putty() {
356 let private_key = include_str!("../resources/import/rsa_putty_openssh_unencrypted");
357 let public_key =
358 include_str!("../resources/import/rsa_putty_openssh_unencrypted.pub").trim();
359 let result = import_key(private_key.to_string(), Some("".to_string())).unwrap();
360 assert_eq!(result.public_key, public_key);
361 }
362
363 #[test]
364 fn import_key_rsa_pkcs8_putty() {
365 let private_key = include_str!("../resources/import/rsa_putty_pkcs1_unencrypted");
366 let result = import_key(private_key.to_string(), Some("".to_string()));
367 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
368 }
369
370 #[test]
371 fn import_ed25519_key_regression_17028() {
372 let private_key = include_str!("../resources/import/ed25519_regression_17028");
374 let public_key = include_str!("../resources/import/ed25519_regression_17028.pub").trim();
375 let result = import_key(private_key.to_string(), None).unwrap();
376 assert_eq!(result.public_key, public_key);
377 }
378
379 #[test]
380 fn import_sk_ed25519_openssh_unencrypted_is_unsupported() {
381 let private_key = include_str!("../resources/import/sk_ed25519_openssh_unencrypted");
382 let result = import_key(private_key.to_string(), None);
383 assert_eq!(result.unwrap_err(), SshKeyImportError::UnsupportedKeyType);
384 }
385
386 #[test]
387 fn supported_algorithms_are_accepted() {
388 for algorithm in [
389 Algorithm::Ed25519,
390 Algorithm::Rsa { hash: None },
391 Algorithm::Ecdsa {
392 curve: ssh_key::EcdsaCurve::NistP256,
393 },
394 Algorithm::Ecdsa {
395 curve: ssh_key::EcdsaCurve::NistP384,
396 },
397 Algorithm::Ecdsa {
398 curve: ssh_key::EcdsaCurve::NistP521,
399 },
400 ] {
401 assert!(
402 is_supported_algorithm(&algorithm),
403 "{algorithm:?} should be supported"
404 );
405 }
406 }
407
408 #[test]
409 fn unsupported_algorithms_are_rejected() {
410 for algorithm in [
411 Algorithm::Dsa,
412 Algorithm::SkEd25519,
413 Algorithm::SkEcdsaSha2NistP256,
414 Algorithm::new("[email protected]").expect("unknown names map to Algorithm::Other"),
415 ] {
416 assert!(
417 !is_supported_algorithm(&algorithm),
418 "{algorithm:?} should not be supported"
419 );
420 }
421 }
422}