Skip to content

Commit ddf937b

Browse files
committed
fixed #316 - correctly parse OpenSSH keys generated by PuTTYgen
1 parent 52ed3e0 commit ddf937b

4 files changed

Lines changed: 52 additions & 4 deletions

File tree

ssh-key/src/private.rs

Lines changed: 25 additions & 3 deletions
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"))]
@@ -354,10 +354,12 @@ impl PrivateKey {
354354
let mut buffer = Zeroizing::new(ciphertext.to_vec());
355355
self.cipher.decrypt(&key, &iv, &mut buffer, self.auth_tag)?;
356356

357+
#[allow(clippy::arithmetic_side_effects)] // block sizes are constants
357358
Self::decode_privatekey_comment_pair(
358359
&mut &**buffer,
359360
self.public_key.key_data.clone(),
360361
self.cipher.block_size(),
362+
self.cipher.block_size() - 1,
361363
)
362364
}
363365

@@ -548,8 +550,10 @@ impl PrivateKey {
548550
reader: &mut impl Reader,
549551
public_key: public::KeyData,
550552
block_size: usize,
553+
max_padding_size: usize,
551554
) -> Result<Self> {
552555
debug_assert!(block_size <= MAX_BLOCK_SIZE);
556+
debug_assert!(max_padding_size <= MAX_BLOCK_SIZE);
553557

554558
// Ensure input data is padding-aligned
555559
if reader.remaining_len().checked_rem(block_size) != Some(0) {
@@ -575,7 +579,7 @@ impl PrivateKey {
575579

576580
let padding_len = reader.remaining_len();
577581

578-
if padding_len >= block_size {
582+
if padding_len > max_padding_size {
579583
return Err(encoding::Error::Length.into());
580584
}
581585

@@ -733,7 +737,25 @@ impl Decode for PrivateKey {
733737
}
734738

735739
reader.read_prefixed(|reader| {
736-
Self::decode_privatekey_comment_pair(reader, public_key, cipher.block_size())
740+
// PuTTYgen uses a non-standard block size of 16
741+
// and _always_ adds a padding even if data length
742+
// is divisible by 16 - for unencrypted keys
743+
// in the OpenSSH format.
744+
// We're only relaxing the exact length check, but will
745+
// still validate that the contents of the padding area.
746+
// In all other cases there can be up to (but not including)
747+
// `block_size` padding bytes as per `PROTOCOL.key`.
748+
let max_padding_size = match cipher {
749+
Cipher::None => 16,
750+
#[allow(clippy::arithmetic_side_effects)] // block sizes are constants
751+
_ => cipher.block_size() - 1,
752+
};
753+
Self::decode_privatekey_comment_pair(
754+
reader,
755+
public_key,
756+
cipher.block_size(),
757+
max_padding_size,
758+
)
737759
})
738760
}
739761
}

ssh-key/tests/dot_ssh.rs

Lines changed: 1 addition & 1 deletion
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]
Lines changed: 8 additions & 0 deletions
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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ const OPENSSH_OPAQUE_EXAMPLE: &str = include_str!("examples/id_opaque");
5454
#[cfg(feature = "ecdsa")]
5555
const OPENSSH_PADLESS_WONDER_EXAMPLE: &str = include_str!("examples/padless_wonder");
5656

57+
/// OpenSSH-formatted private key generated by PuTTYgen that showcases its
58+
/// incorrect 16-byte "block size"
59+
#[cfg(feature = "ed25519")]
60+
const OPENSSH_OVERPADDED_PUTTYGEN_EXAMPLE: &str = include_str!("examples/puttygen_overpadded");
61+
5762
/// Get a path into the `tests/scratch` directory.
5863
#[cfg(feature = "std")]
5964
pub fn scratch_path(filename: &str) -> PathBuf {
@@ -155,6 +160,19 @@ fn decode_padless_wonder_openssh() {
155160
assert_eq!("", key.comment());
156161
}
157162

163+
#[cfg(feature = "ed25519")]
164+
#[test]
165+
fn decode_overpadded_puttygen_openssh() {
166+
let key = PrivateKey::from_openssh(OPENSSH_OVERPADDED_PUTTYGEN_EXAMPLE).unwrap();
167+
assert_eq!(Algorithm::Ed25519, key.algorithm());
168+
assert_eq!(Cipher::None, key.cipher());
169+
assert_eq!(KdfAlg::None, key.kdf().algorithm());
170+
assert!(key.kdf().is_none());
171+
172+
#[cfg(feature = "alloc")]
173+
assert_eq!("eddsa-key-20241227a1234567890", key.comment());
174+
}
175+
158176
#[cfg(feature = "ecdsa")]
159177
#[test]
160178
fn decode_ecdsa_p384_openssh() {

0 commit comments

Comments
 (0)