From bdcf054de61d66b25a8a12aff4658b11d29a3a7b Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 8 Apr 2022 13:37:18 -0400 Subject: [PATCH] Switch to generating certs on the fly since Python packaging was being a pain. --- setup.py | 1 - src/allmydata/test/certs.py | 66 ++++++++++++++ src/allmydata/test/certs/domain.crt | 19 ---- src/allmydata/test/certs/private.key | 27 ------ src/allmydata/test/test_istorageserver.py | 20 +++-- src/allmydata/test/test_storage_https.py | 104 +++++----------------- 6 files changed, 101 insertions(+), 136 deletions(-) create mode 100644 src/allmydata/test/certs.py delete mode 100644 src/allmydata/test/certs/domain.crt delete mode 100644 src/allmydata/test/certs/private.key diff --git a/setup.py b/setup.py index 1883032de..5285b5d08 100644 --- a/setup.py +++ b/setup.py @@ -410,7 +410,6 @@ setup(name="tahoe-lafs", # also set in __init__.py "static/css/*.css", ], "allmydata": ["ported-modules.txt"], - "allmydata.test": ["certs/*"] }, include_package_data=True, setup_requires=setup_requires, diff --git a/src/allmydata/test/certs.py b/src/allmydata/test/certs.py new file mode 100644 index 000000000..9e6640386 --- /dev/null +++ b/src/allmydata/test/certs.py @@ -0,0 +1,66 @@ +"""Utilities for generating TLS certificates.""" + +import datetime + +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import serialization, hashes + +from twisted.python.filepath import FilePath + + +def cert_to_file(path: FilePath, cert) -> FilePath: + """ + Write the given certificate to a file on disk. Returns the path. + """ + path.setContent(cert.public_bytes(serialization.Encoding.PEM)) + return path + + +def private_key_to_file(path: FilePath, private_key) -> FilePath: + """ + Write the given key to a file on disk. Returns the path. + """ + path.setContent( + private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + ) + return path + + +def generate_private_key(): + """Create a RSA private key.""" + return rsa.generate_private_key(public_exponent=65537, key_size=2048) + + +def generate_certificate( + private_key, + expires_days: int = 10, + valid_in_days: int = 0, + org_name: str = "Yoyodyne", +): + """Generate a certificate from a RSA private key.""" + subject = issuer = x509.Name( + [x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name)] + ) + starts = datetime.datetime.utcnow() + datetime.timedelta(days=valid_in_days) + expires = datetime.datetime.utcnow() + datetime.timedelta(days=expires_days) + return ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(private_key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(min(starts, expires)) + .not_valid_after(expires) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName("localhost")]), + critical=False, + # Sign our certificate with our private key + ) + .sign(private_key, hashes.SHA256()) + ) diff --git a/src/allmydata/test/certs/domain.crt b/src/allmydata/test/certs/domain.crt deleted file mode 100644 index 8932b44e7..000000000 --- a/src/allmydata/test/certs/domain.crt +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDCzCCAfMCFHrs4pMBs35SlU3ZGMnVY5qp5MfZMA0GCSqGSIb3DQEBCwUAMEIx -CzAJBgNVBAYTAlhYMRUwEwYDVQQHDAxEZWZhdWx0IENpdHkxHDAaBgNVBAoME0Rl -ZmF1bHQgQ29tcGFueSBMdGQwHhcNMjIwMzIzMjAwNTM0WhcNMjIwNDIyMjAwNTM0 -WjBCMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQK -DBNEZWZhdWx0IENvbXBhbnkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB -CgKCAQEAzj0C3W4OiugEb3nr7NVQfrgzL3Tet5ze8pJCew0lIsDNrZiESF/Lvnrc -VcQtjraC3ySZO3rLDLhCwALLC1TVw3lp2ou+02kYtfJJVr1XyEcVCpsxx89u/W5B -VgMyBMoVZLS2BoBA1652XZnphvgm8n/daf9HH2Y4ifRDTIl1Bgl+4XtqlCKNFGYO -7zeadViKeI3fJOyzQaT+WTONQRWtAMP6c/n6VnlrdNughUEM+X0HqwVmr7UgatuA -LrJpfAkfKfOTZ70p2iOvegBH5ryR3InsB/O0/fp2+esNCEU3pfXWzg8ACif+ZQJc -AukUpQ4iJB7r1AK6iTZUqHnFgu9gYwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQCv -BzVSinez5lcSWWCRve/TlMePEJK5d7OFcc90n7kmn+rkEYrel3a7Q+ctJxY9AKYG -A9X1AMDSH9z3243KFNaRJ1xKg0Mg8J/BLN9iphM5AnAuiAkCqs8VbD4hF4hZHLMZ -BVNyuLSdo+lBzbS57/Lz+lUWcxrXR5qgsEWSbjP+SrsDQKODfyoxKuU0XrxmNLd2 -dTswbZKsqXBs80/T1jHwJjJXLp6YUsZqN1TGYtk8hEcE7bGaC3n7WhRjBP1WghNl -OG7FFRPte2w5seQRgkrBodLb9OCkhU4xfdyLnFICqkQHQAqIdXksEvir9uGY/yjC -zxAh60LEEQz6Kz29jYog ------END CERTIFICATE----- diff --git a/src/allmydata/test/certs/private.key b/src/allmydata/test/certs/private.key deleted file mode 100644 index b4b5ed580..000000000 --- a/src/allmydata/test/certs/private.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAzj0C3W4OiugEb3nr7NVQfrgzL3Tet5ze8pJCew0lIsDNrZiE -SF/LvnrcVcQtjraC3ySZO3rLDLhCwALLC1TVw3lp2ou+02kYtfJJVr1XyEcVCpsx -x89u/W5BVgMyBMoVZLS2BoBA1652XZnphvgm8n/daf9HH2Y4ifRDTIl1Bgl+4Xtq -lCKNFGYO7zeadViKeI3fJOyzQaT+WTONQRWtAMP6c/n6VnlrdNughUEM+X0HqwVm -r7UgatuALrJpfAkfKfOTZ70p2iOvegBH5ryR3InsB/O0/fp2+esNCEU3pfXWzg8A -Cif+ZQJcAukUpQ4iJB7r1AK6iTZUqHnFgu9gYwIDAQABAoIBAG71rGDuIazif+Bq -PGDDs/c5q3BQ9LLdF6Zywonp3J0CFqbbc/BsefYVrA4I6mnqECd2TWsO+cfyKxeb -aRrDne75l9YZcaXU2ZKqtIKShHQgqlV2giX6mMCJXWWlenfRMglooLaGslxYZR6e -/GG9iVbXLI0m52EhYjH21W6MVgXUhsrDoI16pB87zk7jFZzyNsjRU5+bwr4L2jed -A1PseE6AI2kIpJCl8IIu6hRhVwjr8MIkaAtI3G8WmSAru99apHNttf6sgB2kcq2r -Qp1uXEXNVFQiJqcwMPOlWZ5X0kMIBxmFe10MkJbmCUoE/jPqO90XN2jyZPSONZU8 -4yqd9GECgYEA5qQ+tfgOJFJ86t103pwkPm0+PxuQUTXry5ETnez+wxqqwbDxrEHi -MQoPZuVXPkbQ6g80KSpdI7AkFvu6BzcNlgpOI3gLZ30PHTF4HJTP01fKfbbVhg8N -WJS0yUh+kQDrGVcZbIbB5Q1vS5hu8ftk5ukns5BdFf/NS7fBfU8b3K0CgYEA5Onm -V2D1D9kdhTjta8f0d0s6+TdHoV86SRbkAEgnqzwlHWV17LlpXQic6iwokfS4TQSl -+1Z23Dt+OhLm/N0N3LgCxBhzTMnWGdy+w9co4GifwqR6T72JAxGOVoqIWk2cVpa5 -8qJx0eAFXqcvpIASEoxYrdoKFUh60mAiQE6JQ08CgYB8wCoLUviTPOrEPrSQE/Sm -r4ATsl0FEB1SJk5uBVpnPW1PBt4xRhGKZN6f0Ty3OqaVc1PLUFbAju12YQHmFSkM -Ftbc6HmCqGocaD2HeBZRQhMMnHAx6sJVP1np5YRP+icvtaTSxrDpq7KfOPwJdujE -3SfUQCmZVJs+cU3+8WMooQKBgQCdvvl2eWAm/a00IxipT2+NzY/kMU3xTFg0Ccww -zYhYnefNrB9pdBPBgq/vR2LlwchHes6OtvTNq0m+50u6MPLeiQeO7nJ2FhiuVco3 -1staaX6+eO24iZojPTPjOy/fWuBDYzbcl0jsIf5RTdCtAXxyv7hUhY6xP/Mzif/Q -ZM5+TQKBgQCF7pB6yLIAp6YJQ4uqOVbC2bMLr6tVWaNdEykiDd9zQkoacw/DPX1Y -FKehY/9EKraJ4t6d2/BBpJQyuIU4/gz8QMvjqQGP3NIfVeqBAPYo/nTYKOK0PSxB -Kl28Axxz6rjEeK4BixOES5PXuq2nNJXT8OSPYZQxQdTHstCWOP4Z6g== ------END RSA PRIVATE KEY----- diff --git a/src/allmydata/test/test_istorageserver.py b/src/allmydata/test/test_istorageserver.py index bc7d5b853..3d6f610be 100644 --- a/src/allmydata/test/test_istorageserver.py +++ b/src/allmydata/test/test_istorageserver.py @@ -25,6 +25,12 @@ from foolscap.api import Referenceable, RemoteException from allmydata.interfaces import IStorageServer # really, IStorageClient from .common_system import SystemTestMixin from .common import AsyncTestCase, SameProcessStreamEndpointAssigner +from .certs import ( + generate_certificate, + generate_private_key, + private_key_to_file, + cert_to_file, +) from allmydata.storage.server import StorageServer # not a IStorageServer!! from allmydata.storage.http_server import HTTPServer, listen_tls from allmydata.storage.http_client import StorageClient @@ -1057,20 +1063,16 @@ class _HTTPMixin(_SharedMixin): swissnum = b"1234" http_storage_server = HTTPServer(self.server, swissnum) - # Listen on randomly assigned port, using self-signed cert we generated - # manually: - certs_dir = FilePath(__file__).parent().child("certs") + # Listen on randomly assigned port, using self-signed cert: + private_key = generate_private_key() + certificate = generate_certificate(private_key) _, endpoint_string = self._port_assigner.assign(reactor) nurl, listening_port = yield listen_tls( http_storage_server, "127.0.0.1", serverFromString(reactor, endpoint_string), - # This is just a self-signed certificate with randomly generated - # private key; nothing at all special about it. You can regenerate - # with code in allmydata.test.test_storage_https or with openssl - # CLI, with no meaningful change to the test. - certs_dir.child("private.key"), - certs_dir.child("domain.crt"), + private_key_to_file(FilePath(self.mktemp()), private_key), + cert_to_file(FilePath(self.mktemp()), certificate), ) self.addCleanup(listening_port.stopListening) diff --git a/src/allmydata/test/test_storage_https.py b/src/allmydata/test/test_storage_https.py index 6877dae2a..73c99725a 100644 --- a/src/allmydata/test/test_storage_https.py +++ b/src/allmydata/test/test_storage_https.py @@ -6,14 +6,10 @@ server authentication logic, which may one day apply outside of HTTP Storage Protocol. """ -import datetime from functools import wraps from contextlib import asynccontextmanager from cryptography import x509 -from cryptography.x509.oid import NameOID -from cryptography.hazmat.primitives.asymmetric import rsa -from cryptography.hazmat.primitives import serialization, hashes from twisted.internet.endpoints import serverFromString from twisted.internet import reactor @@ -26,6 +22,12 @@ from twisted.python.filepath import FilePath from treq.client import HTTPClient from .common import SyncTestCase, AsyncTestCase, SameProcessStreamEndpointAssigner +from .certs import ( + generate_certificate, + generate_private_key, + private_key_to_file, + cert_to_file, +) from ..storage.http_common import get_spki_hash from ..storage.http_client import _StorageClientHTTPSPolicy from ..storage.http_server import _TLSEndpointWrapper @@ -100,68 +102,6 @@ class PinningHTTPSValidation(AsyncTestCase): self.addCleanup(self._port_assigner.tearDown) return AsyncTestCase.setUp(self) - def _temp_file_with_data(self, data: bytes) -> FilePath: - """ - Write data to temporary file, return its path. - """ - path = self.mktemp() - with open(path, "wb") as f: - f.write(data) - return FilePath(path) - - def cert_to_file(self, cert) -> FilePath: - """ - Write the given certificate to a temporary file on disk, return the - path. - """ - return self._temp_file_with_data(cert.public_bytes(serialization.Encoding.PEM)) - - def private_key_to_file(self, private_key) -> FilePath: - """ - Write the given key to a temporary file on disk, return the - path. - """ - return self._temp_file_with_data( - private_key.private_bytes( - encoding=serialization.Encoding.PEM, - format=serialization.PrivateFormat.TraditionalOpenSSL, - encryption_algorithm=serialization.NoEncryption(), - ) - ) - - def generate_private_key(self): - """Create a RSA private key.""" - return rsa.generate_private_key(public_exponent=65537, key_size=2048) - - def generate_certificate( - self, - private_key, - expires_days: int = 10, - valid_in_days: int = 0, - org_name: str = "Yoyodyne", - ): - """Generate a certificate from a RSA private key.""" - subject = issuer = x509.Name( - [x509.NameAttribute(NameOID.ORGANIZATION_NAME, org_name)] - ) - starts = datetime.datetime.utcnow() + datetime.timedelta(days=valid_in_days) - expires = datetime.datetime.utcnow() + datetime.timedelta(days=expires_days) - return ( - x509.CertificateBuilder() - .subject_name(subject) - .issuer_name(issuer) - .public_key(private_key.public_key()) - .serial_number(x509.random_serial_number()) - .not_valid_before(min(starts, expires)) - .not_valid_after(expires) - .add_extension( - x509.SubjectAlternativeName([x509.DNSName("localhost")]), - critical=False, - # Sign our certificate with our private key - ) - .sign(private_key, hashes.SHA256()) - ) - @asynccontextmanager async def listen(self, private_key_path: FilePath, cert_path: FilePath): """ @@ -210,10 +150,11 @@ class PinningHTTPSValidation(AsyncTestCase): If all conditions are met, a TLS client using the Tahoe-LAFS policy can connect to the server. """ - private_key = self.generate_private_key() - certificate = self.generate_certificate(private_key) + private_key = generate_private_key() + certificate = generate_certificate(private_key) async with self.listen( - self.private_key_to_file(private_key), self.cert_to_file(certificate) + private_key_to_file(FilePath(self.mktemp()), private_key), + cert_to_file(FilePath(self.mktemp()), certificate), ) as url: response = await self.request(url, certificate) self.assertEqual(await response.content(), b"YOYODYNE") @@ -224,13 +165,14 @@ class PinningHTTPSValidation(AsyncTestCase): If the server's certificate hash doesn't match the hash the client expects, the request to the server fails. """ - private_key1 = self.generate_private_key() - certificate1 = self.generate_certificate(private_key1) - private_key2 = self.generate_private_key() - certificate2 = self.generate_certificate(private_key2) + private_key1 = generate_private_key() + certificate1 = generate_certificate(private_key1) + private_key2 = generate_private_key() + certificate2 = generate_certificate(private_key2) async with self.listen( - self.private_key_to_file(private_key1), self.cert_to_file(certificate1) + private_key_to_file(FilePath(self.mktemp()), private_key1), + cert_to_file(FilePath(self.mktemp()), certificate1), ) as url: with self.assertRaises(ResponseNeverReceived): await self.request(url, certificate2) @@ -242,11 +184,12 @@ class PinningHTTPSValidation(AsyncTestCase): succeeds if the hash matches the one the client expects; expiration has no effect. """ - private_key = self.generate_private_key() - certificate = self.generate_certificate(private_key, expires_days=-10) + private_key = generate_private_key() + certificate = generate_certificate(private_key, expires_days=-10) async with self.listen( - self.private_key_to_file(private_key), self.cert_to_file(certificate) + private_key_to_file(FilePath(self.mktemp()), private_key), + cert_to_file(FilePath(self.mktemp()), certificate), ) as url: response = await self.request(url, certificate) self.assertEqual(await response.content(), b"YOYODYNE") @@ -258,13 +201,14 @@ class PinningHTTPSValidation(AsyncTestCase): request to the server succeeds if the hash matches the one the client expects; start time has no effect. """ - private_key = self.generate_private_key() - certificate = self.generate_certificate( + private_key = generate_private_key() + certificate = generate_certificate( private_key, expires_days=10, valid_in_days=5 ) async with self.listen( - self.private_key_to_file(private_key), self.cert_to_file(certificate) + private_key_to_file(FilePath(self.mktemp()), private_key), + cert_to_file(FilePath(self.mktemp()), certificate), ) as url: response = await self.request(url, certificate) self.assertEqual(await response.content(), b"YOYODYNE")