Switch to generating certs on the fly since Python packaging was being a pain.

This commit is contained in:
Itamar Turner-Trauring 2022-04-08 13:37:18 -04:00
parent 423512ad00
commit bdcf054de6
6 changed files with 101 additions and 136 deletions

View File

@ -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,

View File

@ -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())
)

View File

@ -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-----

View File

@ -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-----

View File

@ -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)

View File

@ -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")