Skip to content

Commit 8aa1da6

Browse files
authored
fixes #12949 -- added support for decrypting des-cbc-md5 keys (#12978)
1 parent d1dc187 commit 8aa1da6

File tree

8 files changed

+106
-4
lines changed

8 files changed

+106
-4
lines changed

src/cryptography/hazmat/decrepit/ciphers/algorithms.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ def key_size(self) -> int:
4040
return len(self.key) * 8
4141

4242

43+
# Not actually supported, marker for tests
44+
class _DES:
45+
key_size = 64
46+
47+
4348
class Blowfish(BlockCipherAlgorithm):
4449
name = "Blowfish"
4550
block_size = 64

src/rust/cryptography-crypto/src/pbkdf1.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,37 @@ pub fn openssl_kdf(
3030
Ok(key)
3131
}
3232

33+
/// PBKDF1 as defined in RFC 2898 for PKCS#5 v1.5 PBE algorithms
34+
pub fn pbkdf1(
35+
hash_alg: openssl::hash::MessageDigest,
36+
password: &[u8],
37+
salt: [u8; 8],
38+
iterations: u64,
39+
length: usize,
40+
) -> Result<Vec<u8>, openssl::error::ErrorStack> {
41+
if length > hash_alg.size() || iterations == 0 {
42+
return Err(openssl::error::ErrorStack::get());
43+
}
44+
45+
let mut h = openssl::hash::Hasher::new(hash_alg)?;
46+
h.update(password)?;
47+
h.update(&salt)?;
48+
let mut t = h.finish()?;
49+
50+
// Apply hash function for specified iterations
51+
for _ in 1..iterations {
52+
let mut h = openssl::hash::Hasher::new(hash_alg)?;
53+
h.update(&t)?;
54+
t = h.finish()?;
55+
}
56+
57+
// Return the first `length` bytes
58+
Ok(t[..length].to_vec())
59+
}
60+
3361
#[cfg(test)]
3462
mod tests {
35-
use super::openssl_kdf;
63+
use super::{openssl_kdf, pbkdf1};
3664

3765
#[test]
3866
fn test_openssl_kdf() {
@@ -98,4 +126,10 @@ mod tests {
98126
assert_eq!(key, expected);
99127
}
100128
}
129+
130+
#[test]
131+
fn test_pbkdf1() {
132+
assert!(pbkdf1(openssl::hash::MessageDigest::md5(), b"abc", [0; 8], 1, 20).is_err());
133+
assert!(pbkdf1(openssl::hash::MessageDigest::md5(), b"abc", [0; 8], 0, 8).is_err());
134+
}
101135
}

src/rust/cryptography-key-parsing/src/pkcs8.rs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
33
// for complete details.
44

5-
use cryptography_x509::common::{AlgorithmIdentifier, AlgorithmParameters, Pkcs12PbeParams};
5+
use cryptography_x509::common::{
6+
AlgorithmIdentifier, AlgorithmParameters, PbeParams, Pkcs12PbeParams,
7+
};
68
use cryptography_x509::csr::Attributes;
79
use cryptography_x509::pkcs8::EncryptedPrivateKeyInfo;
810

@@ -153,6 +155,31 @@ fn pkcs12_pbe_decrypt(
153155
.map_err(|_| KeyParsingError::IncorrectPassword)
154156
}
155157

158+
fn pkcs5_pbe_decrypt(
159+
data: &[u8],
160+
password: &[u8],
161+
cipher: openssl::symm::Cipher,
162+
hash: openssl::hash::MessageDigest,
163+
params: &PbeParams,
164+
) -> KeyParsingResult<Vec<u8>> {
165+
// PKCS#5 v1.5 uses PBKDF1 with iteration count
166+
// For PKCS#5 PBE, we need key + IV length
167+
let key_iv_len = cipher.key_len() + cipher.iv_len().unwrap();
168+
let key_iv = cryptography_crypto::pbkdf1::pbkdf1(
169+
hash,
170+
password,
171+
params.salt,
172+
params.iterations,
173+
key_iv_len,
174+
)?;
175+
176+
let key = &key_iv[..cipher.key_len()];
177+
let iv = &key_iv[cipher.key_len()..];
178+
179+
openssl::symm::decrypt(cipher, key, Some(iv), data)
180+
.map_err(|_| KeyParsingError::IncorrectPassword)
181+
}
182+
156183
pub fn parse_encrypted_private_key(
157184
data: &[u8],
158185
password: Option<&[u8]>,
@@ -164,6 +191,13 @@ pub fn parse_encrypted_private_key(
164191
};
165192

166193
let plaintext = match epki.encryption_algorithm.params {
194+
AlgorithmParameters::PbeWithMd5AndDesCbc(params) => pkcs5_pbe_decrypt(
195+
epki.encrypted_data,
196+
password,
197+
openssl::symm::Cipher::des_cbc(),
198+
openssl::hash::MessageDigest::md5(),
199+
&params,
200+
)?,
167201
AlgorithmParameters::PbeWithShaAnd3KeyTripleDesCbc(params) => pkcs12_pbe_decrypt(
168202
epki.encrypted_data,
169203
password,

src/rust/cryptography-x509/src/common.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ pub enum AlgorithmParameters<'a> {
165165
#[defined_by(oid::RC2_CBC)]
166166
Rc2Cbc(Rc2CbcParams),
167167

168+
#[defined_by(oid::PBE_WITH_MD5_AND_DES_CBC)]
169+
PbeWithMd5AndDesCbc(PbeParams),
168170
#[defined_by(oid::PBE_WITH_SHA_AND_3KEY_TRIPLEDES_CBC)]
169171
PbeWithShaAnd3KeyTripleDesCbc(Pkcs12PbeParams<'a>),
170172
#[defined_by(oid::PBE_WITH_SHA_AND_40_BIT_RC2_CBC)]
@@ -529,6 +531,13 @@ pub struct ScryptParams<'a> {
529531
pub key_length: Option<u32>,
530532
}
531533

534+
// RFC 8018 Appendix A.3
535+
#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Eq, Hash, Clone, Debug)]
536+
pub struct PbeParams {
537+
pub salt: [u8; 8],
538+
pub iterations: u64,
539+
}
540+
532541
// From RFC 7202 Appendix C
533542
#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Eq, Hash, Clone, Debug)]
534543
pub struct Pkcs12PbeParams<'a> {

src/rust/cryptography-x509/src/oid.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ pub const EKU_CERTIFICATE_TRANSPARENCY_OID: asn1::ObjectIdentifier =
152152

153153
pub const PBES2_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 5, 13);
154154
pub const PBKDF2_OID: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 5, 12);
155+
pub const PBE_WITH_MD5_AND_DES_CBC: asn1::ObjectIdentifier = asn1::oid!(1, 2, 840, 113549, 1, 5, 3);
155156
pub const SCRYPT_OID: asn1::ObjectIdentifier = asn1::oid!(1, 3, 6, 1, 4, 1, 11591, 4, 11);
156157

157158
pub const PBE_WITH_SHA_AND_3KEY_TRIPLEDES_CBC: asn1::ObjectIdentifier =

src/rust/src/backend/cipher_registry.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ fn get_cipher_registry(
123123
let aes128 = types::AES128.get(py)?;
124124
let aes256 = types::AES256.get(py)?;
125125
let triple_des = types::TRIPLE_DES.get(py)?;
126+
let des = types::DES.get(py)?;
126127
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_CAMELLIA"))]
127128
let camellia = types::CAMELLIA.get(py)?;
128129
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_BF"))]
@@ -306,6 +307,8 @@ fn get_cipher_registry(
306307
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_RC4"))]
307308
m.add(&arc4, none_type.as_any(), None, Cipher::rc4())?;
308309

310+
m.add(&des, &cbc, Some(64), Cipher::des_cbc())?;
311+
309312
if let Some(rc2_cbc) = Cipher::from_nid(openssl::nid::Nid::RC2_CBC) {
310313
m.add(&rc2, &cbc, Some(128), rc2_cbc)?;
311314
}

src/rust/src/types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,8 @@ pub static TRIPLE_DES: LazyPyImport = LazyPyImport::new(
513513
"cryptography.hazmat.decrepit.ciphers.algorithms",
514514
&["TripleDES"],
515515
);
516+
pub static DES: LazyPyImport =
517+
LazyPyImport::new("cryptography.hazmat.decrepit.ciphers.algorithms", &["_DES"]);
516518
pub static AES: LazyPyImport = LazyPyImport::new(
517519
"cryptography.hazmat.primitives.ciphers.algorithms",
518520
&["AES"],

tests/hazmat/primitives/test_serialization.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import pytest
1212

1313
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
14-
from cryptography.hazmat.decrepit.ciphers.algorithms import RC2
14+
from cryptography.hazmat.decrepit.ciphers.algorithms import _DES, RC2
1515
from cryptography.hazmat.primitives.asymmetric import (
1616
dsa,
1717
ec,
@@ -22,7 +22,7 @@
2222
x25519,
2323
)
2424
from cryptography.hazmat.primitives.ciphers import modes
25-
from cryptography.hazmat.primitives.hashes import SHA1
25+
from cryptography.hazmat.primitives.hashes import MD5, SHA1
2626
from cryptography.hazmat.primitives.serialization import (
2727
BestAvailableEncryption,
2828
Encoding,
@@ -573,6 +573,20 @@ def test_load_pkcs8_scrypt(self):
573573
)
574574
assert isinstance(key, ed25519.Ed25519PrivateKey)
575575

576+
@pytest.mark.supported(
577+
only_if=lambda backend: backend.hash_supported(MD5())
578+
and backend.cipher_supported(_DES(), modes.CBC(b"\x00" * 8)),
579+
skip_message="Does not support DES MD5",
580+
)
581+
def test_load_pkcs8_pbe_with_md5_and_des_cbc(self):
582+
key = load_vectors_from_file(
583+
os.path.join("asymmetric", "PKCS8", "rsa-pbewithmd5anddescbc.pem"),
584+
lambda f: load_pem_private_key(f.read(), password=b"hunter2"),
585+
mode="rb",
586+
)
587+
assert isinstance(key, rsa.RSAPrivateKey)
588+
assert key.key_size == 2048
589+
576590

577591
class TestPEMSerialization:
578592
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)