1use crate::signing::{KeyPair, PublicKey};
29use base64::{Engine, engine::general_purpose::STANDARD};
30use serde::{Deserialize, Serialize};
31use std::fmt;
32use thiserror::Error;
33
34#[derive(Debug, Error, Clone, PartialEq, Eq)]
36pub enum SshKeyError {
37 #[error("Invalid SSH key format: {0}")]
39 InvalidFormat(String),
40
41 #[error("Invalid key length: expected {expected}, got {actual}")]
43 InvalidLength { expected: usize, actual: usize },
44
45 #[error("Unsupported algorithm: {0}")]
47 UnsupportedAlgorithm(String),
48
49 #[error("Base64 decode error: {0}")]
51 Base64Error(String),
52
53 #[error("Unexpected end of data")]
55 UnexpectedEof,
56
57 #[error("UTF-8 decode error: {0}")]
59 Utf8Error(String),
60
61 #[error("Invalid secret key")]
63 InvalidSecretKey,
64}
65
66pub type SshKeyResult<T> = Result<T, SshKeyError>;
68
69const SSH_ED25519_KEY_TYPE: &str = "ssh-ed25519";
70const OPENSSH_PRIVATE_KEY_HEADER: &str = "-----BEGIN OPENSSH PRIVATE KEY-----";
71const OPENSSH_PRIVATE_KEY_FOOTER: &str = "-----END OPENSSH PRIVATE KEY-----";
72const OPENSSH_AUTH_MAGIC: &[u8] = b"openssh-key-v1\0";
73
74#[derive(Clone, Debug, Serialize, Deserialize)]
76pub struct SshPublicKey {
77 pub algorithm: String,
79 pub key_data: Vec<u8>,
81 pub comment: Option<String>,
83}
84
85impl SshPublicKey {
86 pub fn from_ed25519(public_key: &PublicKey) -> Self {
88 Self {
89 algorithm: SSH_ED25519_KEY_TYPE.to_string(),
90 key_data: public_key.to_vec(),
91 comment: None,
92 }
93 }
94
95 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
97 self.comment = Some(comment.into());
98 self
99 }
100
101 pub fn to_openssh_format(&self) -> Vec<u8> {
105 let mut buf = Vec::new();
106
107 write_string(&mut buf, &self.algorithm);
109
110 write_bytes(&mut buf, &self.key_data);
112
113 buf
114 }
115
116 pub fn to_string_with_comment(&self, comment: &str) -> String {
118 let encoded = STANDARD.encode(self.to_openssh_format());
119 format!("{} {} {}", self.algorithm, encoded, comment)
120 }
121
122 pub fn to_string_no_comment(&self) -> String {
124 let encoded = STANDARD.encode(self.to_openssh_format());
125 format!("{} {}", self.algorithm, encoded)
126 }
127
128 pub fn parse(s: &str) -> SshKeyResult<Self> {
132 let parts: Vec<&str> = s.split_whitespace().collect();
133
134 if parts.len() < 2 {
135 return Err(SshKeyError::InvalidFormat(
136 "Invalid SSH public key format".to_string(),
137 ));
138 }
139
140 let algorithm = parts[0].to_string();
141 let encoded = parts[1];
142 let comment = if parts.len() > 2 {
143 Some(parts[2..].join(" "))
144 } else {
145 None
146 };
147
148 let data = STANDARD
150 .decode(encoded)
151 .map_err(|e| SshKeyError::Base64Error(e.to_string()))?;
152
153 let mut offset = 0;
155 let parsed_algo = read_string(&data, &mut offset)?;
156
157 if parsed_algo != algorithm {
158 return Err(SshKeyError::InvalidFormat(format!(
159 "Algorithm mismatch: {} vs {}",
160 parsed_algo, algorithm
161 )));
162 }
163
164 let key_data = read_bytes(&data, &mut offset)?;
165
166 if algorithm == SSH_ED25519_KEY_TYPE && key_data.len() != 32 {
167 return Err(SshKeyError::InvalidLength {
168 expected: 32,
169 actual: key_data.len(),
170 });
171 }
172
173 Ok(Self {
174 algorithm,
175 key_data,
176 comment,
177 })
178 }
179
180 pub fn to_ed25519(&self) -> SshKeyResult<PublicKey> {
182 if self.algorithm != SSH_ED25519_KEY_TYPE {
183 return Err(SshKeyError::UnsupportedAlgorithm(self.algorithm.clone()));
184 }
185
186 if self.key_data.len() != 32 {
187 return Err(SshKeyError::InvalidLength {
188 expected: 32,
189 actual: self.key_data.len(),
190 });
191 }
192
193 let mut bytes = [0u8; 32];
194 bytes.copy_from_slice(&self.key_data);
195 Ok(bytes)
196 }
197}
198
199impl fmt::Display for SshPublicKey {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 if let Some(comment) = &self.comment {
202 write!(f, "{}", self.to_string_with_comment(comment))
203 } else {
204 write!(f, "{}", self.to_string_no_comment())
205 }
206 }
207}
208
209#[derive(Clone, Serialize, Deserialize)]
211pub struct SshPrivateKey {
212 pub public_key: Vec<u8>,
214 pub private_key: Vec<u8>,
216 pub comment: Option<String>,
218}
219
220impl SshPrivateKey {
221 pub fn from_ed25519(keypair: &KeyPair) -> Self {
223 let secret = keypair.secret_key();
225 let public = keypair.public_key();
226 let mut private_key = Vec::with_capacity(64);
227 private_key.extend_from_slice(&secret);
228 private_key.extend_from_slice(&public);
229
230 Self {
231 public_key: public.to_vec(),
232 private_key,
233 comment: None,
234 }
235 }
236
237 pub fn with_comment(mut self, comment: impl Into<String>) -> Self {
239 self.comment = Some(comment.into());
240 self
241 }
242
243 pub fn to_openssh_format(&self) -> Vec<u8> {
245 let mut buf = Vec::new();
246
247 buf.extend_from_slice(OPENSSH_AUTH_MAGIC);
249
250 write_string(&mut buf, "none");
252
253 write_string(&mut buf, "none");
255
256 write_string(&mut buf, "");
258
259 buf.extend_from_slice(&1u32.to_be_bytes());
261
262 let mut public_section = Vec::new();
264 write_string(&mut public_section, SSH_ED25519_KEY_TYPE);
265 write_bytes(&mut public_section, &self.public_key);
266 write_bytes(&mut buf, &public_section);
267
268 let mut private_section = Vec::new();
270
271 let check = rand::random::<u32>();
273 private_section.extend_from_slice(&check.to_be_bytes());
274 private_section.extend_from_slice(&check.to_be_bytes());
275
276 write_string(&mut private_section, SSH_ED25519_KEY_TYPE);
278
279 write_bytes(&mut private_section, &self.public_key);
281
282 write_bytes(&mut private_section, &self.private_key);
284
285 write_string(&mut private_section, self.comment.as_deref().unwrap_or(""));
287
288 let padding_len = 8 - (private_section.len() % 8);
290 for i in 1..=padding_len {
291 private_section.push(i as u8);
292 }
293
294 write_bytes(&mut buf, &private_section);
295
296 buf
297 }
298
299 pub fn to_pem(&self) -> String {
301 let data = self.to_openssh_format();
302 let encoded = STANDARD.encode(&data);
303
304 let mut result = String::new();
306 result.push_str(OPENSSH_PRIVATE_KEY_HEADER);
307 result.push('\n');
308
309 for chunk in encoded.as_bytes().chunks(70) {
310 result.push_str(std::str::from_utf8(chunk).unwrap());
311 result.push('\n');
312 }
313
314 result.push_str(OPENSSH_PRIVATE_KEY_FOOTER);
315 result.push('\n');
316
317 result
318 }
319
320 pub fn from_pem(pem: &str) -> SshKeyResult<Self> {
322 let pem = pem
324 .lines()
325 .filter(|line| {
326 !line.contains("BEGIN OPENSSH PRIVATE KEY")
327 && !line.contains("END OPENSSH PRIVATE KEY")
328 })
329 .collect::<String>();
330
331 let data = STANDARD
332 .decode(pem.trim())
333 .map_err(|e| SshKeyError::Base64Error(e.to_string()))?;
334
335 Self::parse_binary(&data)
336 }
337
338 pub fn parse_binary(data: &[u8]) -> SshKeyResult<Self> {
340 let mut offset = 0;
341
342 if data.len() < OPENSSH_AUTH_MAGIC.len()
344 || &data[..OPENSSH_AUTH_MAGIC.len()] != OPENSSH_AUTH_MAGIC
345 {
346 return Err(SshKeyError::InvalidFormat(
347 "Invalid OpenSSH private key magic".to_string(),
348 ));
349 }
350 offset += OPENSSH_AUTH_MAGIC.len();
351
352 let cipher = read_string(data, &mut offset)?;
354 if cipher != "none" {
355 return Err(SshKeyError::InvalidFormat(
356 "Encrypted SSH keys not supported yet".to_string(),
357 ));
358 }
359
360 let _kdf = read_string(data, &mut offset)?;
362
363 let _kdf_options = read_bytes(data, &mut offset)?;
365
366 let num_keys = read_u32(data, &mut offset)?;
368 if num_keys != 1 {
369 return Err(SshKeyError::InvalidFormat(format!(
370 "Expected 1 key, found {}",
371 num_keys
372 )));
373 }
374
375 let _public_section = read_bytes(data, &mut offset)?;
377
378 let private_section = read_bytes(data, &mut offset)?;
380 let mut priv_offset = 0;
381
382 let check1 = read_u32(&private_section, &mut priv_offset)?;
384 let check2 = read_u32(&private_section, &mut priv_offset)?;
385 if check1 != check2 {
386 return Err(SshKeyError::InvalidFormat(
387 "Checksum mismatch (possibly encrypted)".to_string(),
388 ));
389 }
390
391 let algorithm = read_string(&private_section, &mut priv_offset)?;
393 if algorithm != SSH_ED25519_KEY_TYPE {
394 return Err(SshKeyError::UnsupportedAlgorithm(algorithm));
395 }
396
397 let public_key = read_bytes(&private_section, &mut priv_offset)?;
399
400 let private_key = read_bytes(&private_section, &mut priv_offset)?;
402
403 let comment = read_string(&private_section, &mut priv_offset)?;
405 let comment = if comment.is_empty() {
406 None
407 } else {
408 Some(comment)
409 };
410
411 Ok(Self {
412 public_key,
413 private_key,
414 comment,
415 })
416 }
417
418 pub fn to_ed25519(&self) -> SshKeyResult<KeyPair> {
420 if self.private_key.len() != 64 {
421 return Err(SshKeyError::InvalidLength {
422 expected: 64,
423 actual: self.private_key.len(),
424 });
425 }
426
427 let mut secret = [0u8; 32];
429 secret.copy_from_slice(&self.private_key[..32]);
430
431 KeyPair::from_secret_key(&secret).map_err(|_| SshKeyError::InvalidSecretKey)
432 }
433}
434
435fn write_string(buf: &mut Vec<u8>, s: &str) {
438 let bytes = s.as_bytes();
439 buf.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
440 buf.extend_from_slice(bytes);
441}
442
443fn write_bytes(buf: &mut Vec<u8>, data: &[u8]) {
444 buf.extend_from_slice(&(data.len() as u32).to_be_bytes());
445 buf.extend_from_slice(data);
446}
447
448fn read_u32(data: &[u8], offset: &mut usize) -> SshKeyResult<u32> {
449 if *offset + 4 > data.len() {
450 return Err(SshKeyError::UnexpectedEof);
451 }
452
453 let mut bytes = [0u8; 4];
454 bytes.copy_from_slice(&data[*offset..*offset + 4]);
455 *offset += 4;
456
457 Ok(u32::from_be_bytes(bytes))
458}
459
460fn read_string(data: &[u8], offset: &mut usize) -> SshKeyResult<String> {
461 let bytes = read_bytes(data, offset)?;
462 String::from_utf8(bytes).map_err(|e| SshKeyError::Utf8Error(e.to_string()))
463}
464
465fn read_bytes(data: &[u8], offset: &mut usize) -> SshKeyResult<Vec<u8>> {
466 let len = read_u32(data, offset)? as usize;
467
468 if *offset + len > data.len() {
469 return Err(SshKeyError::UnexpectedEof);
470 }
471
472 let bytes = data[*offset..*offset + len].to_vec();
473 *offset += len;
474
475 Ok(bytes)
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_ssh_public_key_roundtrip() {
484 let keypair = KeyPair::generate();
485 let ssh_pub = SshPublicKey::from_ed25519(&keypair.public_key());
486
487 let formatted = ssh_pub.to_string_with_comment("test@host");
488 let parsed = SshPublicKey::parse(&formatted).unwrap();
489
490 assert_eq!(parsed.algorithm, SSH_ED25519_KEY_TYPE);
491 assert_eq!(parsed.key_data, ssh_pub.key_data);
492 assert_eq!(parsed.comment, Some("test@host".to_string()));
493 }
494
495 #[test]
496 fn test_ssh_public_key_no_comment() {
497 let keypair = KeyPair::generate();
498 let ssh_pub = SshPublicKey::from_ed25519(&keypair.public_key());
499
500 let formatted = ssh_pub.to_string_no_comment();
501 let parsed = SshPublicKey::parse(&formatted).unwrap();
502
503 assert_eq!(parsed.key_data, ssh_pub.key_data);
504 assert_eq!(parsed.comment, None);
505 }
506
507 #[test]
508 fn test_ssh_public_key_to_ed25519() {
509 let keypair = KeyPair::generate();
510 let ssh_pub = SshPublicKey::from_ed25519(&keypair.public_key());
511
512 let ed25519_pub = ssh_pub.to_ed25519().unwrap();
513 assert_eq!(&ed25519_pub, &keypair.public_key());
514 }
515
516 #[test]
517 fn test_ssh_private_key_pem_roundtrip() {
518 let keypair = KeyPair::generate();
519 let ssh_priv = SshPrivateKey::from_ed25519(&keypair).with_comment("test@host");
520
521 let pem = ssh_priv.to_pem();
522 assert!(pem.contains(OPENSSH_PRIVATE_KEY_HEADER));
523 assert!(pem.contains(OPENSSH_PRIVATE_KEY_FOOTER));
524
525 let parsed = SshPrivateKey::from_pem(&pem).unwrap();
526 assert_eq!(parsed.public_key, ssh_priv.public_key);
527 assert_eq!(parsed.private_key, ssh_priv.private_key);
528 assert_eq!(parsed.comment, ssh_priv.comment);
529 }
530
531 #[test]
532 fn test_ssh_private_key_to_ed25519() {
533 let keypair = KeyPair::generate();
534 let ssh_priv = SshPrivateKey::from_ed25519(&keypair);
535
536 let recovered = ssh_priv.to_ed25519().unwrap();
537 assert_eq!(&recovered.public_key(), &keypair.public_key());
538 assert_eq!(recovered.secret_key(), keypair.secret_key());
539 }
540
541 #[test]
542 fn test_ssh_keys_compatibility() {
543 let keypair = KeyPair::generate();
544
545 let ssh_pub = SshPublicKey::from_ed25519(&keypair.public_key());
547 let ssh_priv = SshPrivateKey::from_ed25519(&keypair);
548
549 assert_eq!(ssh_pub.key_data, ssh_priv.public_key);
551
552 let recovered = ssh_priv.to_ed25519().unwrap();
554 assert_eq!(&recovered.public_key(), &keypair.public_key());
555 }
556
557 #[test]
558 fn test_invalid_ssh_public_key() {
559 assert!(SshPublicKey::parse("invalid").is_err());
560 assert!(SshPublicKey::parse("ssh-ed25519").is_err());
561 assert!(SshPublicKey::parse("ssh-ed25519 !!!invalid-base64!!!").is_err());
562 }
563
564 #[test]
565 fn test_ssh_public_key_with_multiword_comment() {
566 let keypair = KeyPair::generate();
567 let ssh_pub = SshPublicKey::from_ed25519(&keypair.public_key());
568
569 let formatted = ssh_pub.to_string_with_comment("user@host with spaces");
570 let parsed = SshPublicKey::parse(&formatted).unwrap();
571
572 assert_eq!(parsed.comment, Some("user@host with spaces".to_string()));
573 }
574
575 #[test]
576 fn test_openssh_format_structure() {
577 let keypair = KeyPair::generate();
578 let ssh_priv = SshPrivateKey::from_ed25519(&keypair);
579
580 let binary = ssh_priv.to_openssh_format();
581
582 assert_eq!(&binary[..OPENSSH_AUTH_MAGIC.len()], OPENSSH_AUTH_MAGIC);
584 }
585
586 #[test]
587 fn test_write_read_string() {
588 let mut buf = Vec::new();
589 write_string(&mut buf, "test");
590
591 let mut offset = 0;
592 let s = read_string(&buf, &mut offset).unwrap();
593 assert_eq!(s, "test");
594 assert_eq!(offset, buf.len());
595 }
596
597 #[test]
598 fn test_write_read_bytes() {
599 let mut buf = Vec::new();
600 let data = vec![1, 2, 3, 4, 5];
601 write_bytes(&mut buf, &data);
602
603 let mut offset = 0;
604 let read = read_bytes(&buf, &mut offset).unwrap();
605 assert_eq!(read, data);
606 assert_eq!(offset, buf.len());
607 }
608
609 #[test]
610 fn test_pem_line_wrapping() {
611 let keypair = KeyPair::generate();
612 let ssh_priv = SshPrivateKey::from_ed25519(&keypair);
613 let pem = ssh_priv.to_pem();
614
615 for line in pem.lines() {
617 if !line.contains("BEGIN") && !line.contains("END") {
618 assert!(line.len() <= 70, "Line too long: {}", line.len());
619 }
620 }
621 }
622
623 #[test]
624 fn test_serialization() {
625 let keypair = KeyPair::generate();
626 let ssh_pub = SshPublicKey::from_ed25519(&keypair.public_key()).with_comment("test@host");
627
628 let serialized = crate::codec::encode(&ssh_pub).unwrap();
629 let deserialized: SshPublicKey = crate::codec::decode(&serialized).unwrap();
630
631 assert_eq!(deserialized.algorithm, ssh_pub.algorithm);
632 assert_eq!(deserialized.key_data, ssh_pub.key_data);
633 assert_eq!(deserialized.comment, ssh_pub.comment);
634 }
635}