Skip to content

Commit 8de9a6c

Browse files
authored
fixed #316 - correctly parse OpenSSH keys generated by PuTTYgen (#321)
1 parent 9764c7a commit 8de9a6c

File tree

4 files changed

+52
-4
lines changed

4 files changed

+52
-4
lines changed

ssh-key/src/private.rs

+25-3
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ const DEFAULT_RSA_KEY_SIZE: usize = 4096;
176176
const MAX_BLOCK_SIZE: usize = 16;
177177

178178
/// Padding bytes to use.
179-
const PADDING_BYTES: [u8; MAX_BLOCK_SIZE - 1] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
179+
const PADDING_BYTES: [u8; MAX_BLOCK_SIZE] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
180180

181181
/// Unix file permissions for SSH private keys.
182182
#[cfg(all(unix, feature = "std"))]
@@ -377,10 +377,12 @@ impl PrivateKey {
377377
let mut buffer = Zeroizing::new(ciphertext.to_vec());
378378
self.cipher.decrypt(&key, &iv, &mut buffer, self.auth_tag)?;
379379

380+
#[allow(clippy::arithmetic_side_effects)] // block sizes are constants
380381
Self::decode_privatekey_comment_pair(
381382
&mut &**buffer,
382383
self.public_key.key_data.clone(),
383384
self.cipher.block_size(),
385+
self.cipher.block_size() - 1,
384386
)
385387
}
386388

@@ -571,8 +573,10 @@ impl PrivateKey {
571573
reader: &mut impl Reader,
572574
public_key: public::KeyData,
573575
block_size: usize,
576+
max_padding_size: usize,
574577
) -> Result<Self> {
575578
debug_assert!(block_size <= MAX_BLOCK_SIZE);
579+
debug_assert!(max_padding_size <= MAX_BLOCK_SIZE);
576580

577581
// Ensure input data is padding-aligned
578582
if reader.remaining_len().checked_rem(block_size) != Some(0) {
@@ -598,7 +602,7 @@ impl PrivateKey {
598602

599603
let padding_len = reader.remaining_len();
600604

601-
if padding_len >= block_size {
605+
if padding_len > max_padding_size {
602606
return Err(encoding::Error::Length.into());
603607
}
604608

@@ -756,7 +760,25 @@ impl Decode for PrivateKey {
756760
}
757761

758762
reader.read_prefixed(|reader| {
759-
Self::decode_privatekey_comment_pair(reader, public_key, cipher.block_size())
763+
// PuTTYgen uses a non-standard block size of 16
764+
// and _always_ adds a padding even if data length
765+
// is divisible by 16 - for unencrypted keys
766+
// in the OpenSSH format.
767+
// We're only relaxing the exact length check, but will
768+
// still validate that the contents of the padding area.
769+
// In all other cases there can be up to (but not including)
770+
// `block_size` padding bytes as per `PROTOCOL.key`.
771+
let max_padding_size = match cipher {
772+
Cipher::None => 16,
773+
#[allow(clippy::arithmetic_side_effects)] // block sizes are constants
774+
_ => cipher.block_size() - 1,
775+
};
776+
Self::decode_privatekey_comment_pair(
777+
reader,
778+
public_key,
779+
cipher.block_size(),
780+
max_padding_size,
781+
)
760782
})
761783
}
762784
}

ssh-key/tests/dot_ssh.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ fn path_round_trip() {
2020
#[test]
2121
fn private_keys() {
2222
let dot_ssh = dot_ssh();
23-
assert_eq!(dot_ssh.private_keys().unwrap().count(), 20);
23+
assert_eq!(dot_ssh.private_keys().unwrap().count(), 21);
2424
}
2525

2626
#[test]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtz
3+
c2gtZWQyNTUxOQAAACA+af4QkC9p+OQHgC8EQ1xT+4Ykkf0SYPmEF85tb57WMwAA
4+
ALBRB2JGUQdiRgAAAAtzc2gtZWQyNTUxOQAAACA+af4QkC9p+OQHgC8EQ1xT+4Yk
5+
kf0SYPmEF85tb57WMwAAAEBGxdSjfrbFQ17/N6WcP1EmN6ymf3qRR3NGSGh6zCtm
6+
JD5p/hCQL2n45AeALwRDXFP7hiSR/RJg+YQXzm1vntYzAAAAHWVkZHNhLWtleS0y
7+
MDI0MTIyN2ExMjM0NTY3ODkwAQIDBAUGBwgJCgsMDQ4PEA==
8+
-----END OPENSSH PRIVATE KEY-----

ssh-key/tests/private_key.rs

+18
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,11 @@ const OPENSSH_OPAQUE_EXAMPLE: &str = include_str!("examples/id_opaque");
9494
#[cfg(feature = "p384")]
9595
const OPENSSH_PADLESS_WONDER_EXAMPLE: &str = include_str!("examples/padless_wonder");
9696

97+
/// OpenSSH-formatted private key generated by PuTTYgen that showcases its
98+
/// incorrect 16-byte "block size"
99+
#[cfg(feature = "ed25519")]
100+
const OPENSSH_OVERPADDED_PUTTYGEN_EXAMPLE: &str = include_str!("examples/puttygen_overpadded");
101+
97102
/// Get a path into the `tests/scratch` directory.
98103
#[cfg(feature = "std")]
99104
pub fn scratch_path(filename: &str) -> PathBuf {
@@ -229,6 +234,19 @@ fn decode_padless_wonder_openssh() {
229234
assert_eq!("", key.comment());
230235
}
231236

237+
#[cfg(feature = "ed25519")]
238+
#[test]
239+
fn decode_overpadded_puttygen_openssh() {
240+
let key = PrivateKey::from_openssh(OPENSSH_OVERPADDED_PUTTYGEN_EXAMPLE).unwrap();
241+
assert_eq!(Algorithm::Ed25519, key.algorithm());
242+
assert_eq!(Cipher::None, key.cipher());
243+
assert_eq!(KdfAlg::None, key.kdf().algorithm());
244+
assert!(key.kdf().is_none());
245+
246+
#[cfg(feature = "alloc")]
247+
assert_eq!("eddsa-key-20241227a1234567890", key.comment());
248+
}
249+
232250
#[cfg(feature = "p384")]
233251
#[test]
234252
fn decode_ecdsa_p384_openssh() {

0 commit comments

Comments
 (0)