Fix writing pem files with Python3 (#3757)

* Standardize arguments name for mode and chmod in the util API

* Handle OpenSSL pem as bytes objects only for Python3 compatibility

* Handle OpenSSL pem as bytes objects only (remaining bits)

* Manipulate bytes objects only when testing PEM-related functions

* Fix argument order when calling util.unique_file
This commit is contained in:
kaiyou
2016-11-07 14:22:20 -08:00
committed by Peter Eckersley
parent 549f0eccf9
commit 82c69815d1
7 changed files with 38 additions and 34 deletions
+2 -1
View File
@@ -405,7 +405,8 @@ class NginxConfigurator(common.Plugin):
cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()]) cert = acme_crypto_util.gen_ss_cert(key, domains=[socket.gethostname()])
cert_pem = OpenSSL.crypto.dump_certificate( cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert) OpenSSL.crypto.FILETYPE_PEM, cert)
cert_file, cert_path = util.unique_file(os.path.join(tmp_dir, "cert.pem")) cert_file, cert_path = util.unique_file(
os.path.join(tmp_dir, "cert.pem"), mode="wb")
with cert_file: with cert_file:
cert_file.write(cert_pem) cert_file.write(cert_pem)
return cert_path, le_key.file return cert_path, le_key.file
+3 -3
View File
@@ -322,7 +322,7 @@ class Client(object):
self.config.strict_permissions) self.config.strict_permissions)
cert_pem = OpenSSL.crypto.dump_certificate( cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped).decode('ascii') OpenSSL.crypto.FILETYPE_PEM, certr.body.wrapped)
cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path) cert_file, abs_cert_path = _open_pem_file('cert_path', cert_path)
@@ -595,10 +595,10 @@ def _open_pem_file(cli_arg_path, pem_path):
""" """
if cli.set_by_cli(cli_arg_path): if cli.set_by_cli(cli_arg_path):
return util.safe_open(pem_path, chmod=0o644),\ return util.safe_open(pem_path, chmod=0o644, mode="wb"),\
os.path.abspath(pem_path) os.path.abspath(pem_path)
else: else:
uniq = util.unique_file(pem_path, 0o644) uniq = util.unique_file(pem_path, 0o644, "wb")
return uniq[0], os.path.abspath(uniq[1]) return uniq[0], os.path.abspath(uniq[1])
def _save_chain(chain_pem, chain_file): def _save_chain(chain_pem, chain_file):
+5 -4
View File
@@ -53,7 +53,8 @@ def init_save_key(key_size, key_dir, keyname="key-certbot.pem"):
# Save file # Save file
util.make_or_verify_dir(key_dir, 0o700, os.geteuid(), util.make_or_verify_dir(key_dir, 0o700, os.geteuid(),
config.strict_permissions) config.strict_permissions)
key_f, key_path = util.unique_file(os.path.join(key_dir, keyname), 0o600) key_f, key_path = util.unique_file(
os.path.join(key_dir, keyname), 0o600, "wb")
with key_f: with key_f:
key_f.write(key_pem) key_f.write(key_pem)
@@ -85,7 +86,7 @@ def init_save_csr(privkey, names, path, csrname="csr-certbot.pem"):
util.make_or_verify_dir(path, 0o755, os.geteuid(), util.make_or_verify_dir(path, 0o755, os.geteuid(),
config.strict_permissions) config.strict_permissions)
csr_f, csr_filename = util.unique_file( csr_f, csr_filename = util.unique_file(
os.path.join(path, csrname), 0o644) os.path.join(path, csrname), 0o644, "wb")
csr_f.write(csr_pem) csr_f.write(csr_pem)
csr_f.close() csr_f.close()
@@ -351,11 +352,11 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
if isinstance(cert, jose.ComparableX509): if isinstance(cert, jose.ComparableX509):
# pylint: disable=protected-access # pylint: disable=protected-access
cert = cert.wrapped cert = cert.wrapped
return OpenSSL.crypto.dump_certificate(filetype, cert).decode('ascii') return OpenSSL.crypto.dump_certificate(filetype, cert)
# assumes that OpenSSL.crypto.dump_certificate includes ending # assumes that OpenSSL.crypto.dump_certificate includes ending
# newline character # newline character
return "".join(_dump_cert(cert) for cert in chain) return b"".join(_dump_cert(cert) for cert in chain)
def notBefore(cert_path): def notBefore(cert_path):
+4 -4
View File
@@ -817,17 +817,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
for kind in ALL_FOUR: for kind in ALL_FOUR:
os.symlink(os.path.join(archive, kind + "1.pem"), os.symlink(os.path.join(archive, kind + "1.pem"),
target[kind]) target[kind])
with open(target["cert"], "w") as f: with open(target["cert"], "wb") as f:
logger.debug("Writing certificate to %s.", target["cert"]) logger.debug("Writing certificate to %s.", target["cert"])
f.write(cert) f.write(cert)
with open(target["privkey"], "w") as f: with open(target["privkey"], "wb") as f:
logger.debug("Writing private key to %s.", target["privkey"]) logger.debug("Writing private key to %s.", target["privkey"])
f.write(privkey) f.write(privkey)
# XXX: Let's make sure to get the file permissions right here # XXX: Let's make sure to get the file permissions right here
with open(target["chain"], "w") as f: with open(target["chain"], "wb") as f:
logger.debug("Writing chain to %s.", target["chain"]) logger.debug("Writing chain to %s.", target["chain"])
f.write(chain) f.write(chain)
with open(target["fullchain"], "w") as f: with open(target["fullchain"], "wb") as f:
# assumes that OpenSSL.crypto.dump_certificate includes # assumes that OpenSSL.crypto.dump_certificate includes
# ending newline character # ending newline character
logger.debug("Writing full chain to %s.", target["fullchain"]) logger.debug("Writing full chain to %s.", target["fullchain"])
+4 -4
View File
@@ -40,9 +40,9 @@ class InitSaveKeyTest(unittest.TestCase):
@mock.patch('certbot.crypto_util.make_key') @mock.patch('certbot.crypto_util.make_key')
def test_success(self, mock_make): def test_success(self, mock_make):
mock_make.return_value = 'key_pem' mock_make.return_value = b'key_pem'
key = self._call(1024, self.key_dir) key = self._call(1024, self.key_dir)
self.assertEqual(key.pem, 'key_pem') self.assertEqual(key.pem, b'key_pem')
self.assertTrue('key-certbot.pem' in key.file) self.assertTrue('key-certbot.pem' in key.file)
@mock.patch('certbot.crypto_util.make_key') @mock.patch('certbot.crypto_util.make_key')
@@ -67,13 +67,13 @@ class InitSaveCSRTest(unittest.TestCase):
def test_it(self, unused_mock_verify, mock_csr): def test_it(self, unused_mock_verify, mock_csr):
from certbot.crypto_util import init_save_csr from certbot.crypto_util import init_save_csr
mock_csr.return_value = ('csr_pem', 'csr_der') mock_csr.return_value = (b'csr_pem', b'csr_der')
csr = init_save_csr( csr = init_save_csr(
mock.Mock(pem='dummy_key'), 'example.com', self.csr_dir, mock.Mock(pem='dummy_key'), 'example.com', self.csr_dir,
'csr-certbot.pem') 'csr-certbot.pem')
self.assertEqual(csr.data, 'csr_der') self.assertEqual(csr.data, b'csr_der')
self.assertTrue('csr-certbot.pem' in csr.file) self.assertTrue('csr-certbot.pem' in csr.file)
+9 -9
View File
@@ -573,18 +573,18 @@ class RenewableCertTests(BaseRenewableCertTest):
from certbot import storage from certbot import storage
result = storage.RenewableCert.new_lineage( result = storage.RenewableCert.new_lineage(
"the-lineage.com", "cert", "privkey", "chain", self.cli_config) "the-lineage.com", b"cert", b"privkey", b"chain", self.cli_config)
# This consistency check tests most relevant properties about the # This consistency check tests most relevant properties about the
# newly created cert lineage. # newly created cert lineage.
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertTrue(result._consistent()) self.assertTrue(result._consistent())
self.assertTrue(os.path.exists(os.path.join( self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
with open(result.fullchain) as f: with open(result.fullchain, "rb") as f:
self.assertEqual(f.read(), "cert" + "chain") self.assertEqual(f.read(), b"cert" + b"chain")
# Let's do it again and make sure it makes a different lineage # Let's do it again and make sure it makes a different lineage
result = storage.RenewableCert.new_lineage( result = storage.RenewableCert.new_lineage(
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
self.assertTrue(os.path.exists(os.path.join( self.assertTrue(os.path.exists(os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf"))) self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
# Now trigger the detection of already existing files # Now trigger the detection of already existing files
@@ -592,15 +592,15 @@ class RenewableCertTests(BaseRenewableCertTest):
self.cli_config.live_dir, "the-lineage.com-0002")) self.cli_config.live_dir, "the-lineage.com-0002"))
self.assertRaises(errors.CertStorageError, self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage, "the-lineage.com", storage.RenewableCert.new_lineage, "the-lineage.com",
"cert3", "privkey3", "chain3", self.cli_config) b"cert3", b"privkey3", b"chain3", self.cli_config)
os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com")) os.mkdir(os.path.join(self.cli_config.default_archive_dir, "other-example.com"))
self.assertRaises(errors.CertStorageError, self.assertRaises(errors.CertStorageError,
storage.RenewableCert.new_lineage, storage.RenewableCert.new_lineage,
"other-example.com", "cert4", "other-example.com", b"cert4",
"privkey4", "chain4", self.cli_config) b"privkey4", b"chain4", self.cli_config)
# Make sure it can accept renewal parameters # Make sure it can accept renewal parameters
result = storage.RenewableCert.new_lineage( result = storage.RenewableCert.new_lineage(
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
# TODO: Conceivably we could test that the renewal parameters actually # TODO: Conceivably we could test that the renewal parameters actually
# got saved # got saved
@@ -617,7 +617,7 @@ class RenewableCertTests(BaseRenewableCertTest):
shutil.rmtree(self.cli_config.live_dir) shutil.rmtree(self.cli_config.live_dir)
storage.RenewableCert.new_lineage( storage.RenewableCert.new_lineage(
"the-lineage.com", "cert2", "privkey2", "chain2", self.cli_config) "the-lineage.com", b"cert2", b"privkey2", b"chain2", self.cli_config)
self.assertTrue(os.path.exists( self.assertTrue(os.path.exists(
os.path.join( os.path.join(
self.cli_config.renewal_configs_dir, "the-lineage.com.conf"))) self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
+11 -9
View File
@@ -151,11 +151,11 @@ def safe_open(path, mode="w", chmod=None, buffering=None):
mode, *fdopen_args) mode, *fdopen_args)
def _unique_file(path, filename_pat, count, mode): def _unique_file(path, filename_pat, count, chmod, mode):
while True: while True:
current_path = os.path.join(path, filename_pat(count)) current_path = os.path.join(path, filename_pat(count))
try: try:
return safe_open(current_path, chmod=mode),\ return safe_open(current_path, chmod=chmod, mode=mode),\
os.path.abspath(current_path) os.path.abspath(current_path)
except OSError as err: except OSError as err:
# "File exists," is okay, try a different name. # "File exists," is okay, try a different name.
@@ -164,11 +164,12 @@ def _unique_file(path, filename_pat, count, mode):
count += 1 count += 1
def unique_file(path, mode=0o777): def unique_file(path, chmod=0o777, mode="w"):
"""Safely finds a unique file. """Safely finds a unique file.
:param str path: path/filename.ext :param str path: path/filename.ext
:param int mode: File mode :param int chmod: File mode
:param str mode: Open mode
:returns: tuple of file object and file name :returns: tuple of file object and file name
@@ -176,15 +177,16 @@ def unique_file(path, mode=0o777):
path, tail = os.path.split(path) path, tail = os.path.split(path)
return _unique_file( return _unique_file(
path, filename_pat=(lambda count: "%04d_%s" % (count, tail)), path, filename_pat=(lambda count: "%04d_%s" % (count, tail)),
count=0, mode=mode) count=0, chmod=chmod, mode=mode)
def unique_lineage_name(path, filename, mode=0o777): def unique_lineage_name(path, filename, chmod=0o777, mode="w"):
"""Safely finds a unique file using lineage convention. """Safely finds a unique file using lineage convention.
:param str path: directory path :param str path: directory path
:param str filename: proposed filename :param str filename: proposed filename
:param int mode: file mode :param int chmod: file mode
:param str mode: open mode
:returns: tuple of file object and file name (which may be modified :returns: tuple of file object and file name (which may be modified
from the requested one by appending digits to ensure uniqueness) from the requested one by appending digits to ensure uniqueness)
@@ -196,13 +198,13 @@ def unique_lineage_name(path, filename, mode=0o777):
""" """
preferred_path = os.path.join(path, "%s.conf" % (filename)) preferred_path = os.path.join(path, "%s.conf" % (filename))
try: try:
return safe_open(preferred_path, chmod=mode), preferred_path return safe_open(preferred_path, chmod=chmod), preferred_path
except OSError as err: except OSError as err:
if err.errno != errno.EEXIST: if err.errno != errno.EEXIST:
raise raise
return _unique_file( return _unique_file(
path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)), path, filename_pat=(lambda count: "%s-%04d.conf" % (filename, count)),
count=1, mode=mode) count=1, chmod=chmod, mode=mode)
def safely_remove(path): def safely_remove(path):