Repository: pyca/cryptography

Issue:
RFE: remove password length limit on `OpenSSH` encoded private keys
### Summary

The current implementation of `SSH` serialization enforces a [maximum password length](https://github.com/pyca/cryptography/blob/main/src/cryptography/hazmat/primitives/serialization/ssh.py#L56) of 72 characters and [references](https://github.com/pyca/cryptography/blob/28934d16fcad8de97284483aeec5d4c2fffab30d/src/cryptography/hazmat/primitives/serialization/ssh.py#L614-L615) the OpenSSH format as a source.

The [OpenSSH format](https://github.com/openssh/openssh-portable/blob/eb679e2959bdb15454eb94751930eb4c9110da94/PROTOCOL.key) however specifies no such restriction since the passphrase is never stored and is supplied directly to the underlying KDF implementation.

`bcrypt 3.1.5`, required by `cryptography`, does not specify any requirements for the password either (besides [non-zero length](https://github.com/pyca/bcrypt/blob/3.1.5/src/_csrc/bcrypt_pbkdf.c)).

Originally reported [here](https://github.com/ansible-collections/community.crypto/issues/492)


Discussion:
Huh. The original bcrypt algorithmis limited to 72 characters, but this limitation does not apply to bcrypt_pbkdf2 (which is what's used in the OpenSSH format). It looks like this limitation was enforced by accident.

We'd be happy to accept a PR with tests that removed this limitation.
Hi, I'm the OP of the Ansible's issue. I'm afraid removing this limitation is out of my reach :/
I hope a well-meaning soul will be able a submit a PR someday! 🥇 Regards,

Resolving patch:
diff --git a/src/cryptography/hazmat/primitives/serialization/ssh.py b/src/cryptography/hazmat/primitives/serialization/ssh.py
--- a/src/cryptography/hazmat/primitives/serialization/ssh.py
+++ b/src/cryptography/hazmat/primitives/serialization/ssh.py
@@ -53,7 +53,6 @@ def _bcrypt_kdf(
 _NONE = b"none"
 _DEFAULT_CIPHER = b"aes256-ctr"
 _DEFAULT_ROUNDS = 16
-_MAX_PASSWORD = 72
 
 # re is only way to work on bytes-like data
 _PEM_RC = re.compile(_SK_START + b"(.*?)" + _SK_END, re.DOTALL)
@@ -609,11 +608,6 @@ def serialize_ssh_private_key(
     """Serialize private key with OpenSSH custom encoding."""
     if password is not None:
         utils._check_bytes("password", password)
-    if password and len(password) > _MAX_PASSWORD:
-        raise ValueError(
-            "Passwords longer than 72 bytes are not supported by "
-            "OpenSSH private key format"
-        )
 
     if isinstance(private_key, ec.EllipticCurvePrivateKey):
         key_type = _ecdsa_key_type(private_key.public_key())


Test patch:
diff --git a/tests/hazmat/primitives/test_serialization.py b/tests/hazmat/primitives/test_serialization.py
--- a/tests/hazmat/primitives/test_serialization.py
+++ b/tests/hazmat/primitives/test_serialization.py
@@ -2342,14 +2342,6 @@ def test_serialize_ssh_private_key_errors(self, backend):
 
         private_key = ec.generate_private_key(ec.SECP256R1(), backend)
 
-        # too long password
-        with pytest.raises(ValueError):
-            private_key.private_bytes(
-                Encoding.PEM,
-                PrivateFormat.OpenSSH,
-                BestAvailableEncryption(b"p" * 73),
-            )
-
         # unknown encryption class
         with pytest.raises(ValueError):
             private_key.private_bytes(
@@ -2358,6 +2350,41 @@ def test_serialize_ssh_private_key_errors(self, backend):
                 DummyKeySerializationEncryption(),
             )
 
+    @pytest.mark.supported(
+        only_if=lambda backend: ssh._bcrypt_supported,
+        skip_message="Requires that bcrypt exists",
+    )
+    @pytest.mark.parametrize(
+        "password",
+        (
+            b"1234",
+            b"p@ssw0rd",
+            b"x" * 100,
+        ),
+    )
+    def test_serialize_ssh_private_key_with_password(self, password, backend):
+        original_key = ec.generate_private_key(ec.SECP256R1(), backend)
+        encoded_key_data = ssh.serialize_ssh_private_key(
+            private_key=original_key,
+            password=password,
+        )
+
+        decoded_key = load_ssh_private_key(
+            data=encoded_key_data,
+            password=password,
+            backend=backend,
+        )
+
+        original_public_key = original_key.public_key().public_bytes(
+            Encoding.OpenSSH, PublicFormat.OpenSSH
+        )
+
+        decoded_public_key = decoded_key.public_key().public_bytes(
+            Encoding.OpenSSH, PublicFormat.OpenSSH
+        )
+
+        assert original_public_key == decoded_public_key
+
     @pytest.mark.supported(
         only_if=lambda backend: backend.dsa_supported(),
         skip_message="Does not support DSA.",