Skip to content

Commit cdd6696

Browse files
authored
crypto._PassphraseHelper: pass non-callable passphrase using callback (#947)
* crypto._PassphraseHelper: pass non-callable passphrase using callback Fixes #945 Before this commit, we would pass a bytes passphrase as a null terminated string. This causes issue when a randomly generated key's first byte is null because OpenSSL rightly determines the key length is 0. This commit modifies the passphrase helper to pass the passphrase via the callback * Update changelog to document bug fix
1 parent 83ef230 commit cdd6696

File tree

3 files changed

+68
-11
lines changed

3 files changed

+68
-11
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ Changes:
3838
- Make verification callback optional in ``Context.set_verify``.
3939
If omitted, OpenSSL's default verification is used.
4040
`#933 <https://github.com/pyca/pyopenssl/pull/933>`_
41-
41+
- Fixed a bug that could truncate or cause a zero-length key error due to a
42+
null byte in private key passphrase in ``OpenSSL.crypto.load_privatekey``
43+
and ``OpenSSL.crypto.dump_privatekey``.
44+
`#947 <https://github.com/pyca/pyopenssl/pull/947>`_
4245

4346
19.1.0 (2019-11-18)
4447
-------------------

src/OpenSSL/crypto.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2788,9 +2788,7 @@ def __init__(self, type, passphrase, more_args=False, truncate=False):
27882788
def callback(self):
27892789
if self._passphrase is None:
27902790
return _ffi.NULL
2791-
elif isinstance(self._passphrase, bytes):
2792-
return _ffi.NULL
2793-
elif callable(self._passphrase):
2791+
elif isinstance(self._passphrase, bytes) or callable(self._passphrase):
27942792
return _ffi.callback("pem_password_cb", self._read_passphrase)
27952793
else:
27962794
raise TypeError(
@@ -2801,9 +2799,7 @@ def callback(self):
28012799
def callback_args(self):
28022800
if self._passphrase is None:
28032801
return _ffi.NULL
2804-
elif isinstance(self._passphrase, bytes):
2805-
return self._passphrase
2806-
elif callable(self._passphrase):
2802+
elif isinstance(self._passphrase, bytes) or callable(self._passphrase):
28072803
return _ffi.NULL
28082804
else:
28092805
raise TypeError(
@@ -2823,12 +2819,15 @@ def raise_if_problem(self, exceptionType=Error):
28232819

28242820
def _read_passphrase(self, buf, size, rwflag, userdata):
28252821
try:
2826-
if self._more_args:
2827-
result = self._passphrase(size, rwflag, userdata)
2822+
if callable(self._passphrase):
2823+
if self._more_args:
2824+
result = self._passphrase(size, rwflag, userdata)
2825+
else:
2826+
result = self._passphrase(rwflag)
28282827
else:
2829-
result = self._passphrase(rwflag)
2828+
result = self._passphrase
28302829
if not isinstance(result, bytes):
2831-
raise ValueError("String expected")
2830+
raise ValueError("Bytes expected")
28322831
if len(result) > size:
28332832
if self._truncate:
28342833
result = result[:size]

tests/test_crypto.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,23 @@ def normalize_privatekey_pem(pem):
516516

517517
encryptedPrivateKeyPEMPassphrase = b"foobar"
518518

519+
cleartextPrivateKeyPEM = """-----BEGIN PRIVATE KEY-----
520+
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAMcRMugJ4kvkOEuT
521+
AvMFr9+3A6+HAB6nKYcXXZz93ube8rJpBZQEfWn73H10dQiQR/a+rhxYEeLy8dPc
522+
UkFcGR9miVkukJ59zex7iySJY76bdBD8gyx1LTKrkCstP2XHKEYqgbj+tm7VzJnY
523+
sQLqoaa5NeyWJnUC3MJympkAS7p3AgMBAAECgYAoBAcNqd75jnjaiETRgVUnTWzK
524+
PgMCJmwsob/JrSa/lhWHU6Exbe2f/mcGOQDFpesxaIcrX3DJBDkkc2d9h/vsfo5v
525+
JLk/rbHoItWxwuY5n5raAPeQPToKpTDxDrL6Ejhgcxd19wNht7/XSrYZ+dq3iU6G
526+
mOEvU2hrnfIW3kwVYQJBAP62G6R0gucNfaKGtHzfR3TN9G/DnCItchF+TxGTtpdh
527+
Cz32MG+7pirT/0xunekmUIp15QHdRy496sVxWTCooLkCQQDIEwXTAwhLNRGFEs5S
528+
jSkxNfTVeNiOzlG8jPBJJDAdlLt1gUqjZWnk9yU+itMSGi/6eeuH2n04FFk+SV/T
529+
7ryvAkB0y0ZDk5VOozX/p2rtc2iNm77A3N4kIdiTQuq4sZXhNgN0pwWwxke8jbcb
530+
8gEAnqwBwWt//locTxHu9TmjgT8pAkEAlbF16B0atXptM02QxT8MlN8z4gxaqu4/
531+
RX2FwpOq1FcVsqMbvwj/o+ouGY8wwRiK0TMrQCf/DFhdNTcc1aqHzQJBAKWtq4LI
532+
uVZjCAuyrqEnt7R1bOiLrar+/ezJPY2z+f2rb1TGr31ztPeFvO3edLw+QdhzwJGp
533+
QKImYzqMe+zkIOQ=
534+
-----END PRIVATE KEY-----
535+
"""
519536

520537
cleartextPublicKeyPEM = b"""-----BEGIN PUBLIC KEY-----
521538
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxszlc+b71LvlLS0ypt/l
@@ -3167,6 +3184,44 @@ def cb(ignored):
31673184
with pytest.raises(ValueError):
31683185
dump_privatekey(FILETYPE_PEM, key, GOOD_CIPHER, cb)
31693186

3187+
def test_dump_privatekey_truncated(self):
3188+
"""
3189+
`crypto.dump_privatekey` should not truncate a passphrase that contains
3190+
a null byte.
3191+
"""
3192+
key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)
3193+
passphrase = b"foo\x00bar"
3194+
truncated_passphrase = passphrase.split(b"\x00", 1)[0]
3195+
3196+
# By dumping with the full passphrase load should raise an error if we
3197+
# try to load using the truncated passphrase. If dump truncated the
3198+
# passphrase, then we WILL load the privatekey and the test fails
3199+
encrypted_key_pem = dump_privatekey(
3200+
FILETYPE_PEM, key, "AES-256-CBC", passphrase
3201+
)
3202+
with pytest.raises(Error):
3203+
load_privatekey(
3204+
FILETYPE_PEM, encrypted_key_pem, truncated_passphrase
3205+
)
3206+
3207+
def test_load_privatekey_truncated(self):
3208+
"""
3209+
`crypto.load_privatekey` should not truncate a passphrase that contains
3210+
a null byte.
3211+
"""
3212+
key = load_privatekey(FILETYPE_PEM, cleartextPrivateKeyPEM)
3213+
passphrase = b"foo\x00bar"
3214+
truncated_passphrase = passphrase.split(b"\x00", 1)[0]
3215+
3216+
# By dumping using the truncated passphrase load should raise an error
3217+
# if we try to load using the full passphrase. If load truncated the
3218+
# passphrase, then we WILL load the privatekey and the test fails
3219+
encrypted_key_pem = dump_privatekey(
3220+
FILETYPE_PEM, key, "AES-256-CBC", truncated_passphrase
3221+
)
3222+
with pytest.raises(Error):
3223+
load_privatekey(FILETYPE_PEM, encrypted_key_pem, passphrase)
3224+
31703225
def test_load_pkcs7_data_pem(self):
31713226
"""
31723227
`load_pkcs7_data` accepts a PKCS#7 string and returns an instance of

0 commit comments

Comments
 (0)