From 03bf4cd3aa3543d3770ee6fef94cc3442b5d7d7d Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Sun, 11 Oct 2026 12:18:22 +0000 Subject: [PATCH 1/5] feat(serialization): add support for OpenSSH private key comments --- .../hazmat/primitives/asymmetric/dh.py | 2 + .../hazmat/primitives/asymmetric/dsa.py | 2 + .../hazmat/primitives/asymmetric/ec.py | 2 + .../hazmat/primitives/asymmetric/ed25519.py | 2 + .../hazmat/primitives/asymmetric/ed448.py | 2 + .../hazmat/primitives/asymmetric/mldsa.py | 2 + .../hazmat/primitives/asymmetric/mlkem.py | 2 + .../hazmat/primitives/asymmetric/rsa.py | 2 + .../hazmat/primitives/asymmetric/x25519.py | 2 + .../hazmat/primitives/asymmetric/x448.py | 2 + .../primitives/serialization/__init__.py | 2 + .../hazmat/primitives/serialization/ssh.py | 43 +++++++++++++---- src/rust/src/backend/dh.rs | 9 ++++ src/rust/src/backend/dsa.rs | 9 ++++ src/rust/src/backend/ec.rs | 9 ++++ src/rust/src/backend/ed25519.rs | 9 ++++ src/rust/src/backend/ed448.rs | 9 ++++ src/rust/src/backend/mldsa.rs | 28 +++++++++++ src/rust/src/backend/mlkem.rs | 19 ++++++++ src/rust/src/backend/rsa.rs | 9 ++++ src/rust/src/backend/utils.rs | 3 +- src/rust/src/backend/x25519.rs | 9 ++++ src/rust/src/backend/x448.rs | 9 ++++ tests/hazmat/primitives/test_serialization.py | 48 +++++++++++++++++++ 24 files changed, 225 insertions(+), 10 deletions(-) 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..0da554b47ae8 100644 --- a/src/rust/src/backend/dh.rs +++ b/src/rust/src/backend/dh.rs @@ -4,6 +4,7 @@ use cryptography_x509::common; use pyo3::types::PyAnyMethods; +use pyo3::types::PyBytesMethods; use crate::asn1::encode_der_data; use crate::backend::utils; @@ -263,12 +264,14 @@ impl DHPrivateKey { }) } + #[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> { if format != crate::serialization::PrivateFormat::PKCS8 { return Err(CryptographyError::from( @@ -278,6 +281,11 @@ impl DHPrivateKey { )); } + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -287,6 +295,7 @@ impl DHPrivateKey { encryption_algorithm, true, false, + comment_bytes, ) } diff --git a/src/rust/src/backend/dsa.rs b/src/rust/src/backend/dsa.rs index 56363f49366c..99bf3a3dc7dd 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, + comment_bytes, ) } diff --git a/src/rust/src/backend/ec.rs b/src/rust/src/backend/ec.rs index ca215ba1637a..3f6c846e9018 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, + comment_bytes, ) } diff --git a/src/rust/src/backend/ed25519.rs b/src/rust/src/backend/ed25519.rs index c58be6a68b51..b9cec9f01fbe 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, + comment_bytes, ) } diff --git a/src/rust/src/backend/ed448.rs b/src/rust/src/backend/ed448.rs index f5a31ff0bffb..22c7b1f3ee0a 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::PyBytesMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.ed448")] pub(crate) struct Ed448PrivateKey { @@ -98,13 +99,20 @@ impl Ed448PrivateKey { 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, @@ -114,6 +122,7 @@ impl Ed448PrivateKey { encryption_algorithm, true, true, + comment_bytes, ) } diff --git a/src/rust/src/backend/mldsa.rs b/src/rust/src/backend/mldsa.rs index 0fe421f472b8..fa4250196ca4 100644 --- a/src/rust/src/backend/mldsa.rs +++ b/src/rust/src/backend/mldsa.rs @@ -4,6 +4,7 @@ use cryptography_openssl::mldsa::MlDsaVariant; use pyo3::types::PyAnyMethods; +use pyo3::types::PyBytesMethods; use crate::backend::utils; use crate::buf::CffiBuf; @@ -130,12 +131,14 @@ impl MlDsa44PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[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> { if encoding == crate::serialization::Encoding::Raw && format == crate::serialization::PrivateFormat::Raw @@ -143,6 +146,12 @@ impl MlDsa44PrivateKey { { return slf.borrow().private_bytes_raw(py); } + + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -152,6 +161,7 @@ impl MlDsa44PrivateKey { encryption_algorithm, true, false, + comment_bytes, ) } @@ -377,12 +387,14 @@ impl MlDsa65PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[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> { // Intercept Raw/Raw/NoEncryption so we return the seed. // The generic pkey_private_bytes raw path calls raw_private_key() @@ -393,6 +405,12 @@ impl MlDsa65PrivateKey { { return slf.borrow().private_bytes_raw(py); } + + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -402,6 +420,7 @@ impl MlDsa65PrivateKey { encryption_algorithm, true, false, + comment_bytes, ) } @@ -627,12 +646,14 @@ impl MlDsa87PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[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> { if encoding == crate::serialization::Encoding::Raw && format == crate::serialization::PrivateFormat::Raw @@ -640,6 +661,12 @@ impl MlDsa87PrivateKey { { return slf.borrow().private_bytes_raw(py); } + + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -649,6 +676,7 @@ impl MlDsa87PrivateKey { encryption_algorithm, true, false, + comment_bytes, ) } diff --git a/src/rust/src/backend/mlkem.rs b/src/rust/src/backend/mlkem.rs index 42195dbc2559..2e3f13f652ba 100644 --- a/src/rust/src/backend/mlkem.rs +++ b/src/rust/src/backend/mlkem.rs @@ -4,6 +4,7 @@ use cryptography_openssl::mlkem::MlKemVariant; use pyo3::types::PyAnyMethods; +use pyo3::types::PyBytesMethods; use crate::backend::utils; use crate::buf::CffiBuf; @@ -127,12 +128,14 @@ impl MlKem768PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[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> { // Intercept Raw/Raw/NoEncryption so we return the seed. // The generic pkey_private_bytes raw path calls raw_private_key() @@ -143,6 +146,12 @@ impl MlKem768PrivateKey { { return slf.borrow().private_bytes_raw(py); } + + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -152,6 +161,7 @@ impl MlKem768PrivateKey { encryption_algorithm, true, false, + comment_bytes, ) } @@ -277,12 +287,14 @@ impl MlKem1024PrivateKey { Ok(pyo3::types::PyBytes::new(py, &seed)) } + #[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> { // Intercept Raw/Raw/NoEncryption so we return the seed. // The generic pkey_private_bytes raw path calls raw_private_key() @@ -293,6 +305,12 @@ impl MlKem1024PrivateKey { { return slf.borrow().private_bytes_raw(py); } + + let comment_bytes = match comment { + Some(b) => b.as_bytes(), + None => b"", + }; + utils::pkey_private_bytes( py, slf, @@ -302,6 +320,7 @@ impl MlKem1024PrivateKey { encryption_algorithm, true, false, + comment_bytes, ) } diff --git a/src/rust/src/backend/rsa.rs b/src/rust/src/backend/rsa.rs index 86bc1a05b3e8..352f303289db 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, + comment_bytes, ) } diff --git a/src/rust/src/backend/utils.rs b/src/rust/src/backend/utils.rs index f8a6369a4292..fe99aa66aebc 100644 --- a/src/rust/src/backend/utils.rs +++ b/src/rust/src/backend/utils.rs @@ -48,6 +48,7 @@ pub(crate) fn pkey_private_bytes<'p>( encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, openssh_allowed: bool, raw_allowed: bool, + comment: &[u8], ) -> CryptographyResult> { if !encryption_algorithm.is_instance(&types::KEY_SERIALIZATION_ENCRYPTION.get(py)?)? { return Err(CryptographyError::from( @@ -193,7 +194,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..47b7ba6ee40e 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::PyBytesMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.x25519")] pub(crate) struct X25519PrivateKey { @@ -99,13 +100,20 @@ impl X25519PrivateKey { 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, @@ -115,6 +123,7 @@ impl X25519PrivateKey { encryption_algorithm, false, true, + comment_bytes, ) } diff --git a/src/rust/src/backend/x448.rs b/src/rust/src/backend/x448.rs index 9374d8753f2f..b0a78a9db949 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::PyBytesMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.x448")] pub(crate) struct X448PrivateKey { @@ -98,13 +99,20 @@ impl X448PrivateKey { 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, @@ -114,6 +122,7 @@ impl X448PrivateKey { encryption_algorithm, false, true, + comment_bytes, ) } diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 6c280c10acc2..f9a4ac96f741 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1542,6 +1542,34 @@ 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) + class TestKeySerializationEncryptionTypes: def test_non_bytes_password(self): @@ -1727,6 +1755,26 @@ 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(), From 7349903fce278a99573d80c330c875cc8b6c759c Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Sun, 11 Oct 2026 17:24:56 +0000 Subject: [PATCH 2/5] feat(rust): add optional OpenSSH private key comment support --- src/rust/src/backend/dh.rs | 11 ++----- src/rust/src/backend/dsa.rs | 2 +- src/rust/src/backend/ec.rs | 2 +- src/rust/src/backend/ed25519.rs | 2 +- src/rust/src/backend/ed448.rs | 20 +++++++----- src/rust/src/backend/mldsa.rs | 31 ++++++------------- src/rust/src/backend/mlkem.rs | 21 ++++--------- src/rust/src/backend/rsa.rs | 2 +- src/rust/src/backend/utils.rs | 3 +- src/rust/src/backend/x25519.rs | 18 ++++++----- src/rust/src/backend/x448.rs | 18 ++++++----- tests/doubles.py | 2 ++ tests/hazmat/primitives/test_serialization.py | 6 ++-- 13 files changed, 62 insertions(+), 76 deletions(-) diff --git a/src/rust/src/backend/dh.rs b/src/rust/src/backend/dh.rs index 0da554b47ae8..f123333230d9 100644 --- a/src/rust/src/backend/dh.rs +++ b/src/rust/src/backend/dh.rs @@ -4,7 +4,6 @@ use cryptography_x509::common; use pyo3::types::PyAnyMethods; -use pyo3::types::PyBytesMethods; use crate::asn1::encode_der_data; use crate::backend::utils; @@ -265,6 +264,7 @@ 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>, @@ -281,11 +281,6 @@ impl DHPrivateKey { )); } - let comment_bytes = match comment { - Some(b) => b.as_bytes(), - None => b"", - }; - utils::pkey_private_bytes( py, slf, @@ -293,9 +288,9 @@ impl DHPrivateKey { encoding, format, encryption_algorithm, - true, false, - comment_bytes, + false, + None, ) } diff --git a/src/rust/src/backend/dsa.rs b/src/rust/src/backend/dsa.rs index 99bf3a3dc7dd..88bc451cb507 100644 --- a/src/rust/src/backend/dsa.rs +++ b/src/rust/src/backend/dsa.rs @@ -174,7 +174,7 @@ impl DsaPrivateKey { encryption_algorithm, true, false, - comment_bytes, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/ec.rs b/src/rust/src/backend/ec.rs index 3f6c846e9018..2c2a436c1b99 100644 --- a/src/rust/src/backend/ec.rs +++ b/src/rust/src/backend/ec.rs @@ -364,7 +364,7 @@ impl ECPrivateKey { encryption_algorithm, true, false, - comment_bytes, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/ed25519.rs b/src/rust/src/backend/ed25519.rs index b9cec9f01fbe..1ad0785224d4 100644 --- a/src/rust/src/backend/ed25519.rs +++ b/src/rust/src/backend/ed25519.rs @@ -124,7 +124,7 @@ impl Ed25519PrivateKey { encryption_algorithm, true, true, - comment_bytes, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/ed448.rs b/src/rust/src/backend/ed448.rs index 22c7b1f3ee0a..2e89b801b84f 100644 --- a/src/rust/src/backend/ed448.rs +++ b/src/rust/src/backend/ed448.rs @@ -6,7 +6,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::{CryptographyError, CryptographyResult}; use crate::exceptions; -use pyo3::types::PyBytesMethods; +use pyo3::types::PyAnyMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.ed448")] pub(crate) struct Ed448PrivateKey { @@ -100,6 +100,7 @@ impl Ed448PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -108,10 +109,13 @@ impl Ed448PrivateKey { 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"", - }; + // 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, @@ -120,9 +124,9 @@ impl Ed448PrivateKey { encoding, format, encryption_algorithm, - true, - true, - comment_bytes, + false, + false, + None, ) } diff --git a/src/rust/src/backend/mldsa.rs b/src/rust/src/backend/mldsa.rs index fa4250196ca4..8af6281d0ccd 100644 --- a/src/rust/src/backend/mldsa.rs +++ b/src/rust/src/backend/mldsa.rs @@ -4,7 +4,6 @@ use cryptography_openssl::mldsa::MlDsaVariant; use pyo3::types::PyAnyMethods; -use pyo3::types::PyBytesMethods; use crate::backend::utils; use crate::buf::CffiBuf; @@ -132,6 +131,7 @@ impl MlDsa44PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -147,11 +147,6 @@ impl MlDsa44PrivateKey { return slf.borrow().private_bytes_raw(py); } - let comment_bytes = match comment { - Some(b) => b.as_bytes(), - None => b"", - }; - utils::pkey_private_bytes( py, slf, @@ -159,9 +154,9 @@ impl MlDsa44PrivateKey { encoding, format, encryption_algorithm, - true, false, - comment_bytes, + false, + None, ) } @@ -388,6 +383,7 @@ impl MlDsa65PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -406,11 +402,6 @@ impl MlDsa65PrivateKey { return slf.borrow().private_bytes_raw(py); } - let comment_bytes = match comment { - Some(b) => b.as_bytes(), - None => b"", - }; - utils::pkey_private_bytes( py, slf, @@ -418,9 +409,9 @@ impl MlDsa65PrivateKey { encoding, format, encryption_algorithm, - true, false, - comment_bytes, + false, + None, ) } @@ -647,6 +638,7 @@ impl MlDsa87PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -662,11 +654,6 @@ impl MlDsa87PrivateKey { return slf.borrow().private_bytes_raw(py); } - let comment_bytes = match comment { - Some(b) => b.as_bytes(), - None => b"", - }; - utils::pkey_private_bytes( py, slf, @@ -674,9 +661,9 @@ impl MlDsa87PrivateKey { encoding, format, encryption_algorithm, - true, false, - comment_bytes, + false, + None, ) } diff --git a/src/rust/src/backend/mlkem.rs b/src/rust/src/backend/mlkem.rs index 2e3f13f652ba..fe1557daa13a 100644 --- a/src/rust/src/backend/mlkem.rs +++ b/src/rust/src/backend/mlkem.rs @@ -4,7 +4,6 @@ use cryptography_openssl::mlkem::MlKemVariant; use pyo3::types::PyAnyMethods; -use pyo3::types::PyBytesMethods; use crate::backend::utils; use crate::buf::CffiBuf; @@ -129,6 +128,7 @@ impl MlKem768PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -147,11 +147,6 @@ impl MlKem768PrivateKey { return slf.borrow().private_bytes_raw(py); } - let comment_bytes = match comment { - Some(b) => b.as_bytes(), - None => b"", - }; - utils::pkey_private_bytes( py, slf, @@ -159,9 +154,9 @@ impl MlKem768PrivateKey { encoding, format, encryption_algorithm, - true, false, - comment_bytes, + false, + None, ) } @@ -288,6 +283,7 @@ impl MlKem1024PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -306,11 +302,6 @@ impl MlKem1024PrivateKey { return slf.borrow().private_bytes_raw(py); } - let comment_bytes = match comment { - Some(b) => b.as_bytes(), - None => b"", - }; - utils::pkey_private_bytes( py, slf, @@ -318,9 +309,9 @@ impl MlKem1024PrivateKey { encoding, format, encryption_algorithm, - true, false, - comment_bytes, + false, + None, ) } diff --git a/src/rust/src/backend/rsa.rs b/src/rust/src/backend/rsa.rs index 352f303289db..372527d1318d 100644 --- a/src/rust/src/backend/rsa.rs +++ b/src/rust/src/backend/rsa.rs @@ -447,7 +447,7 @@ impl RsaPrivateKey { encryption_algorithm, true, false, - comment_bytes, + Some(comment_bytes), ) } diff --git a/src/rust/src/backend/utils.rs b/src/rust/src/backend/utils.rs index fe99aa66aebc..d8405b870174 100644 --- a/src/rust/src/backend/utils.rs +++ b/src/rust/src/backend/utils.rs @@ -48,8 +48,9 @@ pub(crate) fn pkey_private_bytes<'p>( encryption_algorithm: &pyo3::Bound<'p, pyo3::PyAny>, openssh_allowed: bool, raw_allowed: bool, - comment: &[u8], + 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( diff --git a/src/rust/src/backend/x25519.rs b/src/rust/src/backend/x25519.rs index 47b7ba6ee40e..85f8cb17a3b8 100644 --- a/src/rust/src/backend/x25519.rs +++ b/src/rust/src/backend/x25519.rs @@ -5,7 +5,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::CryptographyResult; -use pyo3::types::PyBytesMethods; +use pyo3::types::PyAnyMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.x25519")] pub(crate) struct X25519PrivateKey { @@ -101,6 +101,7 @@ impl X25519PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -109,10 +110,13 @@ impl X25519PrivateKey { 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"", - }; + // 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, @@ -122,8 +126,8 @@ impl X25519PrivateKey { format, encryption_algorithm, false, - true, - comment_bytes, + false, + None, ) } diff --git a/src/rust/src/backend/x448.rs b/src/rust/src/backend/x448.rs index b0a78a9db949..23eaf92c6c9b 100644 --- a/src/rust/src/backend/x448.rs +++ b/src/rust/src/backend/x448.rs @@ -5,7 +5,7 @@ use crate::backend::utils; use crate::buf::CffiBuf; use crate::error::CryptographyResult; -use pyo3::types::PyBytesMethods; +use pyo3::types::PyAnyMethods; #[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.x448")] pub(crate) struct X448PrivateKey { @@ -100,6 +100,7 @@ impl X448PrivateKey { } #[pyo3(signature = (encoding, format, encryption_algorithm, *, comment = None))] + #[allow(unused_variables)] fn private_bytes<'p>( slf: &pyo3::Bound<'p, Self>, py: pyo3::Python<'p>, @@ -108,10 +109,13 @@ impl X448PrivateKey { 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"", - }; + // 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, @@ -121,8 +125,8 @@ impl X448PrivateKey { format, encryption_algorithm, false, - true, - comment_bytes, + 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 f9a4ac96f741..1e65c2828e39 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1766,10 +1766,8 @@ def test_ssh_private_key_with_comment(self) -> None: comment=custom_comment, ) - loaded_key, comment = ( - serialization.load_ssh_private_key_with_comment( - serialized, password=None - ) + loaded_key, comment = serialization.load_ssh_private_key_with_comment( + serialized, password=None ) assert comment == custom_comment From 7a4f389ba5c82a0a48a7650889fea55127c66f84 Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Sun, 11 Oct 2026 17:53:43 +0000 Subject: [PATCH 3/5] test(serialization): add unit tests for OpenSSH private key comments --- tests/hazmat/primitives/test_serialization.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 1e65c2828e39..0b844b2595d4 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1570,6 +1570,36 @@ def test_ssh_private_key_round_trip_with_comment(self): ) 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.SSH, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=b"my-rsa-comment", + ) + assert b"my-rsa-comment" in pem + + def test_dsa_openssh_private_key_with_comment(self): + key = dsa.generate_private_key(key_size=2048) + pem = key.private_bytes( + encoding=serialization.Encoding.SSH, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=b"my-dsa-comment", + ) + assert b"my-dsa-comment" in pem + + def test_ecdsa_openssh_private_key_with_comment(self): + key = ec.generate_private_key(ec.SECP256R1()) + pem = key.private_bytes( + encoding=serialization.Encoding.SSH, + format=serialization.PrivateFormat.OpenSSH, + encryption_algorithm=serialization.NoEncryption(), + comment=b"my-ec-comment", + ) + assert b"my-ec-comment" in pem + class TestKeySerializationEncryptionTypes: def test_non_bytes_password(self): From 8c1d449dd611500d6b94e5fd3db12bdeadf927fb Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Sun, 11 Oct 2026 18:19:16 +0000 Subject: [PATCH 4/5] achieving 100% coverage --- tests/hazmat/primitives/test_serialization.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 0b844b2595d4..3334bdf17a31 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1573,33 +1573,32 @@ def test_ssh_private_key_round_trip_with_comment(self): 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.SSH, + encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.OpenSSH, encryption_algorithm=serialization.NoEncryption(), comment=b"my-rsa-comment", ) - assert b"my-rsa-comment" in pem + assert pem.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") def test_dsa_openssh_private_key_with_comment(self): - key = dsa.generate_private_key(key_size=2048) + key = dsa.generate_private_key(key_size=1024) pem = key.private_bytes( - encoding=serialization.Encoding.SSH, + encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.OpenSSH, encryption_algorithm=serialization.NoEncryption(), comment=b"my-dsa-comment", ) - assert b"my-dsa-comment" in pem + 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.SSH, + encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.OpenSSH, encryption_algorithm=serialization.NoEncryption(), comment=b"my-ec-comment", ) - assert b"my-ec-comment" in pem - + assert pem.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") class TestKeySerializationEncryptionTypes: def test_non_bytes_password(self): From 1bdbad207b9b1c57a12c3d64d84eb2a122812216 Mon Sep 17 00:00:00 2001 From: anishbhujbal7 Date: Sun, 11 Oct 2026 18:24:43 +0000 Subject: [PATCH 5/5] corrected the code format --- tests/hazmat/primitives/test_serialization.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py index 3334bdf17a31..1f5733e0d26f 100644 --- a/tests/hazmat/primitives/test_serialization.py +++ b/tests/hazmat/primitives/test_serialization.py @@ -1600,6 +1600,7 @@ def test_ecdsa_openssh_private_key_with_comment(self): ) assert pem.startswith(b"-----BEGIN OPENSSH PRIVATE KEY-----") + class TestKeySerializationEncryptionTypes: def test_non_bytes_password(self): with pytest.raises(ValueError):