Skip to content

Commit

Permalink
Add https benchmark.
Browse files Browse the repository at this point in the history
The new benchmark starts a TLS secured server and spawns clients to
connect, validate the certificate chain, and GET the root resource.  The
certificate chains are generated using the cryptography package.
  • Loading branch information
jlitzingerdev committed Jul 6, 2017
1 parent adec86b commit 37ca9e5
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 0 deletions.
1 change: 1 addition & 0 deletions all.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

"tcp_connect", "ssh_connect", "ssl_connect", "sslbio_connect",
"tcp_throughput", "ssh_throughput", "ssl_throughput", "sslbio_throughput",
"web_https"
]

allBenchmarks = []
Expand Down
150 changes: 150 additions & 0 deletions web_https.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import datetime

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, asymmetric
from cryptography.hazmat.primitives.serialization import (Encoding,
PrivateFormat,
NoEncryption)
from cryptography import x509
from twisted.web import client
from twisted.web.server import Site
from twisted.web.static import Data
from twisted.web.resource import Resource
from twisted.internet.ssl import (Certificate, KeyPair, PrivateCertificate,
trustRootFromCertificates)

from benchlib import Client, driver

"""
This benchmark starts a Twisted Web server secured with TLS and makes
as many requests as possible in a fixed period of time. A certificate
chain is generated as part of the test and consists of:
Server -> Intermediate -> Root
To simulate actual validation overhead.
The following borrowed heavily from test_authentication in the txkube
project.
https://github.com/LeastAuthority/txkube
"""

rootKey, intermediateKey, serverKey = tuple(
asymmetric.rsa.generate_private_key(public_exponent=65537,
key_size=2048,
backend=default_backend())
for i in range(3)
)

def createCert(issuer, subject, privateKey, canSign, signingKey):
issuer = x509.Name([
x509.NameAttribute(x509.NameOID.COMMON_NAME, issuer)])

subject = x509.Name([
x509.NameAttribute(x509.NameOID.COMMON_NAME, subject)])

builder = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
privateKey.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=1)
).add_extension(
x509.SubjectAlternativeName([x509.DNSName(u"localhost")]),
critical=False
)

if canSign:
builder = builder.add_extension(
x509.BasicConstraints(True, None),
critical=True
)

return builder.sign(signingKey, hashes.SHA256(), default_backend())

rootCert = createCert(u"root", u"root", rootKey, True, rootKey)
intermediateCert = createCert(
u"root",
u"intermediate",
intermediateKey,
True,
rootKey
)

serverCert = createCert(
u"intermediate",
u"server",
serverKey,
False,
intermediateKey
)

serverPrivate = serverKey.private_bytes(
Encoding.DER,
PrivateFormat.TraditionalOpenSSL,
NoEncryption()
)

trustRoot = trustRootFromCertificates(
[Certificate.loadPEM(rootCert.public_bytes(Encoding.PEM)),
Certificate.loadPEM(intermediateCert.public_bytes(Encoding.PEM))]
)

privCert = PrivateCertificate.fromCertificateAndKeyPair(
Certificate.loadPEM(serverCert.public_bytes(Encoding.PEM)),
KeyPair.load(serverPrivate)
)

root = Resource()
root.putChild(b'', Data(b"Hello, world", "text/plain"))

class TLSClient(Client):

def __init__(self, reactor, port):
self._host = b'https://localhost:%d/' % port.getHost().port

This comment has been minimized.

Copy link
@exarkun

exarkun Jul 6, 2017

Note that this also incurs hostname resolution overhead. Not necessarily a problem but take care in deciding and documenting whether you want this cost to be part of the measurement or not.

This comment has been minimized.

Copy link
@jlitzingerdev

jlitzingerdev Jul 7, 2017

Author Owner

Thanks...that is a side effect I'd forgotten about. I specified "localhost" intentionally because I wanted to force hostname validation (I also get exceptions.ValueError: Invalid DNS-ID using just the loopback address). My main goal was to keep the benchmark as "real" as possible, but I'm beginning to think it might be more useful to have several, one to do the full path, one that only does cert validation, etc. If nothing else they might be useful in doing a coarse assessment of changes from release to release (assuming the hardware running the benchmark stays the same).

cf = client.BrowserLikePolicyForHTTPS(trustRoot=trustRoot)
self._agent = client.Agent(reactor, contextFactory=cf)
super(TLSClient, self).__init__(reactor)

def _request(self):
d = self._agent.request(b'GET', self._host)
d.addCallbacks(self._read, self._stop)

def _read(self, response):
d = client.readBody(response)
d.addCallback(self._continue)
d.addErrback(self._stop)


def main(reactor, duration):
concurrency = 10
port = reactor.listenSSL(
0,
Site(root),
privCert.options(),
backlog=128,
interface='127.0.0.1'
)

client = TLSClient(reactor, port)
d = client.run(concurrency, duration)

def cleanup(passthrough):
d = port.stopListening()
d.addCallback(lambda ignored: passthrough)
return d
d.addBoth(cleanup)
return d


if __name__ == '__main__':
import sys
import web_https
driver(web_https.main, sys.argv)

1 comment on commit 37ca9e5

@jlitzingerdev
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@exarkun I pushed an update attempting to minimize (or at least make consistent) the overhead associated with the hostname lookup:

1e7b3a3#diff-07cf9306b93291605491c56922db47d4

If you have a moment (no worries if not, I have very few as evidenced by the time gap) to comment on whether this approach was sane I'd appreciate it. Thanks regardless!

Please sign in to comment.