diff --git a/src/cryptography/hazmat/primitives/asymmetric/dh.py b/src/cryptography/hazmat/primitives/asymmetric/dh.py index 814fdd3f8311..bd591626fb07 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/dh.py +++ b/src/cryptography/hazmat/primitives/asymmetric/dh.py @@ -144,6 +144,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ Returns the key serialized as bytes. diff --git a/src/cryptography/hazmat/primitives/asymmetric/dsa.py b/src/cryptography/hazmat/primitives/asymmetric/dsa.py index 5f412df9d023..b007af8e52ae 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/dsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/dsa.py @@ -79,6 +79,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ Returns the key serialized as bytes. diff --git a/src/cryptography/hazmat/primitives/asymmetric/ec.py b/src/cryptography/hazmat/primitives/asymmetric/ec.py index 39e67519cf92..8681769ae518 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/ec.py +++ b/src/cryptography/hazmat/primitives/asymmetric/ec.py @@ -113,6 +113,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ Returns the key serialized as bytes. diff --git a/src/cryptography/hazmat/primitives/asymmetric/ed25519.py b/src/cryptography/hazmat/primitives/asymmetric/ed25519.py index 70aec5b10217..b73d3d4b25a3 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/ed25519.py +++ b/src/cryptography/hazmat/primitives/asymmetric/ed25519.py @@ -82,6 +82,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ The serialized bytes of the private key. diff --git a/src/cryptography/hazmat/primitives/asymmetric/ed448.py b/src/cryptography/hazmat/primitives/asymmetric/ed448.py index 9ecb478bdf99..d63804413253 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/ed448.py +++ b/src/cryptography/hazmat/primitives/asymmetric/ed448.py @@ -114,6 +114,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ The serialized bytes of the private key. diff --git a/src/cryptography/hazmat/primitives/asymmetric/mldsa.py b/src/cryptography/hazmat/primitives/asymmetric/mldsa.py index beba90fb2bee..5ae5526774b5 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/mldsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/mldsa.py @@ -127,6 +127,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ The serialized bytes of the private key. diff --git a/src/cryptography/hazmat/primitives/asymmetric/mlkem.py b/src/cryptography/hazmat/primitives/asymmetric/mlkem.py index 64bca1151bd2..a6671d76f163 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/mlkem.py +++ b/src/cryptography/hazmat/primitives/asymmetric/mlkem.py @@ -249,6 +249,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ The serialized bytes of the private key. diff --git a/src/cryptography/hazmat/primitives/asymmetric/rsa.py b/src/cryptography/hazmat/primitives/asymmetric/rsa.py index d730cebdd35c..c04ae8c1f854 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/rsa.py +++ b/src/cryptography/hazmat/primitives/asymmetric/rsa.py @@ -60,6 +60,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ Returns the key serialized as bytes. diff --git a/src/cryptography/hazmat/primitives/asymmetric/x25519.py b/src/cryptography/hazmat/primitives/asymmetric/x25519.py index 749899888f2b..60c940408b92 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/x25519.py +++ b/src/cryptography/hazmat/primitives/asymmetric/x25519.py @@ -100,6 +100,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ The serialized bytes of the private key. diff --git a/src/cryptography/hazmat/primitives/asymmetric/x448.py b/src/cryptography/hazmat/primitives/asymmetric/x448.py index b9dc8261a2f2..ad204daef2e5 100644 --- a/src/cryptography/hazmat/primitives/asymmetric/x448.py +++ b/src/cryptography/hazmat/primitives/asymmetric/x448.py @@ -102,6 +102,8 @@ def private_bytes( encoding: _serialization.Encoding, format: _serialization.PrivateFormat, encryption_algorithm: _serialization.KeySerializationEncryption, + *, + comment: bytes = b"", ) -> bytes: """ The serialized bytes of the private key. diff --git a/src/cryptography/hazmat/primitives/serialization/__init__.py b/src/cryptography/hazmat/primitives/serialization/__init__.py index 40beb2196ad7..95a78e88bd7c 100644 --- a/src/cryptography/hazmat/primitives/serialization/__init__.py +++ b/src/cryptography/hazmat/primitives/serialization/__init__.py @@ -33,6 +33,7 @@ SSHPrivateKeyTypes, SSHPublicKeyTypes, load_ssh_private_key, + load_ssh_private_key_with_comment, load_ssh_public_identity, load_ssh_public_key, ssh_key_fingerprint, @@ -61,6 +62,7 @@ "load_pem_private_key", "load_pem_public_key", "load_ssh_private_key", + "load_ssh_private_key_with_comment", "load_ssh_public_identity", "load_ssh_public_key", "ssh_key_fingerprint", diff --git a/src/cryptography/hazmat/primitives/serialization/ssh.py b/src/cryptography/hazmat/primitives/serialization/ssh.py index 13972486c1db..0272238dda6f 100644 --- a/src/cryptography/hazmat/primitives/serialization/ssh.py +++ b/src/cryptography/hazmat/primitives/serialization/ssh.py @@ -669,14 +669,17 @@ def _lookup_kformat(key_type: utils.Buffer): ] -def load_ssh_private_key( +def load_ssh_private_key_with_comment( data: utils.Buffer, password: bytes | None, backend: typing.Any = None, *, unsafe_skip_rsa_key_validation: bool = False, -) -> SSHPrivateKeyTypes: - """Load private key from OpenSSH custom encoding.""" +) -> tuple[SSHPrivateKeyTypes, bytes]: + """ + Load private key from OpenSSH custom encoding and + return (key, comment). + """ utils._check_byteslike("data", data) if password is not None: utils._check_bytes("password", password) @@ -772,8 +775,9 @@ def load_ssh_private_key( pubfields, unsafe_skip_rsa_key_validation=unsafe_skip_rsa_key_validation, ) - # We don't use the comment - _, edata = _get_sshstr(edata) + + # Capture the comment instead of discarding it with _ + comment, edata = _get_sshstr(edata) # yes, SSH does padding check *after* all other parsing is done. # need to follow as it writes zero-byte padding too. @@ -788,6 +792,23 @@ def load_ssh_private_key( stacklevel=2, ) + return private_key, comment.tobytes() + + +def load_ssh_private_key( + data: utils.Buffer, + password: bytes | None, + backend: typing.Any = None, + *, + unsafe_skip_rsa_key_validation: bool = False, +) -> SSHPrivateKeyTypes: + """Load private key from OpenSSH custom encoding.""" + private_key, _ = load_ssh_private_key_with_comment( + data, + password, + backend, + unsafe_skip_rsa_key_validation=unsafe_skip_rsa_key_validation, + ) return private_key @@ -795,13 +816,15 @@ def _serialize_ssh_private_key( private_key: SSHPrivateKeyTypes, password: bytes, encryption_algorithm: KeySerializationEncryption, + comment: bytes = b"", ) -> bytes: """Serialize private key with OpenSSH custom encoding.""" utils._check_bytes("password", password) + utils._check_bytes("comment", comment) # <-- Add validation here if isinstance(private_key, dsa._DSAPrivateKey): warnings.warn( - "SSH DSA key support is deprecated and will be " - "removed in a future release", + "SSH DSA key support is deprecated and will be removed in a " + "future release", utils.DeprecatedIn40, stacklevel=4, ) @@ -831,7 +854,9 @@ def _serialize_ssh_private_key( ciph = None nkeys = 1 checkval = os.urandom(4) - comment = b"" + # To this (split into two shorter lines): + # comment is passed in as a parameter now, so we don't + # overwrite it with b"" unless default is used # encode public and private parts together f_public_key = _FragList() @@ -841,7 +866,7 @@ def _serialize_ssh_private_key( f_secrets = _FragList([checkval, checkval]) f_secrets.put_sshstr(key_type) kformat.encode_private(private_key, f_secrets) - f_secrets.put_sshstr(comment) + f_secrets.put_sshstr(comment) # <-- Uses the passed comment parameter here f_secrets.put_raw(_PADDING[: blklen - (f_secrets.size() % blklen)]) # top-level structure diff --git a/src/rust/src/backend/dh.rs b/src/rust/src/backend/dh.rs index 7e5001dfd68e..f123333230d9 100644 --- a/src/rust/src/backend/dh.rs +++ b/src/rust/src/backend/dh.rs @@ -263,12 +263,15 @@ impl DHPrivateKey { }) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { if format != crate::serialization::PrivateFormat::PKCS8 { return Err(CryptographyError::from( @@ -285,8 +288,9 @@ impl DHPrivateKey { encoding, format, encryption_algorithm, - true, false, + false, + None, ) } diff --git a/src/rust/src/backend/dsa.rs b/src/rust/src/backend/dsa.rs index 56363f49366c..88bc451cb507 100644 --- a/src/rust/src/backend/dsa.rs +++ b/src/rust/src/backend/dsa.rs @@ -3,6 +3,7 @@ // for complete details. use pyo3::types::PyAnyMethods; +use pyo3::types::PyBytesMethods; use crate::backend::utils; use crate::buf::CffiBuf; @@ -150,13 +151,20 @@ impl DsaPrivateKey { }) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -166,6 +174,7 @@ impl DsaPrivateKey { encryption_algorithm, true, false, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/ec.rs b/src/rust/src/backend/ec.rs index ca215ba1637a..2c2a436c1b99 100644 --- a/src/rust/src/backend/ec.rs +++ b/src/rust/src/backend/ec.rs @@ -6,6 +6,7 @@ use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use pyo3::types::PyAnyMethods; +use pyo3::types::PyBytesMethods; use crate::backend::utils; use crate::buf::CffiBuf; @@ -340,13 +341,20 @@ impl ECPrivateKey { }) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -356,6 +364,7 @@ impl ECPrivateKey { encryption_algorithm, true, false, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/ed25519.rs b/src/rust/src/backend/ed25519.rs index c58be6a68b51..1ad0785224d4 100644 --- a/src/rust/src/backend/ed25519.rs +++ b/src/rust/src/backend/ed25519.rs @@ -6,6 +6,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::{CryptographyError, CryptographyResult}; use crate::exceptions; +use pyo3::types::PyBytesMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.ed25519")] pub(crate) struct Ed25519PrivateKey { @@ -100,13 +101,20 @@ impl Ed25519PrivateKey { Ok(pyo3::types::PyBytes::new(py, &raw_bytes)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -116,6 +124,7 @@ impl Ed25519PrivateKey { encryption_algorithm, true, true, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/ed448.rs b/src/rust/src/backend/ed448.rs index f5a31ff0bffb..2e89b801b84f 100644 --- a/src/rust/src/backend/ed448.rs +++ b/src/rust/src/backend/ed448.rs @@ -6,6 +6,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::{CryptographyError, CryptographyResult}; use crate::exceptions; +use pyo3::types::PyAnyMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.ed448")] pub(crate) struct Ed448PrivateKey { @@ -98,13 +99,24 @@ impl Ed448PrivateKey { Ok(pyo3::types::PyBytes::new(py, &raw_bytes)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + // Intercept raw requests and route them to private_bytes_raw: + if encoding == crate::serialization::Encoding::Raw + && format == crate::serialization::PrivateFormat::Raw + && encryption_algorithm.is_instance(&crate::types::NO_ENCRYPTION.get(py)?)? + { + return slf.borrow().private_bytes_raw(py); + } + utils::pkey_private_bytes( py, slf, @@ -112,8 +124,9 @@ impl Ed448PrivateKey { encoding, format, encryption_algorithm, - true, - true, + false, + false, + None, ) } diff --git a/src/rust/src/backend/mldsa.rs b/src/rust/src/backend/mldsa.rs index 0fe421f472b8..8af6281d0ccd 100644 --- a/src/rust/src/backend/mldsa.rs +++ b/src/rust/src/backend/mldsa.rs @@ -130,12 +130,15 @@ impl MlDsa44PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { if encoding == crate::serialization::Encoding::Raw && format == crate::serialization::PrivateFormat::Raw @@ -143,6 +146,7 @@ impl MlDsa44PrivateKey { { return slf.borrow().private_bytes_raw(py); } + utils::pkey_private_bytes( py, slf, @@ -150,8 +154,9 @@ impl MlDsa44PrivateKey { encoding, format, encryption_algorithm, - true, false, + false, + None, ) } @@ -377,12 +382,15 @@ impl MlDsa65PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { // Intercept Raw/Raw/NoEncryption so we return the seed. // The generic pkey_private_bytes raw path calls raw_private_key() @@ -393,6 +401,7 @@ impl MlDsa65PrivateKey { { return slf.borrow().private_bytes_raw(py); } + utils::pkey_private_bytes( py, slf, @@ -400,8 +409,9 @@ impl MlDsa65PrivateKey { encoding, format, encryption_algorithm, - true, false, + false, + None, ) } @@ -627,12 +637,15 @@ impl MlDsa87PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { if encoding == crate::serialization::Encoding::Raw && format == crate::serialization::PrivateFormat::Raw @@ -640,6 +653,7 @@ impl MlDsa87PrivateKey { { return slf.borrow().private_bytes_raw(py); } + utils::pkey_private_bytes( py, slf, @@ -647,8 +661,9 @@ impl MlDsa87PrivateKey { encoding, format, encryption_algorithm, - true, false, + false, + None, ) } diff --git a/src/rust/src/backend/mlkem.rs b/src/rust/src/backend/mlkem.rs index 42195dbc2559..fe1557daa13a 100644 --- a/src/rust/src/backend/mlkem.rs +++ b/src/rust/src/backend/mlkem.rs @@ -127,12 +127,15 @@ impl MlKem768PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { // Intercept Raw/Raw/NoEncryption so we return the seed. // The generic pkey_private_bytes raw path calls raw_private_key() @@ -143,6 +146,7 @@ impl MlKem768PrivateKey { { return slf.borrow().private_bytes_raw(py); } + utils::pkey_private_bytes( py, slf, @@ -150,8 +154,9 @@ impl MlKem768PrivateKey { encoding, format, encryption_algorithm, - true, false, + false, + None, ) } @@ -277,12 +282,15 @@ impl MlKem1024PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { // Intercept Raw/Raw/NoEncryption so we return the seed. // The generic pkey_private_bytes raw path calls raw_private_key() @@ -293,6 +301,7 @@ impl MlKem1024PrivateKey { { return slf.borrow().private_bytes_raw(py); } + utils::pkey_private_bytes( py, slf, @@ -300,8 +309,9 @@ impl MlKem1024PrivateKey { encoding, format, encryption_algorithm, - true, false, + false, + None, ) } diff --git a/src/rust/src/backend/rsa.rs b/src/rust/src/backend/rsa.rs index 86bc1a05b3e8..372527d1318d 100644 --- a/src/rust/src/backend/rsa.rs +++ b/src/rust/src/backend/rsa.rs @@ -7,6 +7,7 @@ use std::hash::{Hash, Hasher}; use std::sync::LazyLock; use pyo3::types::PyAnyMethods; +use pyo3::types::PyBytesMethods; use crate::backend::{hashes, utils}; use crate::buf::CffiBuf; @@ -423,13 +424,20 @@ impl RsaPrivateKey { }) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -439,6 +447,7 @@ impl RsaPrivateKey { encryption_algorithm, true, false, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/utils.rs b/src/rust/src/backend/utils.rs index f8a6369a4292..d8405b870174 100644 --- a/src/rust/src/backend/utils.rs +++ b/src/rust/src/backend/utils.rs @@ -48,7 +48,9 @@ pub(crate) fn pkey_private_bytes<'p>( encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, openssh_allowed: bool, raw_allowed: bool, + comment: Option<&[u8]>, ) -> CryptographyResult> { + let comment = comment.unwrap_or(b""); if !encryption_algorithm.is_instance(&types::KEY_SERIALIZATION_ENCRYPTION.get(py)?)? { return Err(CryptographyError::from( pyo3::exceptions::PyTypeError::new_err( @@ -193,7 +195,7 @@ pub(crate) fn pkey_private_bytes<'p>( if encoding == Encoding::PEM { let raw_bytes = types::SERIALIZE_SSH_PRIVATE_KEY .get(py)? - .call1((key_obj, password, encryption_algorithm))? + .call1((key_obj, password, encryption_algorithm, comment))? .extract()?; return crate::asn1::encode_der_data( py, diff --git a/src/rust/src/backend/x25519.rs b/src/rust/src/backend/x25519.rs index 577fa2dd5460..85f8cb17a3b8 100644 --- a/src/rust/src/backend/x25519.rs +++ b/src/rust/src/backend/x25519.rs @@ -5,6 +5,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::CryptographyResult; +use pyo3::types::PyAnyMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.x25519")] pub(crate) struct X25519PrivateKey { @@ -99,13 +100,24 @@ impl X25519PrivateKey { Ok(pyo3::types::PyBytes::new(py, &raw_bytes)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + // Intercept raw requests and route them to private_bytes_raw: + if encoding == crate::serialization::Encoding::Raw + && format == crate::serialization::PrivateFormat::Raw + && encryption_algorithm.is_instance(&crate::types::NO_ENCRYPTION.get(py)?)? + { + return slf.borrow().private_bytes_raw(py); + } + utils::pkey_private_bytes( py, slf, @@ -114,7 +126,8 @@ impl X25519PrivateKey { format, encryption_algorithm, false, - true, + false, + None, ) } diff --git a/src/rust/src/backend/x448.rs b/src/rust/src/backend/x448.rs index 9374d8753f2f..23eaf92c6c9b 100644 --- a/src/rust/src/backend/x448.rs +++ b/src/rust/src/backend/x448.rs @@ -5,6 +5,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::CryptographyResult; +use pyo3::types::PyAnyMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.x448")] pub(crate) struct X448PrivateKey { @@ -98,13 +99,24 @@ impl X448PrivateKey { Ok(pyo3::types::PyBytes::new(py, &raw_bytes)) } + #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, encoding: crate::serialization::Encoding, format: crate::serialization::PrivateFormat, encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, + comment: Option<&pyo3::Bound<'p, pyo3::types::PyBytes>>, ) -> CryptographyResult> { + // Intercept raw requests and route them to private_bytes_raw: + if encoding == crate::serialization::Encoding::Raw + && format == crate::serialization::PrivateFormat::Raw + && encryption_algorithm.is_instance(&crate::types::NO_ENCRYPTION.get(py)?)? + { + return slf.borrow().private_bytes_raw(py); + } + utils::pkey_private_bytes( py, slf, @@ -113,7 +125,8 @@ impl X448PrivateKey { format, encryption_algorithm, false, - true, + false, + None, ) } diff --git a/tests/doubles.py b/tests/doubles.py index d4156df80906..8855d9d9247e 100644 --- a/tests/doubles.py +++ b/tests/doubles.py @@ -127,6 +127,8 @@ def private_bytes( encoding: serialization.Encoding, format: serialization.PrivateFormat, encryption_algorithm: serialization.KeySerializationEncryption, + *, + comment: bytes | None = None, ) -> bytes: raise NotImplementedError diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 6c280c10acc2..1f5733e0d26f 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1542,6 +1542,64 @@ def test_pkcs8_key_with_wrong_pem_delimiter(self): assert len(exc_info.value.__notes__) == 1 assert "PKCS#8 format" in exc_info.value.__notes__[0] + def test_ssh_private_key_round_trip_with_comment(self): + # Generate a test Ed25519 private key + key = ed25519.Ed25519PrivateKey.generate() + custom_comment = b"developer@workstation-victus7" + + # 1. Test serialization with comment + pem_data = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=custom_comment, + ) + + # 2. Test deserialization with load_ssh_private_key_with_comment + loaded_key, comment = serialization.load_ssh_private_key_with_comment( + pem_data, password=None + ) + + # Verify key type and comment match + assert isinstance(loaded_key, ed25519.Ed25519PrivateKey) + assert comment == custom_comment + + # 3. Test backward-compatible load_ssh_private_key wrapper still works + compat_loaded_key = serialization.load_ssh_private_key( + pem_data, password=None + ) + assert isinstance(compat_loaded_key, ed25519.Ed25519PrivateKey) + + def test_rsa_openssh_private_key_with_comment(self): + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=b"my-rsa-comment", + ) + assert pem.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") + + def test_dsa_openssh_private_key_with_comment(self): + key = dsa.generate_private_key(key_size=1024) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=b"my-dsa-comment", + ) + assert pem.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") + + def test_ecdsa_openssh_private_key_with_comment(self): + key = ec.generate_private_key(ec.SECP256R1()) + pem = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=b"my-ec-comment", + ) + assert pem.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") + class TestKeySerializationEncryptionTypes: def test_non_bytes_password(self): @@ -1727,6 +1785,24 @@ def test_openssh_serialization_unsupported(self): Encoding.PEM, PrivateFormat.OpenSSH, NoEncryption() ) + def test_ssh_private_key_with_comment(self) -> None: + key = ed25519.Ed25519PrivateKey.generate() + custom_comment = b"test-user@host" + + serialized = key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=custom_comment, + ) + + loaded_key, comment = serialization.load_ssh_private_key_with_comment( + serialized, password=None + ) + + assert comment == custom_comment + assert isinstance(loaded_key, ed25519.Ed25519PrivateKey) + @pytest.mark.supported( only_if=lambda backend: backend.x25519_supported(),