Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Support ACME for certificate provisioning #4384

Merged
merged 71 commits into from
Jan 23, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
7273a5b
add acme file. clenaup sorts
hawkowl Jan 14, 2019
bebd71e
first cut
hawkowl Jan 14, 2019
17a161e
first cut
hawkowl Jan 14, 2019
35e7768
fix
hawkowl Jan 15, 2019
895ef68
fix
hawkowl Jan 15, 2019
75ec773
fix
hawkowl Jan 15, 2019
bad5b23
fix
hawkowl Jan 15, 2019
727bfc2
try provisioning...?
hawkowl Jan 17, 2019
c5fbe94
try provisioning...?
hawkowl Jan 17, 2019
b999a09
try provisioning...?
hawkowl Jan 17, 2019
9461c00
try provisioning...?
hawkowl Jan 17, 2019
ddc7a17
try provisioning...?
hawkowl Jan 17, 2019
d9bf3eb
try provisioning...?
hawkowl Jan 17, 2019
04f805b
try provisioning...?
hawkowl Jan 17, 2019
aefd46e
try provisioning...?
hawkowl Jan 17, 2019
ac3ca16
try provisioning...?
hawkowl Jan 17, 2019
4740f7d
try provisioning...?
hawkowl Jan 17, 2019
603f896
try provisioning...?
hawkowl Jan 17, 2019
bb7861a
try provisioning...?
hawkowl Jan 17, 2019
1e9c0ad
try provisioning...?
hawkowl Jan 17, 2019
4ab63ea
try provisioning...?
hawkowl Jan 17, 2019
f1b9b0f
try provisioning...?
hawkowl Jan 17, 2019
840fcb0
try provisioning...?
hawkowl Jan 17, 2019
b616b89
try provisioning...?
hawkowl Jan 17, 2019
248d943
try provisioning...?
hawkowl Jan 17, 2019
f1dfb47
try provisioning...?
hawkowl Jan 17, 2019
bd8114c
try provisioning...?
hawkowl Jan 17, 2019
38b86cb
try provisioning...?
hawkowl Jan 17, 2019
6db5717
try provisioning...?
hawkowl Jan 17, 2019
940271c
try provisioning...?
hawkowl Jan 17, 2019
723f3a1
try provisioning...?
hawkowl Jan 17, 2019
fa9b705
try provisioning...?
hawkowl Jan 17, 2019
8b92348
smol fix
hawkowl Jan 17, 2019
52d71ec
smol fix
hawkowl Jan 17, 2019
05ac907
cleanups
hawkowl Jan 18, 2019
f449d20
changelog
hawkowl Jan 18, 2019
b0ff1ca
fix
hawkowl Jan 18, 2019
7f72038
fix
hawkowl Jan 18, 2019
54350ea
fix old python2
hawkowl Jan 18, 2019
26787bd
reduce diff
hawkowl Jan 18, 2019
e01e824
Merge remote-tracking branch 'origin/develop' into hawkowl/acme-porta…
hawkowl Jan 21, 2019
d601f85
fixes and review cleanup
hawkowl Jan 21, 2019
0cc093f
more review cleanup
hawkowl Jan 21, 2019
9051fd8
review cleanup
hawkowl Jan 21, 2019
64e5b41
review cleanup
hawkowl Jan 21, 2019
2f5e68c
review cleanup
hawkowl Jan 21, 2019
6d16053
review cleanup
hawkowl Jan 21, 2019
349ab14
fix py3
hawkowl Jan 21, 2019
f150ec6
write the full chain
hawkowl Jan 21, 2019
956f72d
write the full chain better
hawkowl Jan 21, 2019
521bc24
don't use DHE. ECDHE is now common, easier, and not vulnerable to the…
hawkowl Jan 21, 2019
08ebd2e
cleanup
hawkowl Jan 21, 2019
a7f6727
cleanup
hawkowl Jan 21, 2019
77eef86
cleanup
hawkowl Jan 21, 2019
b58e684
cleanup
hawkowl Jan 21, 2019
fe15602
oops
hawkowl Jan 21, 2019
7323166
Update synapse/app/homeserver.py
richvdh Jan 22, 2019
02a2a49
review cleanup
hawkowl Jan 22, 2019
c12ba8e
Merge branch 'hawkowl/acme-portable-certificates' of ssh://github.com…
hawkowl Jan 22, 2019
8a44d61
review cleanup
hawkowl Jan 22, 2019
67bccf7
Merge remote-tracking branch 'origin/develop' into hawkowl/acme-porta…
hawkowl Jan 22, 2019
1fb1f9c
review cleanup
hawkowl Jan 22, 2019
30af4ad
review cleanup
hawkowl Jan 22, 2019
947ab0a
review cleanup
hawkowl Jan 22, 2019
57ec9f3
review cleanup
hawkowl Jan 22, 2019
4db63ab
fix
hawkowl Jan 22, 2019
c293b21
Merge remote-tracking branch 'origin/develop' into hawkowl/acme-porta…
hawkowl Jan 22, 2019
392cfe9
Update synapse/app/homeserver.py
richvdh Jan 23, 2019
380b01d
Merge remote-tracking branch 'origin/develop' into hawkowl/acme-porta…
hawkowl Jan 23, 2019
1a70173
Merge branch 'hawkowl/acme-portable-certificates' of ssh://github.com…
hawkowl Jan 23, 2019
0baf9ee
review cleanup
hawkowl Jan 23, 2019
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
1 change: 1 addition & 0 deletions changelog.d/4384.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Synapse can now automatically provision TLS certificates via ACME (the protocol used by CAs like Let's Encrypt).
2 changes: 1 addition & 1 deletion scripts-dev/build_debian_packages
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
# can be passed on the commandline for debugging.

import argparse
from concurrent.futures import ThreadPoolExecutor
import os
import signal
import subprocess
import sys
import threading
from concurrent.futures import ThreadPoolExecutor

DISTS = (
"debian:stretch",
Expand Down
56 changes: 47 additions & 9 deletions synapse/app/homeserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import gc
import logging
import os
import sys
import traceback

from six import iteritems

Expand Down Expand Up @@ -324,17 +326,12 @@ def setup(config_options):

events.USE_FROZEN_DICTS = config.use_frozen_dicts

tls_server_context_factory = context_factory.ServerContextFactory(config)
tls_client_options_factory = context_factory.ClientTLSOptionsFactory(config)

database_engine = create_engine(config.database_config)
config.database_config["args"]["cp_openfun"] = database_engine.on_new_connection

hs = SynapseHomeServer(
config.server_name,
db_config=config.database_config,
tls_server_context_factory=tls_server_context_factory,
tls_client_options_factory=tls_client_options_factory,
config=config,
version_string="Synapse/" + get_version_string(synapse),
database_engine=database_engine,
Expand All @@ -361,12 +358,53 @@ def setup(config_options):
logger.info("Database prepared in %s.", config.database_config['name'])

hs.setup()
hs.start_listening()
hawkowl marked this conversation as resolved.
Show resolved Hide resolved

@defer.inlineCallbacks
def start():
hs.get_pusherpool().start()
hs.get_datastore().start_profiling()
hs.get_datastore().start_doing_background_updates()
try:
# Check if the certificate is still valid.
cert_days_remaining = hs.config.is_disk_cert_valid()

if hs.config.acme_enabled:
# If ACME is enabled, we might need to provision a certificate
# before starting.
acme = hs.get_acme_handler()

# Start up the webservices which we will respond to ACME
# challenges with.
yield acme.start_listening()

# We want to reprovision if cert_days_remaining is None (meaning no
# certificate exists), or the days remaining number it returns
# is less than our re-registration threshold.
if (cert_days_remaining is None) or (
not cert_days_remaining > hs.config.acme_reprovision_threshold
):
yield acme.provision_certificate()

# Read the certificate from disk and build the context factories for
# TLS.
hs.config.read_certificate_from_disk()
hs.tls_server_context_factory = context_factory.ServerContextFactory(config)
hs.tls_client_options_factory = context_factory.ClientTLSOptionsFactory(
config
)

# It is now safe to start your Synapse.
hs.start_listening()
hs.get_pusherpool().start()
hs.get_datastore().start_profiling()
hs.get_datastore().start_doing_background_updates()
except Exception as e:
# If a DeferredList failed (like in listening on the ACME listener),
# we need to print the subfailure explicitly.
if isinstance(e, defer.FirstError):
e.subFailure.printTraceback(sys.stderr)
sys.exit(1)

# Something else went wrong when starting. Print it and bail out.
traceback.print_exc(file=sys.stderr)
sys.exit(1)

reactor.callWhenRunning(start)

Expand Down
4 changes: 2 additions & 2 deletions synapse/config/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ def read_config_files(self, config_files, keys_directory=None, generate_keys=Fal
if not keys_directory:
keys_directory = os.path.dirname(config_files[-1])

config_dir_path = os.path.abspath(keys_directory)
self.config_dir_path = os.path.abspath(keys_directory)

specified_config = {}
for config_file in config_files:
Expand All @@ -379,7 +379,7 @@ def read_config_files(self, config_files, keys_directory=None, generate_keys=Fal

server_name = specified_config["server_name"]
config_string = self.generate_config(
config_dir_path=config_dir_path,
config_dir_path=self.config_dir_path,
data_dir_path=os.getcwd(),
server_name=server_name,
generate_secrets=False,
Expand Down
115 changes: 91 additions & 24 deletions synapse/config/tls.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,60 +13,110 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
from datetime import datetime
from hashlib import sha256

from unpaddedbase64 import encode_base64

from OpenSSL import crypto

from ._base import Config
from synapse.config._base import Config

logger = logging.getLogger()


class TlsConfig(Config):
def read_config(self, config):
self.tls_certificate = self.read_tls_certificate(
config.get("tls_certificate_path")
)
self.tls_certificate_file = config.get("tls_certificate_path")

acme_config = config.get("acme", {})
self.acme_enabled = acme_config.get("enabled", False)
self.acme_url = acme_config.get(
"url", "https://acme-v01.api.letsencrypt.org/directory"
)
self.acme_port = acme_config.get("port", 8449)
self.acme_bind_addresses = acme_config.get("bind_addresses", ["127.0.0.1"])
self.acme_reprovision_threshold = acme_config.get("reprovision_threshold", 30)

self.tls_certificate_file = os.path.abspath(config.get("tls_certificate_path"))
self.tls_private_key_file = os.path.abspath(config.get("tls_private_key_path"))
self._original_tls_fingerprints = config["tls_fingerprints"]
self.tls_fingerprints = list(self._original_tls_fingerprints)
self.no_tls = config.get("no_tls", False)

if self.no_tls:
self.tls_private_key = None
else:
self.tls_private_key = self.read_tls_private_key(
config.get("tls_private_key_path")
)
# This config option applies to non-federation HTTP clients
# (e.g. for talking to recaptcha, identity servers, and such)
# It should never be used in production, and is intended for
# use only when running tests.
self.use_insecure_ssl_client_just_for_testing_do_not_use = config.get(
"use_insecure_ssl_client_just_for_testing_do_not_use"
)

self.tls_certificate = None
self.tls_private_key = None

def is_disk_cert_valid(self):
"""
Is the certificate we have on disk valid, and if so, for how long?

Returns:
int: Days remaining of certificate validity.
None: No certificate exists.
"""
Copy link
Member

Choose a reason for hiding this comment

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

please can you document the return types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

if not os.path.exists(self.tls_certificate_file):
return None

try:
with open(self.tls_certificate_file, 'rb') as f:
cert_pem = f.read()
except Exception:
logger.exception("Failed to read existing certificate off disk!")
raise

try:
tls_certificate = crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
hawkowl marked this conversation as resolved.
Show resolved Hide resolved
except Exception:
logger.exception("Failed to parse existing certificate off disk!")
raise

# YYYYMMDDhhmmssZ -- in UTC
expires_on = datetime.strptime(
tls_certificate.get_notAfter().decode('ascii'), "%Y%m%d%H%M%SZ"
)
now = datetime.utcnow()
days_remaining = (expires_on - now).days
return days_remaining

self.tls_fingerprints = config["tls_fingerprints"]
def read_certificate_from_disk(self):
"""
Read the certificates from disk.
"""
self.tls_certificate = self.read_tls_certificate(self.tls_certificate_file)

if not self.no_tls:
self.tls_private_key = self.read_tls_private_key(self.tls_private_key_file)

self.tls_fingerprints = list(self._original_tls_fingerprints)

# Check that our own certificate is included in the list of fingerprints
# and include it if it is not.
x509_certificate_bytes = crypto.dump_certificate(
crypto.FILETYPE_ASN1,
self.tls_certificate
crypto.FILETYPE_ASN1, self.tls_certificate
)
sha256_fingerprint = encode_base64(sha256(x509_certificate_bytes).digest())
sha256_fingerprints = set(f["sha256"] for f in self.tls_fingerprints)
if sha256_fingerprint not in sha256_fingerprints:
self.tls_fingerprints.append({u"sha256": sha256_fingerprint})

# This config option applies to non-federation HTTP clients
# (e.g. for talking to recaptcha, identity servers, and such)
# It should never be used in production, and is intended for
# use only when running tests.
self.use_insecure_ssl_client_just_for_testing_do_not_use = config.get(
"use_insecure_ssl_client_just_for_testing_do_not_use"
)

def default_config(self, config_dir_path, server_name, **kwargs):
base_key_name = os.path.join(config_dir_path, server_name)

tls_certificate_path = base_key_name + ".tls.crt"
tls_private_key_path = base_key_name + ".tls.key"

return """\
return (
"""\
# PEM encoded X509 certificate for TLS.
# You can replace the self-signed certificate that synapse
# autogenerates on launch with your own SSL certificate + key pair
Expand Down Expand Up @@ -107,7 +157,24 @@ def default_config(self, config_dir_path, server_name, **kwargs):
#
tls_fingerprints: []
# tls_fingerprints: [{"sha256": "<base64_encoded_sha256_fingerprint>"}]
""" % locals()

## Support for ACME certificate auto-provisioning.
# acme:
hawkowl marked this conversation as resolved.
Show resolved Hide resolved
# enabled: false
## ACME path.
## If you only want to test, use the staging url:
## https://acme-staging.api.letsencrypt.org/directory
# url: 'https://acme-v01.api.letsencrypt.org/directory'
## Port number (to listen for the HTTP-01 challenge).
## Using port 80 requires utilising something like authbind, or proxying to it.
# port: 8449
## Hosts to bind to.
# bind_addresses: ['127.0.0.1']
## How many days remaining on a certificate before it is renewed.
# reprovision_threshold: 30
"""
% locals()
)

def read_tls_certificate(self, cert_path):
cert_pem = self.read_file(cert_path, "tls_certificate")
Expand Down
Loading