Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
Changelog
=========

.. _v45-0-3:

45.0.3 - 2025-05-25
~~~~~~~~~~~~~~~~~~~

* Fixed decrypting PKCS#8 files encrypted with long salts (this impacts keys
encrypted by Bouncy Castle).
* Fixed decrypting PKCS#8 files encrypted with DES-CBC-MD5. While wildly
insecure, this remains prevalent.

.. _v45-0-2:

45.0.2 - 2025-05-17
Expand Down Expand Up @@ -37,6 +47,24 @@ Changelog
provided (previously no exception was raised), and raises a ``TypeError`` if
the key is encrypted but no password is provided (previously a ``ValueError``
was raised).
* Added ``__copy__`` to the
:class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.ed25519.Ed25519PrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.ed448.Ed448PrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.x25519.X25519PrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.x448.X448PrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`,
:class:`~cryptography.hazmat.primitives.asymmetric.dh.DHPrivateKey`, and
:class:`~cryptography.hazmat.primitives.asymmetric.dh.DHPublicKey`
abstract base classes.
* We significantly refactored how private key loading (
:func:`~cryptography.hazmat.primitives.serialization.load_pem_private_key`
and
Expand Down
5 changes: 5 additions & 0 deletions docs/development/test-vectors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,11 @@ Custom asymmetric vectors
RSA key in an encrypted PEM with a malformed IV (not valid hex).
* ``asymmetric/Traditional_OpenSSL_Serialization/key1-short-iv.pem`` - An
RSA key in an encrypted PEM with an IV that's too short (less than 8 bytes).
* ``asymmetric/PKCS8/rsa-pbewithmd5anddescbc.pem`` - A PKCS8 encoded RSA key
encrypted using the ``pbeWithMD5AndDES-CBC`` algorithm with the password
``hunter2``.
* ``asymmetric/PKCS8/rsa-pbe-3des-long-salt.pem`` - A PKCS8 encoded RSA key
encrypted with a 20 byte salt with the password ``password``.

Key exchange
~~~~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion docs/hazmat/decrepit/ciphers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Decrepit Symmetric algorithms
=============================

.. module:: cryptography.hazmat.decrepit.ciphers
.. module:: cryptography.hazmat.decrepit.ciphers.algorithms

This module contains decrepit symmetric encryption algorithms. These
are algorithms that should not be used unless necessary for backwards
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ build-backend = "maturin"

[project]
name = "cryptography"
version = "45.0.2"
version = "45.0.3"
authors = [
{ name = "The Python Cryptographic Authority and individual contributors", email = "cryptography-dev@python.org" },
]
Expand Down Expand Up @@ -65,7 +65,7 @@ ssh = ["bcrypt >=3.1.5"]
# All the following are used for our own testing.
nox = ["nox >=2024.04.15", "nox[uv] >=2024.03.02; python_version >= '3.8'"]
test = [
"cryptography_vectors==45.0.2",
"cryptography_vectors==45.0.3",
"pytest >=7.4.0",
"pytest-benchmark >=4.0",
"pytest-cov >=2.10.1",
Expand Down
2 changes: 1 addition & 1 deletion src/cryptography/__about__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"__version__",
]

__version__ = "45.0.2"
__version__ = "45.0.3"


__author__ = "The Python Cryptographic Authority and individual contributors"
Expand Down
5 changes: 5 additions & 0 deletions src/cryptography/hazmat/decrepit/ciphers/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ def key_size(self) -> int:
return len(self.key) * 8


# Not actually supported, marker for tests
class _DES:
key_size = 64


class Blowfish(BlockCipherAlgorithm):
name = "Blowfish"
block_size = 64
Expand Down
36 changes: 35 additions & 1 deletion src/rust/cryptography-crypto/src/pbkdf1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,37 @@ pub fn openssl_kdf(
Ok(key)
}

/// PBKDF1 as defined in RFC 2898 for PKCS#5 v1.5 PBE algorithms
pub fn pbkdf1(
hash_alg: openssl::hash::MessageDigest,
password: &[u8],
salt: [u8; 8],
iterations: u64,
length: usize,
) -> Result<Vec<u8>, openssl::error::ErrorStack> {
if length > hash_alg.size() || iterations == 0 {
return Err(openssl::error::ErrorStack::get());
}

let mut h = openssl::hash::Hasher::new(hash_alg)?;
h.update(password)?;
h.update(&salt)?;
let mut t = h.finish()?;

// Apply hash function for specified iterations
for _ in 1..iterations {
let mut h = openssl::hash::Hasher::new(hash_alg)?;
h.update(&t)?;
t = h.finish()?;
}

// Return the first `length` bytes
Ok(t[..length].to_vec())
}

#[cfg(test)]
mod tests {
use super::openssl_kdf;
use super::{openssl_kdf, pbkdf1};

#[test]
fn test_openssl_kdf() {
Expand Down Expand Up @@ -98,4 +126,10 @@ mod tests {
assert_eq!(key, expected);
}
}

#[test]
fn test_pbkdf1() {
assert!(pbkdf1(openssl::hash::MessageDigest::md5(), b"abc", [0; 8], 1, 20).is_err());
assert!(pbkdf1(openssl::hash::MessageDigest::md5(), b"abc", [0; 8], 0, 8).is_err());
}
}
48 changes: 41 additions & 7 deletions src/rust/cryptography-key-parsing/src/pkcs8.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.

use cryptography_x509::common::{AlgorithmIdentifier, AlgorithmParameters, PBES1Params};
use cryptography_x509::common::{
AlgorithmIdentifier, AlgorithmParameters, PbeParams, Pkcs12PbeParams,
};
use cryptography_x509::csr::Attributes;
use cryptography_x509::pkcs8::EncryptedPrivateKeyInfo;

Expand Down Expand Up @@ -122,27 +124,27 @@ pub fn parse_private_key(
}
}

fn pbes1_decrypt(
fn pkcs12_pbe_decrypt(
data: &[u8],
password: &[u8],
cipher: openssl::symm::Cipher,
hash: openssl::hash::MessageDigest,
params: &PBES1Params,
params: &Pkcs12PbeParams<'_>,
) -> KeyParsingResult<Vec<u8>> {
let Ok(password) = std::str::from_utf8(password) else {
return Err(KeyParsingError::IncorrectPassword);
};
let key = cryptography_crypto::pkcs12::kdf(
password,
&params.salt,
params.salt,
cryptography_crypto::pkcs12::KDF_ENCRYPTION_KEY_ID,
params.iterations,
cipher.key_len(),
hash,
)?;
let iv = cryptography_crypto::pkcs12::kdf(
password,
&params.salt,
params.salt,
cryptography_crypto::pkcs12::KDF_IV_ID,
params.iterations,
cipher.block_size(),
Expand All @@ -153,6 +155,31 @@ fn pbes1_decrypt(
.map_err(|_| KeyParsingError::IncorrectPassword)
}

fn pkcs5_pbe_decrypt(
data: &[u8],
password: &[u8],
cipher: openssl::symm::Cipher,
hash: openssl::hash::MessageDigest,
params: &PbeParams,
) -> KeyParsingResult<Vec<u8>> {
// PKCS#5 v1.5 uses PBKDF1 with iteration count
// For PKCS#5 PBE, we need key + IV length
let key_iv_len = cipher.key_len() + cipher.iv_len().unwrap();
let key_iv = cryptography_crypto::pbkdf1::pbkdf1(
hash,
password,
params.salt,
params.iterations,
key_iv_len,
)?;

let key = &key_iv[..cipher.key_len()];
let iv = &key_iv[cipher.key_len()..];

openssl::symm::decrypt(cipher, key, Some(iv), data)
.map_err(|_| KeyParsingError::IncorrectPassword)
}

pub fn parse_encrypted_private_key(
data: &[u8],
password: Option<&[u8]>,
Expand All @@ -164,15 +191,22 @@ pub fn parse_encrypted_private_key(
};

let plaintext = match epki.encryption_algorithm.params {
AlgorithmParameters::Pbes1WithShaAnd3KeyTripleDesCbc(params) => pbes1_decrypt(
AlgorithmParameters::PbeWithMd5AndDesCbc(params) => pkcs5_pbe_decrypt(
epki.encrypted_data,
password,
openssl::symm::Cipher::des_cbc(),
openssl::hash::MessageDigest::md5(),
&params,
)?,
AlgorithmParameters::PbeWithShaAnd3KeyTripleDesCbc(params) => pkcs12_pbe_decrypt(
epki.encrypted_data,
password,
openssl::symm::Cipher::des_ede3_cbc(),
openssl::hash::MessageDigest::sha1(),
&params,
)?,
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_RC2"))]
AlgorithmParameters::Pbe1WithShaAnd40BitRc2Cbc(params) => pbes1_decrypt(
AlgorithmParameters::PbeWithShaAnd40BitRc2Cbc(params) => pkcs12_pbe_decrypt(
epki.encrypted_data,
password,
openssl::symm::Cipher::rc2_40_cbc(),
Expand Down
20 changes: 15 additions & 5 deletions src/rust/cryptography-x509/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,12 @@ pub enum AlgorithmParameters<'a> {
#[defined_by(oid::RC2_CBC)]
Rc2Cbc(Rc2CbcParams),

#[defined_by(oid::PBES1_WITH_SHA_AND_3KEY_TRIPLEDES_CBC)]
Pbes1WithShaAnd3KeyTripleDesCbc(PBES1Params),
#[defined_by(oid::PBES1_WITH_SHA_AND_40_BIT_RC2_CBC)]
Pbe1WithShaAnd40BitRc2Cbc(PBES1Params),
#[defined_by(oid::PBE_WITH_MD5_AND_DES_CBC)]
PbeWithMd5AndDesCbc(PbeParams),
#[defined_by(oid::PBE_WITH_SHA_AND_3KEY_TRIPLEDES_CBC)]
PbeWithShaAnd3KeyTripleDesCbc(Pkcs12PbeParams<'a>),
#[defined_by(oid::PBE_WITH_SHA_AND_40_BIT_RC2_CBC)]
PbeWithShaAnd40BitRc2Cbc(Pkcs12PbeParams<'a>),

#[default]
Other(asn1::ObjectIdentifier, Option<asn1::Tlv<'a>>),
Expand Down Expand Up @@ -529,12 +531,20 @@ pub struct ScryptParams<'a> {
pub key_length: Option<u32>,
}

// RFC 8018 Appendix A.3
#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Eq, Hash, Clone, Debug)]
pub struct PBES1Params {
pub struct PbeParams {
pub salt: [u8; 8],
pub iterations: u64,
}

// From RFC 7202 Appendix C
#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Eq, Hash, Clone, Debug)]
pub struct Pkcs12PbeParams<'a> {
pub salt: &'a [u8],
pub iterations: u64,
}

#[derive(asn1::Asn1Read, asn1::Asn1Write, PartialEq, Eq, Hash, Clone, Debug)]
pub struct Rc2CbcParams {
pub version: Option<u32>,
Expand Down
5 changes: 3 additions & 2 deletions src/rust/cryptography-x509/src/oid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,12 @@ pub const EKU_CERTIFICATE_TRANSPARENCY_OID: asn1::ObjectIdentifier =

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

pub const PBES1_WITH_SHA_AND_3KEY_TRIPLEDES_CBC: asn1::ObjectIdentifier =
pub const PBE_WITH_SHA_AND_3KEY_TRIPLEDES_CBC: asn1::ObjectIdentifier =
asn1::oid!(1, 2, 840, 113549, 1, 12, 1, 3);
pub const PBES1_WITH_SHA_AND_40_BIT_RC2_CBC: asn1::ObjectIdentifier =
pub const PBE_WITH_SHA_AND_40_BIT_RC2_CBC: asn1::ObjectIdentifier =
asn1::oid!(1, 2, 840, 113549, 1, 12, 1, 6);

pub const AES_128_CBC_OID: asn1::ObjectIdentifier = asn1::oid!(2, 16, 840, 1, 101, 3, 4, 1, 2);
Expand Down
3 changes: 3 additions & 0 deletions src/rust/src/backend/cipher_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ fn get_cipher_registry(
let aes128 = types::AES128.get(py)?;
let aes256 = types::AES256.get(py)?;
let triple_des = types::TRIPLE_DES.get(py)?;
let des = types::DES.get(py)?;
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_CAMELLIA"))]
let camellia = types::CAMELLIA.get(py)?;
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_BF"))]
Expand Down Expand Up @@ -306,6 +307,8 @@ fn get_cipher_registry(
#[cfg(not(CRYPTOGRAPHY_OSSLCONF = "OPENSSL_NO_RC4"))]
m.add(&arc4, none_type.as_any(), None, Cipher::rc4())?;

m.add(&des, &cbc, Some(64), Cipher::des_cbc())?;

if let Some(rc2_cbc) = Cipher::from_nid(openssl::nid::Nid::RC2_CBC) {
m.add(&rc2, &cbc, Some(128), rc2_cbc)?;
}
Expand Down
14 changes: 7 additions & 7 deletions src/rust/src/pkcs12.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,14 +112,14 @@ pub(crate) fn symmetric_encrypt(
}

enum EncryptionAlgorithm {
PBESv1SHA1And3KeyTripleDESCBC,
PBESHA1And3KeyTripleDESCBC,
PBESv2SHA256AndAES256CBC,
}

impl EncryptionAlgorithm {
fn salt_length(&self) -> usize {
match self {
EncryptionAlgorithm::PBESv1SHA1And3KeyTripleDESCBC => 8,
EncryptionAlgorithm::PBESHA1And3KeyTripleDESCBC => 8,
EncryptionAlgorithm::PBESv2SHA256AndAES256CBC => 16,
}
}
Expand All @@ -131,11 +131,11 @@ impl EncryptionAlgorithm {
iv: &'a [u8],
) -> cryptography_x509::common::AlgorithmIdentifier<'a> {
match self {
EncryptionAlgorithm::PBESv1SHA1And3KeyTripleDESCBC => {
EncryptionAlgorithm::PBESHA1And3KeyTripleDESCBC => {
cryptography_x509::common::AlgorithmIdentifier {
oid: asn1::DefinedByMarker::marker(),
params: cryptography_x509::common::AlgorithmParameters::Pbes1WithShaAnd3KeyTripleDesCbc(cryptography_x509::common::PBES1Params{
salt: salt[..8].try_into().unwrap(),
params: cryptography_x509::common::AlgorithmParameters::PbeWithShaAnd3KeyTripleDesCbc(cryptography_x509::common::Pkcs12PbeParams{
salt,
iterations: cipher_kdf_iter,
}),
}
Expand Down Expand Up @@ -189,7 +189,7 @@ impl EncryptionAlgorithm {
data: &[u8],
) -> CryptographyResult<Vec<u8>> {
match self {
EncryptionAlgorithm::PBESv1SHA1And3KeyTripleDESCBC => {
EncryptionAlgorithm::PBESHA1And3KeyTripleDESCBC => {
let key = cryptography_crypto::pkcs12::kdf(
password,
salt,
Expand Down Expand Up @@ -341,7 +341,7 @@ fn decode_encryption_algorithm<'a>(
let key_cert_alg =
encryption_algorithm.getattr(pyo3::intern!(py, "_key_cert_algorithm"))?;
let cipher = if key_cert_alg.is(&types::PBES_PBESV1SHA1AND3KEYTRIPLEDESCBC.get(py)?) {
EncryptionAlgorithm::PBESv1SHA1And3KeyTripleDESCBC
EncryptionAlgorithm::PBESHA1And3KeyTripleDESCBC
} else if key_cert_alg.is(&types::PBES_PBESV2SHA256ANDAES256CBC.get(py)?) {
EncryptionAlgorithm::PBESv2SHA256AndAES256CBC
} else {
Expand Down
2 changes: 2 additions & 0 deletions src/rust/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,8 @@ pub static TRIPLE_DES: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.decrepit.ciphers.algorithms",
&["TripleDES"],
);
pub static DES: LazyPyImport =
LazyPyImport::new("cryptography.hazmat.decrepit.ciphers.algorithms", &["_DES"]);
pub static AES: LazyPyImport = LazyPyImport::new(
"cryptography.hazmat.primitives.ciphers.algorithms",
&["AES"],
Expand Down
Loading
Loading