libervia-backend/sat/tools/common/tls.py

141 lines
4.7 KiB
Python

#!/usr/bin/env python3
# SàT: a XMPP client
# Copyright (C) 2009-2020 Jérôme Poisson (goffi@goffi.org)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""TLS handling with twisted"""
from twisted.internet import reactor
from sat.core.log import getLogger
from sat.core import exceptions
from sat.tools import config as tools_config
try:
import OpenSSL
from twisted.internet import ssl
except ImportError:
ssl = None
log = getLogger(__name__)
def getOptionsFromConfig(config, section=""):
options = {}
for option in ('tls_certificate', 'tls_private_key', 'tls_chain'):
options[option] = tools_config.getConfig(config, section, option)
return options
def TLSOptionsCheck(options):
"""Check options coherence if TLS is activated, and update missing values
Must be called only if TLS is activated
"""
if not options["tls_certificate"]:
raise exceptions.ConfigError(
"a TLS certificate is needed to activate HTTPS connection")
if not options["tls_private_key"]:
options["tls_private_key"] = options["tls_certificate"]
def loadCertificates(f):
"""Read a .pem file with a list of certificates
@param f (file): file obj (opened .pem file)
@return (list[OpenSSL.crypto.X509]): list of certificates
@raise OpenSSL.crypto.Error: error while parsing the file
"""
# XXX: didn't found any method to load a .pem file with several certificates
# so the certificates split is done here
certificates = []
buf = []
while True:
line = f.readline()
buf.append(line)
if "-----END CERTIFICATE-----" in line:
certificates.append(
OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, "".join(buf)
)
)
buf = []
elif not line:
log.debug(f"{len(certificates)} certificate(s) found")
return certificates
def loadPKey(f):
"""Read a private key from a .pem file
@param f (file): file obj (opened .pem file)
@return (list[OpenSSL.crypto.PKey]): private key object
@raise OpenSSL.crypto.Error: error while parsing the file
"""
return OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, f.read())
def loadCertificate(f):
"""Read a public certificate from a .pem file
@param f (file): file obj (opened .pem file)
@return (list[OpenSSL.crypto.X509]): public certificate
@raise OpenSSL.crypto.Error: error while parsing the file
"""
return OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, f.read())
def getTLSContextFactory(options):
"""Load TLS certificate and build the context factory needed for listenSSL"""
if ssl is None:
raise ImportError("Python module pyOpenSSL is not installed!")
cert_options = {}
for name, option, method in [
("privateKey", "tls_private_key", loadPKey),
("certificate", "tls_certificate", loadCertificate),
("extraCertChain", "tls_chain", loadCertificates),
]:
path = options[option]
if not path:
assert option == "tls_chain"
continue
log.debug(f"loading {option} from {path}")
try:
with open(path) as f:
cert_options[name] = method(f)
except IOError as e:
raise exceptions.DataError(
f"Error while reading file {path} for option {option}: {e}"
)
except OpenSSL.crypto.Error:
raise exceptions.DataError(
f"Error while parsing file {path} for option {option}, are you sure "
f"it is a valid .pem file?"
)
if (
option == "tls_private_key"
and options["tls_certificate"] == path
):
raise exceptions.ConfigError(
f"You are using the same file for private key and public "
f"certificate, make sure that both a in {path} or use "
f"--tls_private_key option"
)
return ssl.CertificateOptions(**cert_options)