1import sys
2import logging
3import os
4import ssl
5import hashlib
6import random
7
8from Config import config
9from util import helper
10
11
12class CryptConnectionManager:
13    def __init__(self):
14        if sys.platform.startswith("win"):
15            self.openssl_bin = "tools\\openssl\\openssl.exe"
16        elif config.dist_type.startswith("bundle_linux"):
17            self.openssl_bin = "../runtime/bin/openssl"
18        else:
19            self.openssl_bin = "openssl"
20
21        self.openssl_env = {
22            "OPENSSL_CONF": "src/lib/openssl/openssl.cnf",
23            "RANDFILE": config.data_dir + "/openssl-rand.tmp"
24        }
25
26        self.crypt_supported = []  # Supported cryptos
27
28        self.cacert_pem = config.data_dir + "/cacert-rsa.pem"
29        self.cakey_pem = config.data_dir + "/cakey-rsa.pem"
30        self.cert_pem = config.data_dir + "/cert-rsa.pem"
31        self.cert_csr = config.data_dir + "/cert-rsa.csr"
32        self.key_pem = config.data_dir + "/key-rsa.pem"
33
34        self.log = logging.getLogger("CryptConnectionManager")
35        self.log.debug("Version: %s" % ssl.OPENSSL_VERSION)
36
37        self.fakedomains = [
38            "yahoo.com", "amazon.com", "live.com", "microsoft.com", "mail.ru", "csdn.net", "bing.com",
39            "amazon.co.jp", "office.com", "imdb.com", "msn.com", "samsung.com", "huawei.com", "ztedevices.com",
40            "godaddy.com", "w3.org", "gravatar.com", "creativecommons.org", "hatena.ne.jp",
41            "adobe.com", "opera.com", "apache.org", "rambler.ru", "one.com", "nationalgeographic.com",
42            "networksolutions.com", "php.net", "python.org", "phoca.cz", "debian.org", "ubuntu.com",
43            "nazwa.pl", "symantec.com"
44        ]
45
46    def createSslContexts(self):
47        ciphers = "ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:AES128-SHA256:AES256-SHA:"
48        ciphers += "!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK"
49
50        if hasattr(ssl, "PROTOCOL_TLS"):
51            protocol = ssl.PROTOCOL_TLS
52        else:
53            protocol = ssl.PROTOCOL_TLSv1_2
54        self.context_client = ssl.SSLContext(protocol)
55        self.context_client.check_hostname = False
56        self.context_client.verify_mode = ssl.CERT_NONE
57
58        self.context_server = ssl.SSLContext(protocol)
59        self.context_server.load_cert_chain(self.cert_pem, self.key_pem)
60
61        for ctx in (self.context_client, self.context_server):
62            ctx.set_ciphers(ciphers)
63            ctx.options |= ssl.OP_NO_COMPRESSION
64            try:
65                ctx.set_alpn_protocols(["h2", "http/1.1"])
66                ctx.set_npn_protocols(["h2", "http/1.1"])
67            except Exception:
68                pass
69
70    # Select crypt that supported by both sides
71    # Return: Name of the crypto
72    def selectCrypt(self, client_supported):
73        for crypt in self.crypt_supported:
74            if crypt in client_supported:
75                return crypt
76        return False
77
78    # Wrap socket for crypt
79    # Return: wrapped socket
80    def wrapSocket(self, sock, crypt, server=False, cert_pin=None):
81        if crypt == "tls-rsa":
82            if server:
83                sock_wrapped = self.context_server.wrap_socket(sock, server_side=True)
84            else:
85                sock_wrapped = self.context_client.wrap_socket(sock, server_hostname=random.choice(self.fakedomains))
86            if cert_pin:
87                cert_hash = hashlib.sha256(sock_wrapped.getpeercert(True)).hexdigest()
88                if cert_hash != cert_pin:
89                    raise Exception("Socket certificate does not match (%s != %s)" % (cert_hash, cert_pin))
90            return sock_wrapped
91        else:
92            return sock
93
94    def removeCerts(self):
95        if config.keep_ssl_cert:
96            return False
97        for file_name in ["cert-rsa.pem", "key-rsa.pem", "cacert-rsa.pem", "cakey-rsa.pem", "cacert-rsa.srl", "cert-rsa.csr", "openssl-rand.tmp"]:
98            file_path = "%s/%s" % (config.data_dir, file_name)
99            if os.path.isfile(file_path):
100                os.unlink(file_path)
101
102    # Load and create cert files is necessary
103    def loadCerts(self):
104        if config.disable_encryption:
105            return False
106
107        if self.createSslRsaCert() and "tls-rsa" not in self.crypt_supported:
108            self.crypt_supported.append("tls-rsa")
109
110    # Try to create RSA server cert + sign for connection encryption
111    # Return: True on success
112    def createSslRsaCert(self):
113        casubjects = [
114            "/C=US/O=Amazon/OU=Server CA 1B/CN=Amazon",
115            "/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3",
116            "/C=US/O=DigiCert Inc/OU=www.digicert.com/CN=DigiCert SHA2 High Assurance Server CA",
117            "/C=GB/ST=Greater Manchester/L=Salford/O=COMODO CA Limited/CN=COMODO RSA Domain Validation Secure Server CA"
118        ]
119        self.openssl_env['CN'] = random.choice(self.fakedomains)
120
121        if os.path.isfile(self.cert_pem) and os.path.isfile(self.key_pem):
122            self.createSslContexts()
123            return True  # Files already exits
124
125        import subprocess
126
127        # Generate CAcert and CAkey
128        cmd_params = helper.shellquote(
129            self.openssl_bin,
130            self.openssl_env["OPENSSL_CONF"],
131            random.choice(casubjects),
132            self.cakey_pem,
133            self.cacert_pem
134        )
135        cmd = "%s req -new -newkey rsa:2048 -days 3650 -nodes -x509 -config %s -subj %s -keyout %s -out %s -batch" % cmd_params
136        self.log.debug("Generating RSA CAcert and CAkey PEM files...")
137        self.log.debug("Running: %s" % cmd)
138        proc = subprocess.Popen(
139            cmd, shell=True, stderr=subprocess.STDOUT,
140            stdout=subprocess.PIPE, env=self.openssl_env
141        )
142        back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "")
143        proc.wait()
144
145        if not (os.path.isfile(self.cacert_pem) and os.path.isfile(self.cakey_pem)):
146            self.log.error("RSA ECC SSL CAcert generation failed, CAcert or CAkey files not exist. (%s)" % back)
147            return False
148        else:
149            self.log.debug("Result: %s" % back)
150
151        # Generate certificate key and signing request
152        cmd_params = helper.shellquote(
153            self.openssl_bin,
154            self.key_pem,
155            self.cert_csr,
156            "/CN=" + self.openssl_env['CN'],
157            self.openssl_env["OPENSSL_CONF"],
158        )
159        cmd = "%s req -new -newkey rsa:2048 -keyout %s -out %s -subj %s -sha256 -nodes -batch -config %s" % cmd_params
160        self.log.debug("Generating certificate key and signing request...")
161        proc = subprocess.Popen(
162            cmd, shell=True, stderr=subprocess.STDOUT,
163            stdout=subprocess.PIPE, env=self.openssl_env
164        )
165        back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "")
166        proc.wait()
167        self.log.debug("Running: %s\n%s" % (cmd, back))
168
169        # Sign request and generate certificate
170        cmd_params = helper.shellquote(
171            self.openssl_bin,
172            self.cert_csr,
173            self.cacert_pem,
174            self.cakey_pem,
175            self.cert_pem,
176            self.openssl_env["OPENSSL_CONF"]
177        )
178        cmd = "%s x509 -req -in %s -CA %s -CAkey %s -set_serial 01 -out %s -days 730 -sha256 -extensions x509_ext -extfile %s" % cmd_params
179        self.log.debug("Generating RSA cert...")
180        proc = subprocess.Popen(
181            cmd, shell=True, stderr=subprocess.STDOUT,
182            stdout=subprocess.PIPE, env=self.openssl_env
183        )
184        back = proc.stdout.read().strip().decode(errors="replace").replace("\r", "")
185        proc.wait()
186        self.log.debug("Running: %s\n%s" % (cmd, back))
187
188        if os.path.isfile(self.cert_pem) and os.path.isfile(self.key_pem):
189            self.createSslContexts()
190            return True
191        else:
192            self.log.error("RSA ECC SSL cert generation failed, cert or key files not exist.")
193
194
195manager = CryptConnectionManager()
196