Skip to content

Commit 0b56f6f

Browse files
committed
fixed RustCrypto#316 - correctly parse OpenSSH keys generated by PuTTYgen
1 parent cacacce commit 0b56f6f

File tree

4 files changed

+50
-4
lines changed

4 files changed

+50
-4
lines changed

ssh-key/src/private.rs

+23-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"))]
@@ -358,6 +358,7 @@ impl PrivateKey {
358358
&mut &**buffer,
359359
self.public_key.key_data.clone(),
360360
self.cipher.block_size(),
361+
self.cipher.block_size() - 1,
361362
)
362363
}
363364

@@ -548,8 +549,10 @@ impl PrivateKey {
548549
reader: &mut impl Reader,
549550
public_key: public::KeyData,
550551
block_size: usize,
552+
max_padding_size: usize,
551553
) -> Result<Self> {
552554
debug_assert!(block_size <= MAX_BLOCK_SIZE);
555+
debug_assert!(max_padding_size <= MAX_BLOCK_SIZE);
553556

554557
// Ensure input data is padding-aligned
555558
if reader.remaining_len().checked_rem(block_size) != Some(0) {
@@ -575,7 +578,7 @@ impl PrivateKey {
575578

576579
let padding_len = reader.remaining_len();
577580

578-
if padding_len >= block_size {
581+
if padding_len > max_padding_size {
579582
return Err(encoding::Error::Length.into());
580583
}
581584

@@ -733,7 +736,24 @@ impl Decode for PrivateKey {
733736
}
734737

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

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
@@ -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)