Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pyopenssl api support #5

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.idea/*
__pycache__/*
ssl-data/*
*.pyc
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ HTTP/HTTPS proxy in a single python script
* support both of IPv4 and IPv6
* support HTTP/1.1 Persistent Connection
* support dynamic certificate generation for HTTPS intercept
* all openssl operations does via pyopenssl api without any syscalls
* support certificate generation with subjectAltName extension for prevent ERR_CERT_COMMON_NAME_INVALID error.

This script works on Python 2.7.
You need to install OpenSSL to intercept HTTPS connections.
Expand Down Expand Up @@ -41,11 +43,9 @@ $ python proxy2.py 3128

## Enable HTTPS intercept

To intercept HTTPS connections, generate private keys and a private CA certificate:

```
$ ./setup_https_intercept.sh
```
This proxy intercepts HTTPS connections automatically. It generates private keys and a private CA certificate during
the first run or if it doesn't exist as ca.crt, cert.key and ca.key at ssl-data folder. You may change it names at ssl_wrapper.py
file.

Through the proxy, you can access http://proxy2.test/ and install the CA certificate in the browsers.

Expand Down
1 change: 0 additions & 1 deletion examples/proxy2.py

This file was deleted.

35 changes: 28 additions & 7 deletions proxy2.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import time
import json
import re
from ssl_wrapper import *
from string import Template
from OpenSSL import crypto
from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler
from SocketServer import ThreadingMixIn
from cStringIO import StringIO
Expand Down Expand Up @@ -61,26 +64,44 @@ def log_error(self, format, *args):
self.log_message(format, *args)

def do_CONNECT(self):
if os.path.isfile(self.cakey) and os.path.isfile(self.cacert) and os.path.isfile(self.certkey) and os.path.isdir(self.certdir):
if ca_files_exist():
self.connect_intercept()
else:
print("can't encode ssl traffic, just relay it")
self.connect_relay()

def connect_intercept(self):
hostname = self.path.split(':')[0]
certpath = "%s/%s.crt" % (self.certdir.rstrip('/'), hostname)
ippat = re.compile("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$")
cert_category = "DNS"
if ippat.match(hostname):
cert_category = "IP"

certpath = "%s/%s.crt" % (cert_dir.rstrip('/'), hostname)

with self.lock:
if not os.path.isfile(certpath):
epoch = "%d" % (time.time() * 1000)
p1 = Popen(["openssl", "req", "-new", "-key", self.certkey, "-subj", "/CN=%s" % hostname], stdout=PIPE)
p2 = Popen(["openssl", "x509", "-req", "-days", "3650", "-CA", self.cacert, "-CAkey", self.cakey, "-set_serial", epoch, "-out", certpath], stdin=p1.stdout, stderr=PIPE)
p2.communicate()
x509_serial = int("%d" % (time.time() * 1000))
valid_time_interval = (0, 60 * 60 * 24 * 365)
cert_request = create_cert_request(cert_key_obj, CN=hostname)
cert = create_certificate(
cert_request, (ca_crt_obj, ca_key_obj), x509_serial,
valid_time_interval,
subject_alt_names=[
Template("${category}:${hostname}").substitute(hostname=hostname, category=cert_category)
]
)
with open(certpath, 'wb+') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))

self.wfile.write("%s %d %s\r\n" % (self.protocol_version, 200, 'Connection Established'))
self.end_headers()

self.connection = ssl.wrap_socket(self.connection, keyfile=self.certkey, certfile=certpath, server_side=True)
self.connection = ssl.wrap_socket(self.connection,
keyfile=cert_key,
certfile=certpath,
server_side=True)

self.rfile = self.connection.makefile("rb", self.rbufsize)
self.wfile = self.connection.makefile("wb", self.wbufsize)

Expand Down
9 changes: 5 additions & 4 deletions setup_https_intercept.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/sh

openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=proxy2 CA"
openssl genrsa -out cert.key 2048
mkdir certs/
mkdir ssl-data/
openssl genrsa -out ssl-data/ca.key 2048
openssl req -new -x509 -days 3650 -key ssl-data/ca.key -out ssl-data/ca.crt -subj "/CN=proxy2 CA"
openssl genrsa -out ssl-data/cert.key 2048
mkdir ssl-data/certs/
194 changes: 194 additions & 0 deletions ssl_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""
Provides x509 certificates and paths.
"""

from os import mkdir
from os.path import abspath, dirname, isdir, isfile, join
import OpenSSL.crypto as crypto


proxy_CN = 'proxy2 CA'

# TODO: do this on package-install-time after move to pyopenssl
dir_name = join(dirname(abspath(__file__)), 'ssl-data')

ca_key = join(dir_name, 'ca.key')
ca_crt = join(dir_name, 'ca.crt')
cert_key = join(dir_name, 'cert.key')
cert_dir = join(dir_name, 'certs')

def generate_key_pair(key_type, bits):
"""
Creates key pair
:param key_type: one of crypto.TYPE_RSA or crypto.TYPE_DSA
:param bits: key length
:return: key pair in a PKey object
:return type: instance of crypto.PKey
"""
pkey = crypto.PKey()
pkey.generate_key(key_type, bits)
return pkey


def create_cert_request(p_key, digest="sha256", **subject_kwargs):
"""
Creates certificate request
:param p_key: key to associate with the request
:param digest: signing method
:param subject_kwargs: subject of request
valuable args are: (took from RFC 5280)
C: country
ST: state or province name
L: Locality name
O: organization
OU: organizational unit
CN: common name (e.g., "Susan Housley")
emailAddress: e-mail
:return: certificate request
"""
req = crypto.X509Req()
subj = req.get_subject()

for key, value in subject_kwargs.items():
setattr(subj, key, value)

req.set_pubkey(p_key)
req.sign(p_key, digest)
return req


def create_certificate(
req, cert_key_pair, serial, begin_end_validity, digest="sha256",
self_signed_x509v3=False, subject_alt_names=[]):
"""
Create certificate by certificate request.
:param req: certificate request
:param cert_key_pair: tuple with issuer certificate and private key
:param serial: serial number
:param begin_end_validity: tuple with seconds certificate validity.
0 means now. Example for set one year valid certificate from now:
begin_end_validity=(0, 60*60*24*365)
:param digest: signing method
:param self_signed_x509v3: generate self signed x509v3 CA certificate, add
extensions similar to these:
X509v3 extensions:
X509v3 Subject Key Identifier:
88:31:6A:B7:8C:B3:F0:1D:5F:CD:9F:F8:70:F7:D4:7C:E5:5E:D2:A1
X509v3 Authority Key Identifier:
keyid:88:31:6A:B7:8C:B3:F0:1D:5F:CD:9F:F8:70:F7:D4:7C:E5:5E:D2:A1
X509v3 Basic Constraints:
CA:TRUE
:param subject_alt_names: subject alt names e.g. IP:192.168.7.1 or DNS:my.domain
:return: signed certificate
:return type: crypto.X509
"""
i_cert, i_key = cert_key_pair
not_before, not_after = begin_end_validity
ret_x509_obj = crypto.X509()
ret_x509_obj.set_serial_number(serial)

ret_x509_obj.gmtime_adj_notAfter(not_after)
ret_x509_obj.gmtime_adj_notBefore(not_before)
if i_cert == '__self_signed':
i_cert = ret_x509_obj
ret_x509_obj.set_issuer(i_cert.get_subject())
ret_x509_obj.set_subject(req.get_subject())
ret_x509_obj.set_pubkey(req.get_pubkey())
if self_signed_x509v3:
ret_x509_obj.set_version(2)
ret_x509_obj.add_extensions([
crypto.X509Extension("subjectKeyIdentifier", False, "hash",
subject=ret_x509_obj),
crypto.X509Extension("basicConstraints", False, "CA:TRUE"),
])
ret_x509_obj.add_extensions([
crypto.X509Extension("authorityKeyIdentifier", False,
"keyid:always", issuer=ret_x509_obj),
])
if len(subject_alt_names) != 0:
ret_x509_obj.set_version(2) # 0x3
ret_x509_obj.add_extensions([
crypto.X509Extension(
type_name=b'subjectAltName',
critical=False,
value=", ".join(subject_alt_names).encode())
])

ret_x509_obj.sign(i_key, digest)
return ret_x509_obj


def ca_files_exist():
return all(
[map(isfile, [ca_key, ca_crt, cert_key]), isdir(dir_name)])

if not ca_files_exist():
# TODO: move this code to pyopenssl library
try:
if not isdir(dir_name):
mkdir(dir_name)
ca_key_o = generate_key_pair(crypto.TYPE_RSA, 2048)
cert_key_o = generate_key_pair(crypto.TYPE_RSA, 2048)
cert_req_temp = create_cert_request(ca_key_o, CN=proxy_CN)
ca_crt_o = create_certificate(
cert_req_temp, ('__self_signed', ca_key_o), 1509982490957715,
(0, 60 * 60 * 24 * 30), self_signed_x509v3=True
)
with open(ca_key, 'w+') as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, ca_key_o))
with open(cert_key, 'w+') as f:
f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, cert_key_o))
with open(ca_crt, 'w+') as f:
f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, ca_crt_o))

if not isdir(cert_dir):
mkdir(cert_dir)

except StandardError as e:
print(e)


def _load_crypto_obj(path, crypto_method):
with open(path, 'r') as key_fp:
return crypto_method(crypto.FILETYPE_PEM, key_fp.read())

cert_key_obj = _load_crypto_obj(cert_key, crypto.load_privatekey)
ca_key_obj = _load_crypto_obj(ca_key, crypto.load_privatekey)
ca_crt_obj = _load_crypto_obj(ca_crt, crypto.load_certificate)


__all__ = [
'proxy_CN',
'dir_name',
'ca_key',
'ca_crt',
'cert_key',
'cert_dir',
'ca_files_exist',
'cert_key_obj',
'ca_key_obj',
'ca_crt_obj',
'generate_key_pair',
'create_cert_request',
'create_certificate',
]


if __name__ == '__main__':
def _load_crypto_obj(path, crypto_method):
with open(path, 'r') as key_fp:
return crypto_method(crypto.FILETYPE_PEM, key_fp.read())

cert_key_obj = _load_crypto_obj(cert_key, crypto.load_privatekey)
ca_key_obj = _load_crypto_obj(ca_key, crypto.load_privatekey)
ca_crt_obj = _load_crypto_obj(ca_crt, crypto.load_certificate)
cert_req = create_cert_request(ca_key_obj, CN=proxy_CN)
signed_req = create_certificate(
cert_req, (ca_crt_obj, ca_key_obj), 1509982490957715,
(0, 60 * 60 * 24 * 30), self_signed_x509v3=True
)


print(crypto.dump_certificate(crypto.FILETYPE_PEM, signed_req))
'openssl x509 -req -days 3650 -CA ca.crt -CAkey ca.key -set_serial 1509982490957715'
'https://github.com/pyca/pyopenssl/blob/master/examples/certgen.py'