From 35fa386ce3b9c52b469b1ae16400ab4c067e2bef Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Fri, 28 Oct 2016 13:59:42 -0400 Subject: [PATCH 01/82] First draft of certificate management integration (e.g. Let's Encrypt certificate install) --- data/actionsmap/yunohost.yml | 58 +++- locales/de.json | 2 +- locales/en.json | 18 +- locales/es.json | 2 +- locales/fr.json | 2 +- locales/nl.json | 2 +- locales/pt.json | 2 +- src/yunohost/app.py | 16 + src/yunohost/certificate.py | 641 +++++++++++++++++++++++++++++++++++ src/yunohost/domain.py | 46 +-- 10 files changed, 748 insertions(+), 41 deletions(-) create mode 100644 src/yunohost/certificate.py diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 5a1465258..4d0db97ec 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -305,8 +305,64 @@ domain: - !!str ^[0-9]+$ - "pattern_positive_number" + ### certificate_status() + cert-status: + action_help: List status of current certificates (all by default). + api: GET /certs/status/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domainList: + help: Domains to check + nargs: "*" + --full: + help: Show more details + action: store_true - ### domain_info() + ### certificate_install() + cert-install: + action_help: Install Let's Encrypt certificates for given domains (all by default). + api: POST /certs/enable/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domainList: + help: Domains for which to install the certificates + nargs: "*" + --force: + help: Install even if current certificate is not self-signed + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to install. (Not recommended) + action: store_true + --self-signed: + help: Install self-signed certificate instead of Let's Encrypt + action: store_true + + ### certificate_renew() + cert-renew: + action_help: Renew the Let's Encrypt certificates for given domains (all by default). + api: POST /certs/renew/ + configuration: + authenticate: all + authenticator: ldap-anonymous + arguments: + domainList: + help: Domains for which to renew the certificates + nargs: "*" + --force: + help: Ignore the validity treshold (30 days) + action: store_true + --email: + help: Send an email to root with logs if some renewing fails + action: store_true + --no-checks: + help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended) + action: store_true + + ### domain_info() # info: # action_help: Get domain informations # api: GET /domains/ diff --git a/locales/de.json b/locales/de.json index 1331c56b4..e57315caa 100644 --- a/locales/de.json +++ b/locales/de.json @@ -57,7 +57,7 @@ "custom_app_url_required": "Es muss eine URL angegeben um deine benutzerdefinierte App {app:s} zu aktualisieren", "custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben", "dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus", - "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", + "certmanager_domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domain erfolgreich erzeugt", "domain_creation_failed": "Konnte Domain nicht erzeugen", "domain_deleted": "Domain erfolgreich gelöscht", diff --git a/locales/en.json b/locales/en.json index e939b26fa..35da2eef0 100644 --- a/locales/en.json +++ b/locales/en.json @@ -71,7 +71,6 @@ "diagnostic_monitor_system_error": "Can't monitor system: {error}", "diagnostic_no_apps": "No installed application", "dnsmasq_isnt_installed": "dnsmasq does not seem to be installed, please run 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "Unable to generate certificate", "domain_created": "The domain has been created", "domain_creation_failed": "Unable to create domain", "domain_deleted": "The domain has been deleted", @@ -237,5 +236,20 @@ "yunohost_ca_creation_failed": "Unable to create certificate authority", "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", - "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'." + "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", + "certmanager_domain_cert_gen_failed": "Unable to generate certificate", + "certmanager_attempt_to_replace_valid_cert" : "You are attempting to overwrite a good and valid certificate for domain {domain:s} ! (Use --force to bypass)", + "certmanager_domain_unknown": "Unknown domain {domain:s}", + "certmanager_domain_cert_not_selfsigned" : "The certificate of domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", + "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", + "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", + "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", + "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", + "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}). Use --no-checks to disable checks.", + "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", + "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", + "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s})", + "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", + "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !" } diff --git a/locales/es.json b/locales/es.json index 549cbe29a..fdd04d10f 100644 --- a/locales/es.json +++ b/locales/es.json @@ -72,7 +72,7 @@ "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "No se pudo crear el certificado", + "certmanager_domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", "domain_deleted": "El dominio ha sido eliminado", diff --git a/locales/fr.json b/locales/fr.json index 7898de57f..6691b2f28 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -73,7 +73,7 @@ "diagnostic_monitor_system_error": "Impossible de superviser le système : {error}", "diagnostic_no_apps": "Aucune application installée", "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer « apt-get remove bind9 && apt-get install dnsmasq »", - "domain_cert_gen_failed": "Impossible de générer le certificat", + "certmanager_domain_cert_gen_failed": "Impossible de générer le certificat", "domain_created": "Le domaine a été créé", "domain_creation_failed": "Impossible de créer le domaine", "domain_deleted": "Le domaine a été supprimé", diff --git a/locales/nl.json b/locales/nl.json index c2bfed31e..57b05e309 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -37,7 +37,7 @@ "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken", "custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst", "dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'", - "domain_cert_gen_failed": "Kan certificaat niet genereren", + "certmanager_domain_cert_gen_failed": "Kan certificaat niet genereren", "domain_created": "Domein succesvol aangemaakt", "domain_creation_failed": "Kan domein niet aanmaken", "domain_deleted": "Domein succesvol verwijderd", diff --git a/locales/pt.json b/locales/pt.json index d3796d2e9..b9c9e4bce 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -36,7 +36,7 @@ "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia", "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {app:s}", "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", - "domain_cert_gen_failed": "Não foi possível gerar o certificado", + "certmanager_domain_cert_gen_failed": "Não foi possível gerar o certificado", "domain_created": "Domínio criado com êxito", "domain_creation_failed": "Não foi possível criar o domínio", "domain_deleted": "Domínio removido com êxito", diff --git a/src/yunohost/app.py b/src/yunohost/app.py index caa38e95b..2326995ae 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1031,6 +1031,22 @@ def app_ssowatconf(auth): for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) + # Authorize ACME challenge url if a domain seems configured for it... + for domain in domains: + + # Check ACME challenge file is present in nginx conf + nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" + if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue + + # Check the file contains the ACME challenge uri + acme_uri = '/.well-known/acme-challenge' + if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue + + # If so, then authorize the ACME challenge uri to unprotected regex + regex = domain+"/%.well%-known/acme%-challenge/.*$" + unprotected_regex.append(regex) + + conf_dict = { 'portal_domain': main_domain, 'portal_path': '/yunohost/sso/', diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py new file mode 100644 index 000000000..4ed5cb588 --- /dev/null +++ b/src/yunohost/certificate.py @@ -0,0 +1,641 @@ +# -*- coding: utf-8 -*- + +""" License + + Copyright (C) 2016 YUNOHOST.ORG + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program; if not, see http://www.gnu.org/licenses + +""" + +""" yunohost_certificate.py + + Manage certificates, in particular Let's encrypt +""" + +import os +import sys +import errno +import requests +import shutil +import pwd +import grp +import json +import smtplib + +from OpenSSL import crypto +from datetime import datetime +from tabulate import tabulate +from acme_tiny import get_crt as sign_certificate + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger +import yunohost.domain +from yunohost.service import _run_service_command +from yunohost.app import app_setting, app_ssowatconf + + +logger = getActionLogger('yunohost.certmanager') + +# Misc stuff we need + +cert_folder = "/etc/yunohost/certs/" +tmp_folder = "/tmp/acme-challenge-private/" +webroot_folder = "/tmp/acme-challenge-public/" + +selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" +account_key_file = "/etc/yunohost/letsencrypt_account.pem" + +key_size = 2048 + +validity_limit = 15 # days + +# For tests +#certification_authority = "https://acme-staging.api.letsencrypt.org" +# For prod +certification_authority = "https://acme-v01.api.letsencrypt.org" + +intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" + +############################################################################### +# Front-end stuff # +############################################################################### + +# Status + +def certificate_status(auth, domainList, full = False): + """ + Print the status of certificate for given domains (all by default) + + Keyword argument: + domainList -- Domains to be checked + full -- Display more info about the certificates + """ + + # If no domains given, consider all yunohost domains + if (domainList == []) : domainList = yunohost.domain.domain_list(auth)['domains'] + # Else, validate that yunohost knows the domains given + else : + for domain in domainList : + # Is it in Yunohost dmomain list ? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + + # Get status for each domain, and prepare display using tabulate + if not full : + headers = [ "Domain", "Certificate status", "Authority type", "Days remaining"] + else : + headers = [ "Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] + lines = [] + for domain in domainList : + status = _get_status(domain) + + line = [] + line.append(domain) + if (full) : line.append(status["subject"]) + line.append(_summary_code_to_string(status["summaryCode"])) + line.append(status["CAtype"]) + if (full) : line.append(status["CAname"]) + line.append(status["validity"]) + lines.append(line) + + print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) + + +def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False) : + """ + Install a Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domainList -- Domains on which to install certificates + force -- Install even if current certificate is not self-signed + no-check -- Disable some checks about the reachability of web server + before attempting the install + self-signed -- Instal self-signed certificates instead of Let's Encrypt + """ + if (self_signed) : + certificate_install_selfsigned(domainList, force) + else : + certificate_install_letsencrypt(auth, domainList, force, no_checks) + + +# Install self-signed + +def certificate_install_selfsigned(domainList, force=False) : + + for domain in domainList : + + # Check we ain't trying to overwrite a good cert ! + status = _get_status(domain) + if (status != {}) and (status["summaryCode"] > 0) and (not force) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + + + cert_folder_domain = cert_folder+"/"+domain + + # Create cert folder if it does not exists yet + try: + os.listdir(cert_folder_domain) + except OSError: + os.makedirs(cert_folder_domain) + + # Get serial + ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' + with open('%s/serial' % ssl_dir, 'r') as f: + serial = f.readline().rstrip() + + # FIXME : should refactor this to avoid so many os.system() calls... + # We should be able to do all this using OpenSSL.crypto and os/shutil + command_list = [ + 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), + 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), + 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' + % (cert_folder_domain, ssl_dir, ssl_dir), + 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' + % (cert_folder_domain, ssl_dir, ssl_dir), + 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain, + 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), + 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), + 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) + ] + + for command in command_list: + if os.system(command) != 0: + raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + + _set_permissions(cert_folder_domain, "root", "root", 0755); + _set_permissions(cert_folder_domain+"/key.pem", "root", "metronome", 0640); + _set_permissions(cert_folder_domain+"/crt.pem", "root", "metronome", 0640); + _set_permissions(cert_folder_domain+"/openssl.cnf", "root", "root", 0600); + + + +# Install ACME / Let's Encrypt certificate + +def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): + + + if not os.path.exists(account_key_file) : + _generate_account_key() + + # If no domains given, consider all yunohost domains with self-signed + # certificates + if (domainList == []) : + for domain in yunohost.domain.domain_list(auth)['domains'] : + + # Is it self-signed ? + status = _get_status(domain) + if (status["CAtype"] != "Self-signed") : continue + + domainList.append(domain) + + # Else, validate that yunohost knows the domains given + else : + for domain in domainList : + # Is it in Yunohost dmomain list ? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + + # Is it self-signed ? + status = _get_status(domain) + if (not force) and (status["CAtype"] != "Self-signed") : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) + + + # Actual install steps + for domain in domainList : + + logger.info("Now attempting install of certificate for domain "+domain+" !") + + try : + + if not no_checks : _check_domain_is_correctly_configured(domain) + _backup_current_cert(domain) + _configure_for_acme_challenge(auth, domain) + _fetch_and_enable_new_certificate(domain) + _install_cron() + + logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) + + except Exception as e : + + logger.error("Certificate installation for "+domain+" failed !") + logger.error(str(e)) + + +# Renew + + +def certificate_renew(auth, domainList, force=False, no_checks=False, email=False): + """ + Renew Let's Encrypt certificate for given domains (all by default) + + Keyword argument: + domainList -- Domains for which to renew the certificates + force -- Ignore the validity threshold (15 days) + no-check -- Disable some checks about the reachability of web server + before attempting the renewing + email -- Emails root if some renewing failed + """ + + # If no domains given, consider all yunohost domains with Let's Encrypt + # certificates + if (domainList == []) : + for domain in yunohost.domain.domain_list(auth)['domains'] : + + # Does it has a Let's Encrypt cert ? + status = _get_status(domain) + if (status["CAtype"] != "Let's Encrypt") : continue + + # Does it expires soon ? + if (force) or (status["validity"] <= validity_limit) : + domainList.append(domain) + + if (len(domainList) == 0) : + logger.info("No certificate needs to be renewed.") + + # Else, validate the domain list given + else : + for domain in domainList : + + # Is it in Yunohost dmomain list ? + if domain not in yunohost.domain.domain_list(auth)['domains']: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + + status = _get_status(domain) + + # Does it expires soon ? + if not ((force) or (status["validity"] <= validity_limit)) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) + + + # Does it has a Let's Encrypt cert ? + if (status["CAtype"] != "Let's Encrypt") : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + + + # Actual renew steps + for domain in domainList : + + logger.info("Now attempting renewing of certificate for domain "+domain+" !") + + try : + + if not no_checks : _check_domain_is_correctly_configured(domain) + _backup_current_cert(domain) + _fetch_and_enable_new_certificate(domain) + + logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) + + except Exception as e : + + logger.error("Certificate renewing for "+domain+" failed !") + logger.error(str(e)) + + if (email) : + logger.error("Sending email with details to root ...") + _email_renewing_failed(domain, e) + + + + + +############################################################################### +# Back-end stuff # +############################################################################### + +def _install_cron() : + + cron_job_file = "/etc/cron.weekly/certificateRenewer" + + with open(cron_job_file, "w") as f : + + f.write("#!/bin/bash\n") + f.write("yunohost domain cert-renew --email\n") + + _set_permissions(cron_job_file, "root", "root", 0755); + +def _email_renewing_failed(domain, e) : + + from_ = "certmanager@"+domain+" (Certificate Manager)" + to_ = "root" + subject_ = "Certificate renewing attempt for "+domain+" failed!" + + exceptionMessage = str(e) + logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") + text = """ +At attempt for renewing the certificate for domain %s failed with the following +error : + +%s + +Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to +investigate : + +%s + +-- Certificate Manager + +""" % (domain, exceptionMessage, logs) + + message = """ +From: %s +To: %s +Subject: %s + +%s +""" % (from_, to_, subject_, text) + + smtp = smtplib.SMTP("localhost") + smtp.sendmail(from_, [ to_ ], message) + smtp.quit() + + + +def _configure_for_acme_challenge(auth, domain) : + + nginx_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" + + nginx_configuration = ''' +location '/.well-known/acme-challenge' +{ + default_type "text/plain"; + alias '''+webroot_folder+'''; +} + ''' + + # Write the conf + if os.path.exists(nginx_conf_file) : + logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") + else : + logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") + with open(nginx_conf_file, "w") as f : + f.write(nginx_configuration) + + # Assume nginx conf is okay, and reload it + # (FIXME : maybe add a check that it is, using nginx -t, haven't found + # any clean function already implemented in yunohost to do this though) + _run_service_command("reload", "nginx") + + app_ssowatconf(auth) + +def _fetch_and_enable_new_certificate(domain) : + + # Make sure tmp folder exists + logger.debug("Making sure tmp folders exists...") + + if not (os.path.exists(webroot_folder)) : os.makedirs(webroot_folder) + if not (os.path.exists(tmp_folder)) : os.makedirs(tmp_folder) + _set_permissions(webroot_folder, "root", "www-data", 0650); + _set_permissions(tmp_folder, "root", "root", 0640); + + # Prepare certificate signing request + logger.info("Prepare key and certificate signing request (CSR) for "+domain+"...") + + domain_key_file = tmp_folder+"/"+domain+".pem" + _generate_key(domain_key_file) + _set_permissions(domain_key_file, "root", "metronome", 0640); + + _prepare_certificate_signing_request(domain, domain_key_file, tmp_folder) + + # Sign the certificate + logger.info("Now using ACME Tiny to sign the certificate...") + + domain_csr_file = tmp_folder+"/"+domain+".csr" + + signed_certificate = sign_certificate(account_key_file, + domain_csr_file, + webroot_folder, + log=logger, + CA=certification_authority) + intermediate_certificate = requests.get(intermediate_certificate_url).text + + # Now save the key and signed certificate + logger.info("Saving the key and signed certificate...") + + # Create corresponding directory + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + new_cert_folder = cert_folder+"/" + domain + "." + date_tag + os.makedirs(new_cert_folder) + _set_permissions(new_cert_folder, "root", "root", 0655); + + # Move the private key + shutil.move(domain_key_file, new_cert_folder+"/key.pem") + + # Write the cert + domain_cert_file = new_cert_folder+"/crt.pem" + with open(domain_cert_file, "w") as f : + f.write(signed_certificate) + f.write(intermediate_certificate) + _set_permissions(domain_cert_file, "root", "metronome", 0640); + + + + logger.info("Enabling the new certificate...") + + # Replace (if necessary) the link or folder for live cert + live_link = cert_folder+"/"+domain + + if not os.path.islink(live_link) : + shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + elif os.path.lexists(live_link) : + os.remove(live_link) + + os.symlink(new_cert_folder, live_link) + + # Check the status of the certificate is now good + + statusSummaryCode = _get_status(domain)["summaryCode"] + if (statusSummaryCode < 20) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) + + + logger.info("Restarting services...") + + for s in [ "nginx", "postfix", "dovecot", "metronome" ] : + _run_service_command("restart", s) + + +def _prepare_certificate_signing_request(domain, key_file, output_folder) : + + # Init a request + csr = crypto.X509Req() + + # Set the domain + csr.get_subject().CN = domain + + # Set the key + with open(key_file, 'rt') as f : + key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + csr.set_pubkey(key) + + # Sign the request + csr.sign(key, "sha256") + + # Save the request in tmp folder + csr_file = output_folder+domain+".csr" + logger.info("Saving to "+csr_file+" .") + with open(csr_file, "w") as f : + f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) + + +def _get_status(domain) : + + cert_file = cert_folder+"/"+domain+"/crt.pem" + + if (not os.path.isfile(cert_file)) : return {} + + try : + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) + except : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) + + certSubject = cert.get_subject().CN + certIssuer = cert.get_issuer().CN + validUpTo = datetime.strptime(cert.get_notAfter(),"%Y%m%d%H%M%SZ") + daysRemaining = (validUpTo - datetime.now()).days + + CAtype = None + if (certIssuer == _name_selfCA()) : + CAtype = "Self-signed" + elif (certIssuer.startswith("Let's Encrypt")) : + CAtype = "Let's Encrypt" + elif (certIssuer.startswith("Fake LE")) : + CAtype = "Fake Let's Encrypt" + else : + CAtype = "Other / Unknown" + + # Unknown by default + statusSummaryCode = 0 + # Critical + if (daysRemaining <= 0) : statusSummaryCode = -30 + # Warning, self-signed, browser will display a warning discouraging visitors to enter website + elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt") : statusSummaryCode = -20 + # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) + elif (daysRemaining < validity_limit) : statusSummaryCode = -10 + # CA not known, but still a valid certificate, so okay ! + elif (CAtype == "Other / Unknown") : statusSummaryCode = 10 + # Let's Encrypt, great ! + elif (CAtype == "Let's Encrypt") : statusSummaryCode = 20 + + return { "domain" : domain, + "subject" : certSubject, + "CAname" : certIssuer, + "CAtype" : CAtype, + "validity" : daysRemaining, + "summaryCode" : statusSummaryCode + } + +############################################################################### +# Misc small stuff ... # +############################################################################### + +def _generate_account_key() : + + logger.info("Generating account key ...") + _generate_key(account_key_file) + _set_permissions(account_key_file, "root", "root", 0400) + +def _generate_key(destinationPath) : + + k = crypto.PKey() + k.generate_key(crypto.TYPE_RSA, key_size) + + with open(destinationPath, "w") as f : + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) + +def _set_permissions(path, user, group, permissions) : + + uid = pwd.getpwnam(user).pw_uid + gid = grp.getgrnam(group).gr_gid + + os.chown(path, uid, gid) + os.chmod(path, permissions) + +def _backup_current_cert(domain): + + logger.info("Backuping existing certificate for domain "+domain) + + cert_folder_domain = cert_folder+"/"+domain + + dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") + backup_folder = cert_folder_domain+"-backup-"+dateTag + + shutil.copytree(cert_folder_domain, backup_folder) + + +def _check_domain_is_correctly_configured(domain) : + + public_ip = yunohost.domain.get_public_ip() + + # Check if IP from DNS matches public IP + if not _dns_ip_match_public_ip(public_ip, domain) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + + # Check if domain seems to be accessible through HTTP ? + if not _domain_is_accessible_through_HTTP(public_ip, domain) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) + +def _dns_ip_match_public_ip(public_ip, domain) : + + try : + r = requests.get("http://dns-api.org/A/"+domain) + except : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org")) + + if (r.text == "[{\"error\":\"NXDOMAIN\"}]") : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) + + try : + dns_ip = json.loads(r.text)[0]["value"] + except : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text)) + + if (dns_ip != public_ip) : + return False + else : + return True + +def _domain_is_accessible_through_HTTP(ip, domain) : + + try : + requests.head("http://"+ip, headers = { "Host" : domain }) + except Exception : + return False + + return True + +def _summary_code_to_string(code) : + + if (code <= -30) : return "CRITICAL" + elif (code <= -20) : return "WARNING" + elif (code <= -10) : return "Attention" + elif (code <= 0) : return "Unknown?" + elif (code <= 10) : return "Good" + elif (code <= 20) : return "Great!" + + return "Unknown?" + +def _name_selfCA() : + + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) + return cert.get_subject().CN + + +def _tail(n, filePath): + stdin,stdout = os.popen2("tail -n "+str(n)+" "+filePath) + stdin.close() + lines = stdout.readlines(); stdout.close() + lines = "".join(lines) + return lines diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 98fa368ed..58ca41b08 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -34,10 +34,11 @@ import errno import requests from urllib import urlopen -from moulinette.core import MoulinetteError +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.service import service_regen_conf +from yunohost.service import service_regen_conf +import yunohost.certificate logger = getActionLogger('yunohost.domain') @@ -113,37 +114,7 @@ def domain_add(auth, domain, dyndns=False): m18n.n('domain_dyndns_root_unknown')) try: - # Commands - ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - ssl_domain_path = '/etc/yunohost/certs/%s' % domain - with open('%s/serial' % ssl_dir, 'r') as f: - serial = f.readline().rstrip() - try: os.listdir(ssl_domain_path) - except OSError: os.makedirs(ssl_domain_path) - - command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, ssl_domain_path), - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, ssl_domain_path), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' - % (ssl_domain_path, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' - % (ssl_domain_path, ssl_dir, ssl_dir), - 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % ssl_domain_path, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, ssl_domain_path), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, ssl_domain_path), - 'chmod 755 %s' % ssl_domain_path, - 'chmod 640 %s/key.pem' % ssl_domain_path, - 'chmod 640 %s/crt.pem' % ssl_domain_path, - 'chmod 600 %s/openssl.cnf' % ssl_domain_path, - 'chown root:metronome %s/key.pem' % ssl_domain_path, - 'chown root:metronome %s/crt.pem' % ssl_domain_path, - 'cat %s/ca.pem >> %s/crt.pem' % (ssl_domain_path, ssl_domain_path) - ] - - for command in command_list: - if os.system(command) != 0: - raise MoulinetteError(errno.EIO, - m18n.n('domain_cert_gen_failed')) + yunohost.certificate.certificate_install_selfsigned([domain], False) try: auth.validate_uniqueness({ 'virtualdomain': domain }) @@ -285,6 +256,14 @@ def domain_dns_conf(domain, ttl=None): return result +def domain_cert_status(auth, domainList, full=False): + return yunohost.certificate.certificate_status(auth, domainList, full) + +def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False): + return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed) + +def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False): + return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email) def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" @@ -301,3 +280,4 @@ def get_public_ip(protocol=4): logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) + From d47f5919d679e9cbb87b6980e700735e975b72d8 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:22:56 +0100 Subject: [PATCH 02/82] [mod] remove unused imports --- src/yunohost/certificate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 4ed5cb588..69a78964e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -25,7 +25,6 @@ """ import os -import sys import errno import requests import shutil @@ -43,7 +42,7 @@ from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger import yunohost.domain from yunohost.service import _run_service_command -from yunohost.app import app_setting, app_ssowatconf +from yunohost.app import app_ssowatconf logger = getActionLogger('yunohost.certmanager') From bec8f6347959cb5ab38cb24842703e5858bef8f4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:24:54 +0100 Subject: [PATCH 03/82] [mod] autopep8 --- src/yunohost/certificate.py | 415 ++++++++++++++++++------------------ 1 file changed, 210 insertions(+), 205 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 69a78964e..e11ea9e3b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -33,32 +33,34 @@ import grp import json import smtplib -from OpenSSL import crypto -from datetime import datetime -from tabulate import tabulate -from acme_tiny import get_crt as sign_certificate +from OpenSSL import crypto +from datetime import datetime +from tabulate import tabulate +from acme_tiny import get_crt as sign_certificate -from moulinette.core import MoulinetteError -from moulinette.utils.log import getActionLogger import yunohost.domain -from yunohost.service import _run_service_command -from yunohost.app import app_ssowatconf + +from moulinette.core import MoulinetteError +from moulinette.utils.log import getActionLogger + +from yunohost.app import app_ssowatconf +from yunohost.service import _run_service_command logger = getActionLogger('yunohost.certmanager') # Misc stuff we need -cert_folder = "/etc/yunohost/certs/" -tmp_folder = "/tmp/acme-challenge-private/" -webroot_folder = "/tmp/acme-challenge-public/" +cert_folder = "/etc/yunohost/certs/" +tmp_folder = "/tmp/acme-challenge-private/" +webroot_folder = "/tmp/acme-challenge-public/" -selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" +selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" account_key_file = "/etc/yunohost/letsencrypt_account.pem" -key_size = 2048 +key_size = 2048 -validity_limit = 15 # days +validity_limit = 15 # days # For tests #certification_authority = "https://acme-staging.api.letsencrypt.org" @@ -73,7 +75,8 @@ intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cr # Status -def certificate_status(auth, domainList, full = False): + +def certificate_status(auth, domainList, full=False): """ Print the status of certificate for given domains (all by default) @@ -83,36 +86,39 @@ def certificate_status(auth, domainList, full = False): """ # If no domains given, consider all yunohost domains - if (domainList == []) : domainList = yunohost.domain.domain_list(auth)['domains'] + if (domainList == []): + domainList = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given - else : - for domain in domainList : + else: + for domain in domainList: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Get status for each domain, and prepare display using tabulate - if not full : - headers = [ "Domain", "Certificate status", "Authority type", "Days remaining"] - else : - headers = [ "Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] + if not full: + headers = ["Domain", "Certificate status", "Authority type", "Days remaining"] + else: + headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] lines = [] - for domain in domainList : + for domain in domainList: status = _get_status(domain) line = [] line.append(domain) - if (full) : line.append(status["subject"]) + if (full): + line.append(status["subject"]) line.append(_summary_code_to_string(status["summaryCode"])) line.append(status["CAtype"]) - if (full) : line.append(status["CAname"]) + if (full): + line.append(status["CAname"]) line.append(status["validity"]) lines.append(line) print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) -def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False) : +def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -122,31 +128,29 @@ def certificate_install(auth, domainList, force=False, no_checks=False, self_sig no-check -- Disable some checks about the reachability of web server before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt - """ - if (self_signed) : + """ + if (self_signed): certificate_install_selfsigned(domainList, force) - else : + else: certificate_install_letsencrypt(auth, domainList, force, no_checks) -# Install self-signed +# Install self-signed -def certificate_install_selfsigned(domainList, force=False) : - - for domain in domainList : +def certificate_install_selfsigned(domainList, force=False): + for domain in domainList: # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if (status != {}) and (status["summaryCode"] > 0) and (not force) : + if (status != {}) and (status["summaryCode"] > 0) and (not force): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - - cert_folder_domain = cert_folder+"/"+domain + cert_folder_domain = cert_folder + "/" + domain # Create cert folder if it does not exists yet - try: + try: os.listdir(cert_folder_domain) - except OSError: + except OSError: os.makedirs(cert_folder_domain) # Get serial @@ -157,79 +161,77 @@ def certificate_install_selfsigned(domainList, force=False) : # FIXME : should refactor this to avoid so many os.system() calls... # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), + 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), + 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' % (cert_folder_domain, ssl_dir, ssl_dir), 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' % (cert_folder_domain, ssl_dir, ssl_dir), 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), - 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) + 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), + 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), + 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) - _set_permissions(cert_folder_domain, "root", "root", 0755); - _set_permissions(cert_folder_domain+"/key.pem", "root", "metronome", 0640); - _set_permissions(cert_folder_domain+"/crt.pem", "root", "metronome", 0640); - _set_permissions(cert_folder_domain+"/openssl.cnf", "root", "root", 0600); - + _set_permissions(cert_folder_domain, "root", "root", 0755) + _set_permissions(cert_folder_domain + "/key.pem", "root", "metronome", 0640) + _set_permissions(cert_folder_domain + "/crt.pem", "root", "metronome", 0640) + _set_permissions(cert_folder_domain + "/openssl.cnf", "root", "root", 0600) # Install ACME / Let's Encrypt certificate def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): - - - if not os.path.exists(account_key_file) : + if not os.path.exists(account_key_file): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if (domainList == []) : - for domain in yunohost.domain.domain_list(auth)['domains'] : + if (domainList == []): + for domain in yunohost.domain.domain_list(auth)['domains']: # Is it self-signed ? status = _get_status(domain) - if (status["CAtype"] != "Self-signed") : continue + if (status["CAtype"] != "Self-signed"): + continue domainList.append(domain) # Else, validate that yunohost knows the domains given - else : - for domain in domainList : + else: + for domain in domainList: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Is it self-signed ? status = _get_status(domain) - if (not force) and (status["CAtype"] != "Self-signed") : + if (not force) and (status["CAtype"] != "Self-signed"): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) - # Actual install steps - for domain in domainList : - - logger.info("Now attempting install of certificate for domain "+domain+" !") + for domain in domainList: - try : + logger.info("Now attempting install of certificate for domain " + domain + " !") - if not no_checks : _check_domain_is_correctly_configured(domain) + try: + + if not no_checks: + _check_domain_is_correctly_configured(domain) _backup_current_cert(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) _install_cron() - + logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) - - except Exception as e : - - logger.error("Certificate installation for "+domain+" failed !") + + except Exception as e: + + logger.error("Certificate installation for " + domain + " failed !") logger.error(str(e)) @@ -250,86 +252,82 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if (domainList == []) : - for domain in yunohost.domain.domain_list(auth)['domains'] : + if (domainList == []): + for domain in yunohost.domain.domain_list(auth)['domains']: # Does it has a Let's Encrypt cert ? status = _get_status(domain) - if (status["CAtype"] != "Let's Encrypt") : continue + if (status["CAtype"] != "Let's Encrypt"): + continue # Does it expires soon ? - if (force) or (status["validity"] <= validity_limit) : + if (force) or (status["validity"] <= validity_limit): domainList.append(domain) - if (len(domainList) == 0) : + if (len(domainList) == 0): logger.info("No certificate needs to be renewed.") # Else, validate the domain list given - else : - for domain in domainList : + else: + for domain in domainList: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) - + status = _get_status(domain) # Does it expires soon ? - if not ((force) or (status["validity"] <= validity_limit)) : + if not ((force) or (status["validity"] <= validity_limit)): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) - # Does it has a Let's Encrypt cert ? - if (status["CAtype"] != "Let's Encrypt") : + if (status["CAtype"] != "Let's Encrypt"): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) - # Actual renew steps - for domain in domainList : - - logger.info("Now attempting renewing of certificate for domain "+domain+" !") + for domain in domainList: - try : + logger.info("Now attempting renewing of certificate for domain " + domain + " !") - if not no_checks : _check_domain_is_correctly_configured(domain) + try: + + if not no_checks: + _check_domain_is_correctly_configured(domain) _backup_current_cert(domain) _fetch_and_enable_new_certificate(domain) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) - - except Exception as e : - - logger.error("Certificate renewing for "+domain+" failed !") + + except Exception as e: + + logger.error("Certificate renewing for " + domain + " failed !") logger.error(str(e)) - if (email) : + if (email): logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e) - - - ############################################################################### # Back-end stuff # ############################################################################### -def _install_cron() : - +def _install_cron(): cron_job_file = "/etc/cron.weekly/certificateRenewer" - with open(cron_job_file, "w") as f : + with open(cron_job_file, "w") as f: f.write("#!/bin/bash\n") f.write("yunohost domain cert-renew --email\n") - _set_permissions(cron_job_file, "root", "root", 0755); + _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, e) : - from_ = "certmanager@"+domain+" (Certificate Manager)" - to_ = "root" - subject_ = "Certificate renewing attempt for "+domain+" failed!" +def _email_renewing_failed(domain, e): + from_ = "certmanager@" + domain + " (Certificate Manager)" + to_ = "root" + subject_ = "Certificate renewing attempt for " + domain + " failed!" exceptionMessage = str(e) logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") @@ -357,29 +355,27 @@ Subject: %s """ % (from_, to_, subject_, text) smtp = smtplib.SMTP("localhost") - smtp.sendmail(from_, [ to_ ], message) + smtp.sendmail(from_, [to_], message) smtp.quit() - -def _configure_for_acme_challenge(auth, domain) : - - nginx_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" +def _configure_for_acme_challenge(auth, domain): + nginx_conf_file = "/etc/nginx/conf.d/" + domain + ".d/000-acmechallenge.conf" nginx_configuration = ''' location '/.well-known/acme-challenge' { default_type "text/plain"; - alias '''+webroot_folder+'''; + alias ''' + webroot_folder + '''; } ''' # Write the conf - if os.path.exists(nginx_conf_file) : + if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") - else : + else: logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") - with open(nginx_conf_file, "w") as f : + with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) # Assume nginx conf is okay, and reload it @@ -389,29 +385,31 @@ location '/.well-known/acme-challenge' app_ssowatconf(auth) -def _fetch_and_enable_new_certificate(domain) : +def _fetch_and_enable_new_certificate(domain): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") - if not (os.path.exists(webroot_folder)) : os.makedirs(webroot_folder) - if not (os.path.exists(tmp_folder)) : os.makedirs(tmp_folder) - _set_permissions(webroot_folder, "root", "www-data", 0650); - _set_permissions(tmp_folder, "root", "root", 0640); + if not (os.path.exists(webroot_folder)): + os.makedirs(webroot_folder) + if not (os.path.exists(tmp_folder)): + os.makedirs(tmp_folder) + _set_permissions(webroot_folder, "root", "www-data", 0650) + _set_permissions(tmp_folder, "root", "root", 0640) # Prepare certificate signing request - logger.info("Prepare key and certificate signing request (CSR) for "+domain+"...") + logger.info("Prepare key and certificate signing request (CSR) for " + domain + "...") - domain_key_file = tmp_folder+"/"+domain+".pem" + domain_key_file = tmp_folder + "/" + domain + ".pem" _generate_key(domain_key_file) - _set_permissions(domain_key_file, "root", "metronome", 0640); + _set_permissions(domain_key_file, "root", "metronome", 0640) _prepare_certificate_signing_request(domain, domain_key_file, tmp_folder) # Sign the certificate logger.info("Now using ACME Tiny to sign the certificate...") - domain_csr_file = tmp_folder+"/"+domain+".csr" + domain_csr_file = tmp_folder + "/" + domain + ".csr" signed_certificate = sign_certificate(account_key_file, domain_csr_file, @@ -425,30 +423,28 @@ def _fetch_and_enable_new_certificate(domain) : # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = cert_folder+"/" + domain + "." + date_tag + new_cert_folder = cert_folder + "/" + domain + "." + date_tag os.makedirs(new_cert_folder) - _set_permissions(new_cert_folder, "root", "root", 0655); + _set_permissions(new_cert_folder, "root", "root", 0655) # Move the private key - shutil.move(domain_key_file, new_cert_folder+"/key.pem") + shutil.move(domain_key_file, new_cert_folder + "/key.pem") # Write the cert - domain_cert_file = new_cert_folder+"/crt.pem" - with open(domain_cert_file, "w") as f : + domain_cert_file = new_cert_folder + "/crt.pem" + with open(domain_cert_file, "w") as f: f.write(signed_certificate) f.write(intermediate_certificate) - _set_permissions(domain_cert_file, "root", "metronome", 0640); - - + _set_permissions(domain_cert_file, "root", "metronome", 0640) logger.info("Enabling the new certificate...") # Replace (if necessary) the link or folder for live cert - live_link = cert_folder+"/"+domain + live_link = cert_folder + "/" + domain - if not os.path.islink(live_link) : - shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) - elif os.path.lexists(live_link) : + if not os.path.islink(live_link): + shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + elif os.path.lexists(live_link): os.remove(live_link) os.symlink(new_cert_folder, live_link) @@ -456,18 +452,16 @@ def _fetch_and_enable_new_certificate(domain) : # Check the status of the certificate is now good statusSummaryCode = _get_status(domain)["summaryCode"] - if (statusSummaryCode < 20) : + if (statusSummaryCode < 20): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) - logger.info("Restarting services...") - for s in [ "nginx", "postfix", "dovecot", "metronome" ] : + for s in ["nginx", "postfix", "dovecot", "metronome"]: _run_service_command("restart", s) -def _prepare_certificate_signing_request(domain, key_file, output_folder) : - +def _prepare_certificate_signing_request(domain, key_file, output_folder): # Init a request csr = crypto.X509Req() @@ -475,7 +469,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder) : csr.get_subject().CN = domain # Set the key - with open(key_file, 'rt') as f : + with open(key_file, 'rt') as f: key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) csr.set_pubkey(key) @@ -483,158 +477,169 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder) : csr.sign(key, "sha256") # Save the request in tmp folder - csr_file = output_folder+domain+".csr" - logger.info("Saving to "+csr_file+" .") - with open(csr_file, "w") as f : + csr_file = output_folder + domain + ".csr" + logger.info("Saving to " + csr_file + " .") + with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) -def _get_status(domain) : +def _get_status(domain): + cert_file = cert_folder + "/" + domain + "/crt.pem" - cert_file = cert_folder+"/"+domain+"/crt.pem" + if (not os.path.isfile(cert_file)): + return {} - if (not os.path.isfile(cert_file)) : return {} - - try : + try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) - except : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) + except: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) certSubject = cert.get_subject().CN certIssuer = cert.get_issuer().CN - validUpTo = datetime.strptime(cert.get_notAfter(),"%Y%m%d%H%M%SZ") + validUpTo = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") daysRemaining = (validUpTo - datetime.now()).days CAtype = None - if (certIssuer == _name_selfCA()) : + if (certIssuer == _name_selfCA()): CAtype = "Self-signed" - elif (certIssuer.startswith("Let's Encrypt")) : + elif (certIssuer.startswith("Let's Encrypt")): CAtype = "Let's Encrypt" - elif (certIssuer.startswith("Fake LE")) : + elif (certIssuer.startswith("Fake LE")): CAtype = "Fake Let's Encrypt" - else : + else: CAtype = "Other / Unknown" # Unknown by default statusSummaryCode = 0 # Critical - if (daysRemaining <= 0) : statusSummaryCode = -30 + if (daysRemaining <= 0): + statusSummaryCode = -30 # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt") : statusSummaryCode = -20 + elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt"): + statusSummaryCode = -20 # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) - elif (daysRemaining < validity_limit) : statusSummaryCode = -10 + elif (daysRemaining < validity_limit): + statusSummaryCode = -10 # CA not known, but still a valid certificate, so okay ! - elif (CAtype == "Other / Unknown") : statusSummaryCode = 10 + elif (CAtype == "Other / Unknown"): + statusSummaryCode = 10 # Let's Encrypt, great ! - elif (CAtype == "Let's Encrypt") : statusSummaryCode = 20 + elif (CAtype == "Let's Encrypt"): + statusSummaryCode = 20 - return { "domain" : domain, - "subject" : certSubject, - "CAname" : certIssuer, - "CAtype" : CAtype, - "validity" : daysRemaining, - "summaryCode" : statusSummaryCode - } + return {"domain": domain, + "subject": certSubject, + "CAname": certIssuer, + "CAtype": CAtype, + "validity": daysRemaining, + "summaryCode": statusSummaryCode + } ############################################################################### # Misc small stuff ... # ############################################################################### -def _generate_account_key() : +def _generate_account_key(): logger.info("Generating account key ...") _generate_key(account_key_file) _set_permissions(account_key_file, "root", "root", 0400) -def _generate_key(destinationPath) : +def _generate_key(destinationPath): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, key_size) - with open(destinationPath, "w") as f : + with open(destinationPath, "w") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) -def _set_permissions(path, user, group, permissions) : +def _set_permissions(path, user, group, permissions): uid = pwd.getpwnam(user).pw_uid gid = grp.getgrnam(group).gr_gid os.chown(path, uid, gid) os.chmod(path, permissions) + def _backup_current_cert(domain): + logger.info("Backuping existing certificate for domain " + domain) - logger.info("Backuping existing certificate for domain "+domain) - - cert_folder_domain = cert_folder+"/"+domain + cert_folder_domain = cert_folder + "/" + domain dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = cert_folder_domain+"-backup-"+dateTag + backup_folder = cert_folder_domain + "-backup-" + dateTag shutil.copytree(cert_folder_domain, backup_folder) -def _check_domain_is_correctly_configured(domain) : - +def _check_domain_is_correctly_configured(domain): public_ip = yunohost.domain.get_public_ip() # Check if IP from DNS matches public IP - if not _dns_ip_match_public_ip(public_ip, domain) : + if not _dns_ip_match_public_ip(public_ip, domain): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) # Check if domain seems to be accessible through HTTP ? - if not _domain_is_accessible_through_HTTP(public_ip, domain) : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) - -def _dns_ip_match_public_ip(public_ip, domain) : + if not _domain_is_accessible_through_HTTP(public_ip, domain): + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) - try : - r = requests.get("http://dns-api.org/A/"+domain) - except : + +def _dns_ip_match_public_ip(public_ip, domain): + try: + r = requests.get("http://dns-api.org/A/" + domain) + except: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org")) - - if (r.text == "[{\"error\":\"NXDOMAIN\"}]") : + + if (r.text == "[{\"error\":\"NXDOMAIN\"}]"): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) - try : + try: dns_ip = json.loads(r.text)[0]["value"] - except : + except: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text)) - if (dns_ip != public_ip) : + if (dns_ip != public_ip): return False - else : + else: return True -def _domain_is_accessible_through_HTTP(ip, domain) : - try : - requests.head("http://"+ip, headers = { "Host" : domain }) - except Exception : +def _domain_is_accessible_through_HTTP(ip, domain): + try: + requests.head("http://" + ip, headers={"Host": domain}) + except Exception: return False return True -def _summary_code_to_string(code) : - if (code <= -30) : return "CRITICAL" - elif (code <= -20) : return "WARNING" - elif (code <= -10) : return "Attention" - elif (code <= 0) : return "Unknown?" - elif (code <= 10) : return "Good" - elif (code <= 20) : return "Great!" +def _summary_code_to_string(code): + if (code <= -30): + return "CRITICAL" + elif (code <= -20): + return "WARNING" + elif (code <= -10): + return "Attention" + elif (code <= 0): + return "Unknown?" + elif (code <= 10): + return "Good" + elif (code <= 20): + return "Great!" return "Unknown?" -def _name_selfCA() : +def _name_selfCA(): cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) return cert.get_subject().CN def _tail(n, filePath): - stdin,stdout = os.popen2("tail -n "+str(n)+" "+filePath) - stdin.close() - lines = stdout.readlines(); stdout.close() - lines = "".join(lines) - return lines + stdin, stdout = os.popen2("tail -n " + str(n) + " " + filePath) + stdin.close() + lines = stdout.readlines() + stdout.close() + lines = "".join(lines) + return lines From 32bf7423676938f88d561038781dfed94d37dbf4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:25:23 +0100 Subject: [PATCH 04/82] [mod] trailing spaces --- src/yunohost/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2326995ae..c8e9ff481 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1037,11 +1037,11 @@ def app_ssowatconf(auth): # Check ACME challenge file is present in nginx conf nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue - + # Check the file contains the ACME challenge uri acme_uri = '/.well-known/acme-challenge' if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue - + # If so, then authorize the ACME challenge uri to unprotected regex regex = domain+"/%.well%-known/acme%-challenge/.*$" unprotected_regex.append(regex) From 02fc92d21083b869a7f8afe1ed34c24b2d0a0601 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:27:12 +0100 Subject: [PATCH 05/82] [mod] pep8 --- src/yunohost/app.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index c8e9ff481..e3d5ad202 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1035,15 +1035,19 @@ def app_ssowatconf(auth): for domain in domains: # Check ACME challenge file is present in nginx conf - nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/"+domain+".d/000-acmechallenge.conf" - if not (os.path.isfile(nginx_acme_challenge_conf_file)) : continue + nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain + + if not os.path.isfile(nginx_acme_challenge_conf_file): + continue # Check the file contains the ACME challenge uri acme_uri = '/.well-known/acme-challenge' - if not (acme_uri in open(nginx_acme_challenge_conf_file).read()) : continue + + if not acme_uri in open(nginx_acme_challenge_conf_file).read(): + continue # If so, then authorize the ACME challenge uri to unprotected regex - regex = domain+"/%.well%-known/acme%-challenge/.*$" + regex = domain + "/%.well%-known/acme%-challenge/.*$" unprotected_regex.append(regex) From 6a1727da89f26de8b10a7a83099af245e21e7cfb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:27:39 +0100 Subject: [PATCH 06/82] [mod] remove useless imports --- src/yunohost/domain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 58ca41b08..614409fa7 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -24,10 +24,8 @@ Manage domains """ import os -import sys import datetime import re -import shutil import json import yaml import errno From ef6287795299baf7b4b89d28387f5b9feca8ae54 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:29:16 +0100 Subject: [PATCH 07/82] [mod] pep8 --- src/yunohost/domain.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 614409fa7..1a296ee97 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -30,14 +30,16 @@ import json import yaml import errno import requests + from urllib import urlopen -from moulinette.core import MoulinetteError +from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger -from yunohost.service import service_regen_conf import yunohost.certificate +from yunohost.service import service_regen_conf + logger = getActionLogger('yunohost.domain') @@ -115,7 +117,7 @@ def domain_add(auth, domain, dyndns=False): yunohost.certificate.certificate_install_selfsigned([domain], False) try: - auth.validate_uniqueness({ 'virtualdomain': domain }) + auth.validate_uniqueness({'virtualdomain': domain}) except MoulinetteError: raise MoulinetteError(errno.EEXIST, m18n.n('domain_exists')) @@ -254,15 +256,19 @@ def domain_dns_conf(domain, ttl=None): return result + def domain_cert_status(auth, domainList, full=False): return yunohost.certificate.certificate_status(auth, domainList, full) + def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False): return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed) + def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False): return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email) + def get_public_ip(protocol=4): """Retrieve the public IP address from ip.yunohost.org""" if protocol == 4: @@ -278,4 +284,3 @@ def get_public_ip(protocol=4): logger.debug('cannot retrieve public IPv%d' % protocol, exc_info=1) raise MoulinetteError(errno.ENETUNREACH, m18n.n('no_internet_connection')) - From aff4dc40868b8e0fbfbc740bed17bc9ffd3991bb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:42:21 +0100 Subject: [PATCH 08/82] [mod] more verbose error --- locales/en.json | 2 +- src/yunohost/certificate.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 35da2eef0..c2f7b7402 100644 --- a/locales/en.json +++ b/locales/en.json @@ -249,7 +249,7 @@ "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", - "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s})", + "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e11ea9e3b..9d19d84cd 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -491,8 +491,10 @@ def _get_status(domain): try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) - except: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file)) + except Exception as exception: + import traceback + traceback.print_exc(file=sys.stdout) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) certSubject = cert.get_subject().CN certIssuer = cert.get_issuer().CN From 917c23073581d2f2d5bbc814ef50c48b8426af5c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:53:16 +0100 Subject: [PATCH 09/82] [mod] more pythonic and explicit tests with more verbose errors --- locales/en.json | 2 +- src/yunohost/certificate.py | 27 +++++++++++++++------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/locales/en.json b/locales/en.json index c2f7b7402..bdfd1b14a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,7 +245,7 @@ "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", - "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}). Use --no-checks to disable checks.", + "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}), reason: {reason:s}. Use --no-checks to disable checks.", "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9d19d84cd..89c00943c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -589,22 +589,25 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: - r = requests.get("http://dns-api.org/A/" + domain) - except: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org")) + result = requests.get("http://dns-api.org/A/" + domain) + except Exception as exception: + import traceback + traceback.print_exc(file=sys.stdout) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org", reason=exception)) - if (r.text == "[{\"error\":\"NXDOMAIN\"}]"): + dns_ip = result.json() + if not dns_ip or "value" not in dns_ip[0]: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) + + dns_ip = dns_ip[0]["value"] + + if dns_ip.get("error") == "NXDOMAIN": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) - try: - dns_ip = json.loads(r.text)[0]["value"] - except: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=r.text)) - - if (dns_ip != public_ip): - return False - else: + if dns_ip == public_ip: return True + else: + return False def _domain_is_accessible_through_HTTP(ip, domain): From 29f5f2d7534da9edaae2cd97ea9adb7b27cb09cb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 04:56:06 +0100 Subject: [PATCH 10/82] [mod] more pythonic code --- src/yunohost/certificate.py | 127 ++++++++++++++++++++---------------- 1 file changed, 70 insertions(+), 57 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 89c00943c..c318b63c2 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -25,12 +25,12 @@ """ import os +import sys import errno import requests import shutil import pwd import grp -import json import smtplib from OpenSSL import crypto @@ -100,18 +100,23 @@ def certificate_status(auth, domainList, full=False): headers = ["Domain", "Certificate status", "Authority type", "Days remaining"] else: headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] + lines = [] for domain in domainList: status = _get_status(domain) line = [] line.append(domain) - if (full): + + if full: line.append(status["subject"]) + line.append(_summary_code_to_string(status["summaryCode"])) line.append(status["CAtype"]) - if (full): + + if full: line.append(status["CAname"]) + line.append(status["validity"]) lines.append(line) @@ -142,20 +147,19 @@ def certificate_install_selfsigned(domainList, force=False): # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if (status != {}) and (status["summaryCode"] > 0) and (not force): + + if status != {} and status["summaryCode"] > 0 and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - cert_folder_domain = cert_folder + "/" + domain + cert_folder_domain = os.path.join(cert_folder, domain) # Create cert folder if it does not exists yet - try: - os.listdir(cert_folder_domain) - except OSError: + if not os.path.exists(cert_folder_domain): os.makedirs(cert_folder_domain) # Get serial ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - with open('%s/serial' % ssl_dir, 'r') as f: + with open(os.path.join(ssl_dir, 'serial'), 'r') as f: serial = f.readline().rstrip() # FIXME : should refactor this to avoid so many os.system() calls... @@ -178,25 +182,22 @@ def certificate_install_selfsigned(domainList, force=False): raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) _set_permissions(cert_folder_domain, "root", "root", 0755) - _set_permissions(cert_folder_domain + "/key.pem", "root", "metronome", 0640) - _set_permissions(cert_folder_domain + "/crt.pem", "root", "metronome", 0640) - _set_permissions(cert_folder_domain + "/openssl.cnf", "root", "root", 0600) + _set_permissions(os.path.join(cert_folder_domain, "key.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) -# Install ACME / Let's Encrypt certificate - def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): if not os.path.exists(account_key_file): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if (domainList == []): + if domainList == []: for domain in yunohost.domain.domain_list(auth)['domains']: - # Is it self-signed ? status = _get_status(domain) - if (status["CAtype"] != "Self-signed"): + if status["CAtype"] != "Self-signed": continue domainList.append(domain) @@ -210,7 +211,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal # Is it self-signed ? status = _get_status(domain) - if (not force) and (status["CAtype"] != "Self-signed"): + if not force and status["CAtype"] != "Self-signed": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) # Actual install steps @@ -219,9 +220,9 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.info("Now attempting install of certificate for domain " + domain + " !") try: - if not no_checks: _check_domain_is_correctly_configured(domain) + _backup_current_cert(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) @@ -230,14 +231,10 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: - - logger.error("Certificate installation for " + domain + " failed !") + logger.error("Certificate installation for %s failed !" % domain) logger.error(str(e)) -# Renew - - def certificate_renew(auth, domainList, force=False, no_checks=False, email=False): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -257,7 +254,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # Does it has a Let's Encrypt cert ? status = _get_status(domain) - if (status["CAtype"] != "Let's Encrypt"): + if status["CAtype"] != "Let's Encrypt": continue # Does it expires soon ? @@ -278,20 +275,18 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals status = _get_status(domain) # Does it expires soon ? - if not ((force) or (status["validity"] <= validity_limit)): + if not (force or status["validity"] <= validity_limit): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? - if (status["CAtype"] != "Let's Encrypt"): + if status["CAtype"] != "Let's Encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) # Actual renew steps for domain in domainList: - logger.info("Now attempting renewing of certificate for domain " + domain + " !") try: - if not no_checks: _check_domain_is_correctly_configured(domain) _backup_current_cert(domain) @@ -300,11 +295,10 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: - logger.error("Certificate renewing for " + domain + " failed !") logger.error(str(e)) - if (email): + if email: logger.error("Sending email with details to root ...") _email_renewing_failed(domain, e) @@ -317,7 +311,6 @@ def _install_cron(): cron_job_file = "/etc/cron.weekly/certificateRenewer" with open(cron_job_file, "w") as f: - f.write("#!/bin/bash\n") f.write("yunohost domain cert-renew --email\n") @@ -325,9 +318,9 @@ def _install_cron(): def _email_renewing_failed(domain, e): - from_ = "certmanager@" + domain + " (Certificate Manager)" + from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" - subject_ = "Certificate renewing attempt for " + domain + " failed!" + subject_ = "Certificate renewing attempt for %s failed!" % domain exceptionMessage = str(e) logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") @@ -360,7 +353,7 @@ Subject: %s def _configure_for_acme_challenge(auth, domain): - nginx_conf_file = "/etc/nginx/conf.d/" + domain + ".d/000-acmechallenge.conf" + nginx_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain nginx_configuration = ''' location '/.well-known/acme-challenge' @@ -375,6 +368,7 @@ location '/.well-known/acme-challenge' logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") else: logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") + with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) @@ -390,17 +384,19 @@ def _fetch_and_enable_new_certificate(domain): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") - if not (os.path.exists(webroot_folder)): + if not os.path.exists(webroot_folder): os.makedirs(webroot_folder) - if not (os.path.exists(tmp_folder)): + + if not os.path.exists(tmp_folder): os.makedirs(tmp_folder) + _set_permissions(webroot_folder, "root", "www-data", 0650) _set_permissions(tmp_folder, "root", "root", 0640) # Prepare certificate signing request logger.info("Prepare key and certificate signing request (CSR) for " + domain + "...") - domain_key_file = tmp_folder + "/" + domain + ".pem" + domain_key_file = "%s/%s.pem" % (tmp_folder, domain) _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "metronome", 0640) @@ -409,13 +405,14 @@ def _fetch_and_enable_new_certificate(domain): # Sign the certificate logger.info("Now using ACME Tiny to sign the certificate...") - domain_csr_file = tmp_folder + "/" + domain + ".csr" + domain_csr_file = "%s/%s.csr" % (tmp_folder, domain) signed_certificate = sign_certificate(account_key_file, domain_csr_file, webroot_folder, log=logger, CA=certification_authority) + intermediate_certificate = requests.get(intermediate_certificate_url).text # Now save the key and signed certificate @@ -423,42 +420,47 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = cert_folder + "/" + domain + "." + date_tag + + new_cert_folder = "%s/%s.%s" % (cert_folder, domain, date_tag) os.makedirs(new_cert_folder) + _set_permissions(new_cert_folder, "root", "root", 0655) # Move the private key - shutil.move(domain_key_file, new_cert_folder + "/key.pem") + shutil.move(domain_key_file, os.path.join(new_cert_folder, "key.pem")) # Write the cert - domain_cert_file = new_cert_folder + "/crt.pem" + domain_cert_file = os.path.join(new_cert_folder, "crt.pem") + with open(domain_cert_file, "w") as f: f.write(signed_certificate) f.write(intermediate_certificate) + _set_permissions(domain_cert_file, "root", "metronome", 0640) logger.info("Enabling the new certificate...") # Replace (if necessary) the link or folder for live cert - live_link = cert_folder + "/" + domain + live_link = os.path.join(cert_folder, domain) if not os.path.islink(live_link): shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + elif os.path.lexists(live_link): os.remove(live_link) os.symlink(new_cert_folder, live_link) # Check the status of the certificate is now good - statusSummaryCode = _get_status(domain)["summaryCode"] - if (statusSummaryCode < 20): + + if statusSummaryCode < 20: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) logger.info("Restarting services...") - for s in ["nginx", "postfix", "dovecot", "metronome"]: - _run_service_command("restart", s) + for service in ("nginx", "postfix", "dovecot", "metronome"): + _run_service_command("restart", service) def _prepare_certificate_signing_request(domain, key_file, output_folder): @@ -471,6 +473,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Set the key with open(key_file, 'rt') as f: key = crypto.load_privatekey(crypto.FILETYPE_PEM, f.read()) + csr.set_pubkey(key) # Sign the request @@ -479,6 +482,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Save the request in tmp folder csr_file = output_folder + domain + ".csr" logger.info("Saving to " + csr_file + " .") + with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) @@ -486,7 +490,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): cert_file = cert_folder + "/" + domain + "/crt.pem" - if (not os.path.isfile(cert_file)): + if not os.path.isfile(cert_file): return {} try: @@ -504,38 +508,47 @@ def _get_status(domain): CAtype = None if (certIssuer == _name_selfCA()): CAtype = "Self-signed" + elif (certIssuer.startswith("Let's Encrypt")): CAtype = "Let's Encrypt" + elif (certIssuer.startswith("Fake LE")): CAtype = "Fake Let's Encrypt" + else: CAtype = "Other / Unknown" # Unknown by default statusSummaryCode = 0 + # Critical if (daysRemaining <= 0): statusSummaryCode = -30 + # Warning, self-signed, browser will display a warning discouraging visitors to enter website elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt"): statusSummaryCode = -20 + # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) elif (daysRemaining < validity_limit): statusSummaryCode = -10 + # CA not known, but still a valid certificate, so okay ! elif (CAtype == "Other / Unknown"): statusSummaryCode = 10 + # Let's Encrypt, great ! elif (CAtype == "Let's Encrypt"): statusSummaryCode = 20 - return {"domain": domain, - "subject": certSubject, - "CAname": certIssuer, - "CAtype": CAtype, - "validity": daysRemaining, - "summaryCode": statusSummaryCode - } + return { + "domain": domain, + "subject": certSubject, + "CAname": certIssuer, + "CAtype": CAtype, + "validity": daysRemaining, + "summaryCode": statusSummaryCode + } ############################################################################### # Misc small stuff ... # @@ -567,10 +580,10 @@ def _set_permissions(path, user, group, permissions): def _backup_current_cert(domain): logger.info("Backuping existing certificate for domain " + domain) - cert_folder_domain = cert_folder + "/" + domain + cert_folder_domain = os.path.join(cert_folder, domain) dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = cert_folder_domain + "-backup-" + dateTag + backup_folder = "%s-backup-%s" % (cert_folder_domain, dateTag) shutil.copytree(cert_folder_domain, backup_folder) From 39aaa1763984c0ee48eef15c4fc6b2a2fb1028db Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:08:44 +0100 Subject: [PATCH 11/82] [mod] remove useless () --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c318b63c2..f94a2d4e6 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -86,7 +86,7 @@ def certificate_status(auth, domainList, full=False): """ # If no domains given, consider all yunohost domains - if (domainList == []): + if domainList == []: domainList = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given else: From 7dbbd7fdc2bb919af490f213573da5221ce82ce6 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:19:15 +0100 Subject: [PATCH 12/82] [fix] correctly handle all cases --- src/yunohost/certificate.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f94a2d4e6..5dc16cba8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -608,15 +608,24 @@ def _dns_ip_match_public_ip(public_ip, domain): traceback.print_exc(file=sys.stdout) raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org", reason=exception)) - dns_ip = result.json() - if not dns_ip or "value" not in dns_ip[0]: + try: + dns_ip = result.json() + except Exception as exception: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - dns_ip = dns_ip[0]["value"] + if len(dns_ip) == 0: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) + + dns_ip = dns_ip[0] if dns_ip.get("error") == "NXDOMAIN": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) + if "value" not in dns_ip: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) + + dns_ip = dns_ip["value"] + if dns_ip == public_ip: return True else: From ea179b1683b9ac96eb7fbb8ab493319cb7fd5a72 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:20:13 +0100 Subject: [PATCH 13/82] [mod] simplier condition --- src/yunohost/certificate.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5dc16cba8..7d96aebca 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -626,10 +626,7 @@ def _dns_ip_match_public_ip(public_ip, domain): dns_ip = dns_ip["value"] - if dns_ip == public_ip: - return True - else: - return False + return dns_ip == public_ip def _domain_is_accessible_through_HTTP(ip, domain): From 9a4dbd5d31abb3956f55956875ce0f9f863295eb Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:20:40 +0100 Subject: [PATCH 14/82] [fix] uses https --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 7d96aebca..932bf1855 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -602,7 +602,7 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: - result = requests.get("http://dns-api.org/A/" + domain) + result = requests.get("https://dns-api.org/A/" + domain) except Exception as exception: import traceback traceback.print_exc(file=sys.stdout) From ac9f61c7b1f808c2e93c977831a9a54a7d0552b5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:21:27 +0100 Subject: [PATCH 15/82] [mod] more pythonic code --- src/yunohost/certificate.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 932bf1855..5f8c7ff2c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -639,17 +639,22 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _summary_code_to_string(code): - if (code <= -30): + if code <= -30: return "CRITICAL" - elif (code <= -20): - return "WARNING" - elif (code <= -10): - return "Attention" - elif (code <= 0): - return "Unknown?" - elif (code <= 10): - return "Good" - elif (code <= 20): + + if code <= -20: + return "WARNING" + + if code <= -10: + return "Attention" + + if code <= 0: + return "Unknown?" + + if code <= 10: + return "Good" + + if code <= 20: return "Great!" return "Unknown?" From ac901528c325bfc7f62e7a3c5c7f1a8c43f3d18a Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:22:30 +0100 Subject: [PATCH 16/82] [mod] uses + for strings --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5f8c7ff2c..de21299d1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -666,7 +666,7 @@ def _name_selfCA(): def _tail(n, filePath): - stdin, stdout = os.popen2("tail -n " + str(n) + " " + filePath) + stdin, stdout = os.popen2("tail -n %s '%s'" % (n, filePath)) stdin.close() lines = stdout.readlines() stdout.close() From cd21edb26704250305275f61fdbca9c357d9f8aa Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:22:43 +0100 Subject: [PATCH 17/82] [mod] pep8 --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index de21299d1..94defcd2b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -665,8 +665,8 @@ def _name_selfCA(): return cert.get_subject().CN -def _tail(n, filePath): - stdin, stdout = os.popen2("tail -n %s '%s'" % (n, filePath)) +def _tail(n, file_path): + stdin, stdout = os.popen2("tail -n %s '%s'" % (n, file_path)) stdin.close() lines = stdout.readlines() stdout.close() From a7b9226667935158501fe39a56bcd1066359fa18 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:23:32 +0100 Subject: [PATCH 18/82] [mod] lisibility --- src/yunohost/certificate.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 94defcd2b..b1efb3725 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -667,8 +667,10 @@ def _name_selfCA(): def _tail(n, file_path): stdin, stdout = os.popen2("tail -n %s '%s'" % (n, file_path)) + stdin.close() + lines = stdout.readlines() stdout.close() - lines = "".join(lines) - return lines + + return "".join(lines) From dd893b0838d78e2588102ee71b6c70c068bc34c5 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:26:52 +0100 Subject: [PATCH 19/82] [mod] remove useless () --- src/yunohost/certificate.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b1efb3725..1196a2e93 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -506,13 +506,13 @@ def _get_status(domain): daysRemaining = (validUpTo - datetime.now()).days CAtype = None - if (certIssuer == _name_selfCA()): + if certIssuer == _name_selfCA(): CAtype = "Self-signed" - elif (certIssuer.startswith("Let's Encrypt")): + elif certIssuer.startswith("Let's Encrypt"): CAtype = "Let's Encrypt" - elif (certIssuer.startswith("Fake LE")): + elif certIssuer.startswith("Fake LE"): CAtype = "Fake Let's Encrypt" else: @@ -522,23 +522,23 @@ def _get_status(domain): statusSummaryCode = 0 # Critical - if (daysRemaining <= 0): + if daysRemaining <= 0: statusSummaryCode = -30 # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif (CAtype == "Self-signed") or (CAtype == "Fake Let's Encrypt"): + elif CAtype == "Self-signed" or CAtype == "Fake Let's Encrypt": statusSummaryCode = -20 # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) - elif (daysRemaining < validity_limit): + elif daysRemaining < validity_limit: statusSummaryCode = -10 # CA not known, but still a valid certificate, so okay ! - elif (CAtype == "Other / Unknown"): + elif CAtype == "Other / Unknown": statusSummaryCode = 10 # Let's Encrypt, great ! - elif (CAtype == "Let's Encrypt"): + elif CAtype == "Let's Encrypt": statusSummaryCode = 20 return { From 34f98905175033f5ecf5575797517df573b72123 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:29:10 +0100 Subject: [PATCH 20/82] [mod] more pythonic string concatenation --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 1196a2e93..12ee4dbf8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -359,15 +359,15 @@ def _configure_for_acme_challenge(auth, domain): location '/.well-known/acme-challenge' { default_type "text/plain"; - alias ''' + webroot_folder + '''; + alias %s; } - ''' + ''' % webroot_folder # Write the conf if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") else: - logger.info("Adding Nginx configuration file for Acme challenge for domain " + domain + ".") + logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) @@ -394,7 +394,7 @@ def _fetch_and_enable_new_certificate(domain): _set_permissions(tmp_folder, "root", "root", 0640) # Prepare certificate signing request - logger.info("Prepare key and certificate signing request (CSR) for " + domain + "...") + logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) domain_key_file = "%s/%s.pem" % (tmp_folder, domain) _generate_key(domain_key_file) From 55d007f13037a0efc2ffe1661ec7e907e466fec7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:30:54 +0100 Subject: [PATCH 21/82] [mod] avoid useless indentation --- src/yunohost/certificate.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 12ee4dbf8..f83f98570 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -366,18 +366,19 @@ location '/.well-known/acme-challenge' # Write the conf if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") - else: - logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) + return - with open(nginx_conf_file, "w") as f: - f.write(nginx_configuration) + logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) - # Assume nginx conf is okay, and reload it - # (FIXME : maybe add a check that it is, using nginx -t, haven't found - # any clean function already implemented in yunohost to do this though) - _run_service_command("reload", "nginx") + with open(nginx_conf_file, "w") as f: + f.write(nginx_configuration) - app_ssowatconf(auth) + # Assume nginx conf is okay, and reload it + # (FIXME : maybe add a check that it is, using nginx -t, haven't found + # any clean function already implemented in yunohost to do this though) + _run_service_command("reload", "nginx") + + app_ssowatconf(auth) def _fetch_and_enable_new_certificate(domain): From e8e07d464add2dc54ac84a613fc64a970354a87c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:31:36 +0100 Subject: [PATCH 22/82] [mod] remove useless variable --- src/yunohost/certificate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f83f98570..426e0f66a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -317,12 +317,11 @@ def _install_cron(): _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, e): +def _email_renewing_failed(domain, exceptionMessage): from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" subject_ = "Certificate renewing attempt for %s failed!" % domain - exceptionMessage = str(e) logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") text = """ At attempt for renewing the certificate for domain %s failed with the following From 5615e3f4fe7dc9045bfa699b7e43484bb9b4dba3 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:33:21 +0100 Subject: [PATCH 23/82] [mod] uses logger string concatenation api --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 426e0f66a..c35d15f8e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -284,7 +284,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # Actual renew steps for domain in domainList: - logger.info("Now attempting renewing of certificate for domain " + domain + " !") + logger.info("Now attempting renewing of certificate for domain %s !", domain) try: if not no_checks: @@ -295,7 +295,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: - logger.error("Certificate renewing for " + domain + " failed !") + logger.error("Certificate renewing for %s failed !", domain) logger.error(str(e)) if email: From 8ca5d59a96366c1ec285f5e400e8e34d01a4d3b2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:33:57 +0100 Subject: [PATCH 24/82] [mod] remove useless () --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c35d15f8e..9562a3a5f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -249,7 +249,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if (domainList == []): + if domainList == []: for domain in yunohost.domain.domain_list(auth)['domains']: # Does it has a Let's Encrypt cert ? @@ -258,10 +258,10 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals continue # Does it expires soon ? - if (force) or (status["validity"] <= validity_limit): + if force or status["validity"] <= validity_limit: domainList.append(domain) - if (len(domainList) == 0): + if len(domainList) == 0: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given @@ -275,7 +275,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals status = _get_status(domain) # Does it expires soon ? - if not (force or status["validity"] <= validity_limit): + if not force or status["validity"] <= validity_limit: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? From 65e9a4b6d8ac456b61a1bc9fdd59c012f10003b4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:34:30 +0100 Subject: [PATCH 25/82] [mod] uses logger string concatenation api --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9562a3a5f..f903c266a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -217,7 +217,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal # Actual install steps for domain in domainList: - logger.info("Now attempting install of certificate for domain " + domain + " !") + logger.info("Now attempting install of certificate for domain %s!", domain) try: if not no_checks: @@ -231,7 +231,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: - logger.error("Certificate installation for %s failed !" % domain) + logger.error("Certificate installation for %s failed !", domain) logger.error(str(e)) From c1252120a13f720cd811f9379fbc86f91a17734f Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:36:02 +0100 Subject: [PATCH 26/82] [mod] typo --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f903c266a..a547d2000 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -91,7 +91,7 @@ def certificate_status(auth, domainList, full=False): # Else, validate that yunohost knows the domains given else: for domain in domainList: - # Is it in Yunohost dmomain list ? + # Is it in Yunohost domain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) From 02b5ea62feff550a522d4dead957ee6ed048dc4e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:42:16 +0100 Subject: [PATCH 27/82] [mod] pep8 --- data/actionsmap/yunohost.yml | 6 ++--- src/yunohost/certificate.py | 48 ++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 4d0db97ec..8f08a98b8 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -313,7 +313,7 @@ domain: authenticate: all authenticator: ldap-anonymous arguments: - domainList: + domain_list: help: Domains to check nargs: "*" --full: @@ -328,7 +328,7 @@ domain: authenticate: all authenticator: ldap-anonymous arguments: - domainList: + domain_list: help: Domains for which to install the certificates nargs: "*" --force: @@ -349,7 +349,7 @@ domain: authenticate: all authenticator: ldap-anonymous arguments: - domainList: + domain_list: help: Domains for which to renew the certificates nargs: "*" --force: diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index a547d2000..c2f51270d 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -76,21 +76,21 @@ intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cr # Status -def certificate_status(auth, domainList, full=False): +def certificate_status(auth, domain_list, full=False): """ Print the status of certificate for given domains (all by default) Keyword argument: - domainList -- Domains to be checked + domain_list -- Domains to be checked full -- Display more info about the certificates """ # If no domains given, consider all yunohost domains - if domainList == []: - domainList = yunohost.domain.domain_list(auth)['domains'] + if domain_list == []: + domain_list = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given else: - for domain in domainList: + for domain in domain_list: # Is it in Yunohost domain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) @@ -102,7 +102,7 @@ def certificate_status(auth, domainList, full=False): headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] lines = [] - for domain in domainList: + for domain in domain_list: status = _get_status(domain) line = [] @@ -123,27 +123,27 @@ def certificate_status(auth, domainList, full=False): print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) -def certificate_install(auth, domainList, force=False, no_checks=False, self_signed=False): +def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): """ Install a Let's Encrypt certificate for given domains (all by default) Keyword argument: - domainList -- Domains on which to install certificates + domain_list -- Domains on which to install certificates force -- Install even if current certificate is not self-signed no-check -- Disable some checks about the reachability of web server before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt """ if (self_signed): - certificate_install_selfsigned(domainList, force) + certificate_install_selfsigned(domain_list, force) else: - certificate_install_letsencrypt(auth, domainList, force, no_checks) + certificate_install_letsencrypt(auth, domain_list, force, no_checks) # Install self-signed -def certificate_install_selfsigned(domainList, force=False): - for domain in domainList: +def certificate_install_selfsigned(domain_list, force=False): + for domain in domain_list: # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) @@ -187,24 +187,24 @@ def certificate_install_selfsigned(domainList, force=False): _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) -def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=False): +def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): if not os.path.exists(account_key_file): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed # certificates - if domainList == []: + if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: status = _get_status(domain) if status["CAtype"] != "Self-signed": continue - domainList.append(domain) + domain_list.append(domain) # Else, validate that yunohost knows the domains given else: - for domain in domainList: + for domain in domain_list: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) @@ -215,7 +215,7 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) # Actual install steps - for domain in domainList: + for domain in domain_list: logger.info("Now attempting install of certificate for domain %s!", domain) @@ -235,12 +235,12 @@ def certificate_install_letsencrypt(auth, domainList, force=False, no_checks=Fal logger.error(str(e)) -def certificate_renew(auth, domainList, force=False, no_checks=False, email=False): +def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False): """ Renew Let's Encrypt certificate for given domains (all by default) Keyword argument: - domainList -- Domains for which to renew the certificates + domain_list -- Domains for which to renew the certificates force -- Ignore the validity threshold (15 days) no-check -- Disable some checks about the reachability of web server before attempting the renewing @@ -249,7 +249,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # If no domains given, consider all yunohost domains with Let's Encrypt # certificates - if domainList == []: + if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: # Does it has a Let's Encrypt cert ? @@ -259,14 +259,14 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals # Does it expires soon ? if force or status["validity"] <= validity_limit: - domainList.append(domain) + domain_list.append(domain) - if len(domainList) == 0: + if len(domain_list) == 0: logger.info("No certificate needs to be renewed.") # Else, validate the domain list given else: - for domain in domainList: + for domain in domain_list: # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: @@ -283,7 +283,7 @@ def certificate_renew(auth, domainList, force=False, no_checks=False, email=Fals raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) # Actual renew steps - for domain in domainList: + for domain in domain_list: logger.info("Now attempting renewing of certificate for domain %s !", domain) try: From 8b24ab73c25db0ce095c25bf9c03644990b47180 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:42:49 +0100 Subject: [PATCH 28/82] [mod] small opti, getting domain list can be slow --- src/yunohost/certificate.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c2f51270d..73e9ae22b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -90,9 +90,10 @@ def certificate_status(auth, domain_list, full=False): domain_list = yunohost.domain.domain_list(auth)['domains'] # Else, validate that yunohost knows the domains given else: + yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] for domain in domain_list: # Is it in Yunohost domain list ? - if domain not in yunohost.domain.domain_list(auth)['domains']: + if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Get status for each domain, and prepare display using tabulate From 3b5cadb907f1ca00d1c3710221f57dca4befce41 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:43:32 +0100 Subject: [PATCH 29/82] [mod] realign stuff --- src/yunohost/certificate.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 73e9ae22b..81219eab5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -82,7 +82,7 @@ def certificate_status(auth, domain_list, full=False): Keyword argument: domain_list -- Domains to be checked - full -- Display more info about the certificates + full -- Display more info about the certificates """ # If no domains given, consider all yunohost domains @@ -130,10 +130,10 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si Keyword argument: domain_list -- Domains on which to install certificates - force -- Install even if current certificate is not self-signed - no-check -- Disable some checks about the reachability of web server + force -- Install even if current certificate is not self-signed + no-check -- Disable some checks about the reachability of web server before attempting the install - self-signed -- Instal self-signed certificates instead of Let's Encrypt + self-signed -- Instal self-signed certificates instead of Let's Encrypt """ if (self_signed): certificate_install_selfsigned(domain_list, force) From 487e1d25888939b9d6af577ca21e2dfac305a2ac Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 05:54:45 +0100 Subject: [PATCH 30/82] [mod] pylint --- src/yunohost/certificate.py | 98 ++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 50 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 81219eab5..b62118257 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -17,9 +17,7 @@ You should have received a copy of the GNU Affero General Public License along with this program; if not, see http://www.gnu.org/licenses -""" - -""" yunohost_certificate.py + yunohost_certificate.py Manage certificates, in particular Let's encrypt """ @@ -27,22 +25,22 @@ import os import sys import errno -import requests import shutil import pwd import grp import smtplib +import requests from OpenSSL import crypto from datetime import datetime from tabulate import tabulate from acme_tiny import get_crt as sign_certificate -import yunohost.domain - from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger +import yunohost.domain + from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command @@ -135,7 +133,7 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt """ - if (self_signed): + if self_signed: certificate_install_selfsigned(domain_list, force) else: certificate_install_letsencrypt(auth, domain_list, force, no_checks) @@ -318,13 +316,13 @@ def _install_cron(): _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, exceptionMessage): +def _email_renewing_failed(domain, exception_message): from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" subject_ = "Certificate renewing attempt for %s failed!" % domain logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") - text = """ + text = """ At attempt for renewing the certificate for domain %s failed with the following error : @@ -337,7 +335,7 @@ investigate : -- Certificate Manager -""" % (domain, exceptionMessage, logs) +""" % (domain, exception_message, logs) message = """ From: %s @@ -397,7 +395,7 @@ def _fetch_and_enable_new_certificate(domain): # Prepare certificate signing request logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) - domain_key_file = "%s/%s.pem" % (tmp_folder, domain) + domain_key_file = "%s/%s.pem" % (tmp_folder, domain) _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "metronome", 0640) @@ -453,9 +451,9 @@ def _fetch_and_enable_new_certificate(domain): os.symlink(new_cert_folder, live_link) # Check the status of the certificate is now good - statusSummaryCode = _get_status(domain)["summaryCode"] + status_summary_code = _get_status(domain)["summaryCode"] - if statusSummaryCode < 20: + if status_summary_code < 20: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) logger.info("Restarting services...") @@ -501,54 +499,54 @@ def _get_status(domain): traceback.print_exc(file=sys.stdout) raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) - certSubject = cert.get_subject().CN - certIssuer = cert.get_issuer().CN - validUpTo = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") - daysRemaining = (validUpTo - datetime.now()).days + cert_subject = cert.get_subject().CN + cert_issuer = cert.get_issuer().CN + valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") + days_remaining = (valid_up_to - datetime.now()).days - CAtype = None - if certIssuer == _name_selfCA(): - CAtype = "Self-signed" + CA_type = None + if cert_issuer == _name_self_CA(): + CA_type = "Self-signed" - elif certIssuer.startswith("Let's Encrypt"): - CAtype = "Let's Encrypt" + elif cert_issuer.startswith("Let's Encrypt"): + CA_type = "Let's Encrypt" - elif certIssuer.startswith("Fake LE"): - CAtype = "Fake Let's Encrypt" + elif cert_issuer.startswith("Fake LE"): + CA_type = "Fake Let's Encrypt" else: - CAtype = "Other / Unknown" + CA_type = "Other / Unknown" # Unknown by default - statusSummaryCode = 0 + status_summary_code = 0 # Critical - if daysRemaining <= 0: - statusSummaryCode = -30 + if days_remaining <= 0: + status_summary_code = -30 # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif CAtype == "Self-signed" or CAtype == "Fake Let's Encrypt": - statusSummaryCode = -20 + elif CA_type == "Self-signed" or CA_type == "Fake Let's Encrypt": + status_summary_code = -20 # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) - elif daysRemaining < validity_limit: - statusSummaryCode = -10 + elif days_remaining < validity_limit: + status_summary_code = -10 # CA not known, but still a valid certificate, so okay ! - elif CAtype == "Other / Unknown": - statusSummaryCode = 10 + elif CA_type == "Other / Unknown": + status_summary_code = 10 # Let's Encrypt, great ! - elif CAtype == "Let's Encrypt": - statusSummaryCode = 20 + elif CA_type == "Let's Encrypt": + status_summary_code = 20 return { "domain": domain, - "subject": certSubject, - "CAname": certIssuer, - "CAtype": CAtype, - "validity": daysRemaining, - "summaryCode": statusSummaryCode + "subject": cert_subject, + "CAname": cert_issuer, + "CAtype": CA_type, + "validity": days_remaining, + "summaryCode": status_summary_code } ############################################################################### @@ -562,11 +560,11 @@ def _generate_account_key(): _set_permissions(account_key_file, "root", "root", 0400) -def _generate_key(destinationPath): +def _generate_key(destination_path): k = crypto.PKey() k.generate_key(crypto.TYPE_RSA, key_size) - with open(destinationPath, "w") as f: + with open(destination_path, "w") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) @@ -583,8 +581,8 @@ def _backup_current_cert(domain): cert_folder_domain = os.path.join(cert_folder, domain) - dateTag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = "%s-backup-%s" % (cert_folder_domain, dateTag) + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + backup_folder = "%s-backup-%s" % (cert_folder_domain, date_tag) shutil.copytree(cert_folder_domain, backup_folder) @@ -644,16 +642,16 @@ def _summary_code_to_string(code): return "CRITICAL" if code <= -20: - return "WARNING" + return "WARNING" if code <= -10: - return "Attention" + return "Attention" if code <= 0: - return "Unknown?" + return "Unknown?" if code <= 10: - return "Good" + return "Good" if code <= 20: return "Great!" @@ -661,7 +659,7 @@ def _summary_code_to_string(code): return "Unknown?" -def _name_selfCA(): +def _name_self_CA(): cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) return cert.get_subject().CN From 3fc2d45a6ab139a101a78205cdaecb1912eb6c5e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:40:05 +0100 Subject: [PATCH 31/82] [mod] remove useless comments --- src/yunohost/certificate.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b62118257..43c64aa34 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -47,8 +47,6 @@ from yunohost.service import _run_service_command logger = getActionLogger('yunohost.certmanager') -# Misc stuff we need - cert_folder = "/etc/yunohost/certs/" tmp_folder = "/tmp/acme-challenge-private/" webroot_folder = "/tmp/acme-challenge-public/" @@ -71,8 +69,6 @@ intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cr # Front-end stuff # ############################################################################### -# Status - def certificate_status(auth, domain_list, full=False): """ @@ -139,8 +135,6 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si certificate_install_letsencrypt(auth, domain_list, force, no_checks) -# Install self-signed - def certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: @@ -152,7 +146,6 @@ def certificate_install_selfsigned(domain_list, force=False): cert_folder_domain = os.path.join(cert_folder, domain) - # Create cert folder if it does not exists yet if not os.path.exists(cert_folder_domain): os.makedirs(cert_folder_domain) From ac9f2643a755d5ebb2e2a11c4b5f08877b55e367 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:40:26 +0100 Subject: [PATCH 32/82] [mod] move a part of os.system calls to native shutil/os --- src/yunohost/certificate.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 43c64aa34..c8d55170c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -154,25 +154,32 @@ def certificate_install_selfsigned(domain_list, force=False): with open(os.path.join(ssl_dir, 'serial'), 'r') as f: serial = f.readline().rstrip() + shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(cert_folder_domain, "openssl.cnf")) + # FIXME : should refactor this to avoid so many os.system() calls... # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ - 'cp %s/openssl.cnf %s' % (ssl_dir, cert_folder_domain), 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' % (cert_folder_domain, ssl_dir, ssl_dir), 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' % (cert_folder_domain, ssl_dir, ssl_dir), - 'ln -s /etc/ssl/certs/ca-yunohost_crt.pem %s/ca.pem' % cert_folder_domain, - 'cp %s/certs/yunohost_key.pem %s/key.pem' % (ssl_dir, cert_folder_domain), - 'cp %s/newcerts/%s.pem %s/crt.pem' % (ssl_dir, serial, cert_folder_domain), - 'cat %s/ca.pem >> %s/crt.pem' % (cert_folder_domain, cert_folder_domain) ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(cert_folder_domain, "ca.pem")) + shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(cert_folder_domain, "key.pem")) + shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(cert_folder_domain, "crt.pem")) + + # append ca.pem at the end of crt.pem + with open(os.path.join(cert_folder_domain, "ca.pem"), "r") as ca_pem: + with open(os.path.join(cert_folder_domain, "crt.pem"), "a") as crt_pem: + crt_pem.write("\n") + crt_pem.write(ca_pem.read()) + _set_permissions(cert_folder_domain, "root", "root", 0755) _set_permissions(os.path.join(cert_folder_domain, "key.pem"), "root", "metronome", 0640) _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) From a1b42e65f8ddc61902fdc6066b7ea59d7de77cd7 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:49:40 +0100 Subject: [PATCH 33/82] [mod] remove useless variables --- src/yunohost/app.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index e3d5ad202..83fc8614c 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1041,14 +1041,11 @@ def app_ssowatconf(auth): continue # Check the file contains the ACME challenge uri - acme_uri = '/.well-known/acme-challenge' - - if not acme_uri in open(nginx_acme_challenge_conf_file).read(): + if not '/.well-known/acme-challenge' in open(nginx_acme_challenge_conf_file).read(): continue # If so, then authorize the ACME challenge uri to unprotected regex - regex = domain + "/%.well%-known/acme%-challenge/.*$" - unprotected_regex.append(regex) + unprotected_regex.append(domain + "/%.well%-known/acme%-challenge/.*$") conf_dict = { From bd41f0d4a2c1555ea63940db21713e73c9bfe823 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 13:55:04 +0100 Subject: [PATCH 34/82] [mod] os.path.join --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c8d55170c..e71d6cdeb 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -487,7 +487,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - cert_file = cert_folder + "/" + domain + "/crt.pem" + cert_file = os.path.join(cert_folder, domain, "crt.pem") if not os.path.isfile(cert_file): return {} From 248508dcaa824018d031aeb75e447df6bb8fb95c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 14:00:42 +0100 Subject: [PATCH 35/82] [mod] do not uses tabulate, the api needs json --- src/yunohost/certificate.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e71d6cdeb..c8ea58d71 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -90,32 +90,19 @@ def certificate_status(auth, domain_list, full=False): if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) - # Get status for each domain, and prepare display using tabulate - if not full: - headers = ["Domain", "Certificate status", "Authority type", "Days remaining"] - else: - headers = ["Domain", "Certificate subject", "Certificate status", "Authority type", "Authority name", "Days remaining"] - lines = [] + for domain in domain_list: status = _get_status(domain) + status["summaryCode"] = _summary_code_to_string(status["summaryCode"]) - line = [] - line.append(domain) + if not full: + del status["subject"] + del status["CAname"] - if full: - line.append(status["subject"]) + lines.append(status) - line.append(_summary_code_to_string(status["summaryCode"])) - line.append(status["CAtype"]) - - if full: - line.append(status["CAname"]) - - line.append(status["validity"]) - lines.append(line) - - print(tabulate(lines, headers=headers, tablefmt="simple", stralign="center")) + return lines def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): From 299aa978f717a9d3e3f04b9f0a3cbb6d2d031caa Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 14:06:34 +0100 Subject: [PATCH 36/82] [mod] simplify code --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index c8ea58d71..8c2662148 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -128,7 +128,7 @@ def certificate_install_selfsigned(domain_list, force=False): # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if status != {} and status["summaryCode"] > 0 and not force: + if status and status["summaryCode"] > 0 and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) cert_folder_domain = os.path.join(cert_folder, domain) From 289eb014806de3d7c4609bb989c4abbbc002b175 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 15:21:23 +0100 Subject: [PATCH 37/82] [mod] small opti, getting domain list can be long --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8c2662148..9a3c564b0 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -191,8 +191,8 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa # Else, validate that yunohost knows the domains given else: for domain in domain_list: - # Is it in Yunohost dmomain list ? - if domain not in yunohost.domain.domain_list(auth)['domains']: + yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] + if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) # Is it self-signed ? From fef1ca08c56d8b0c3530f861cca3982ad4bab619 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 30 Oct 2016 11:48:27 -0400 Subject: [PATCH 38/82] Changing name of cron job to be consistent with other yunohost crons, as requested by @opi --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 9a3c564b0..19e9cea38 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -294,7 +294,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal ############################################################################### def _install_cron(): - cron_job_file = "/etc/cron.weekly/certificateRenewer" + cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" with open(cron_job_file, "w") as f: f.write("#!/bin/bash\n") From 7f8aa4cc75d4e644bd80b148988cb4449ae51477 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 16:14:19 +0100 Subject: [PATCH 39/82] [mod] remove useless assign --- src/yunohost/certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 19e9cea38..aa8a77efb 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -491,7 +491,6 @@ def _get_status(domain): valid_up_to = datetime.strptime(cert.get_notAfter(), "%Y%m%d%H%M%SZ") days_remaining = (valid_up_to - datetime.now()).days - CA_type = None if cert_issuer == _name_self_CA(): CA_type = "Self-signed" From 2d89964bc7ef5aae06f253dd67a5d387e99c676b Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 17:15:06 +0100 Subject: [PATCH 40/82] [enh] include tracebak into error email --- src/yunohost/certificate.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index aa8a77efb..94be8ee3e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -281,12 +281,17 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: + import traceback + from StringIO import StringIO + stack = StringIO() + traceback.print_exc(file=stack) logger.error("Certificate renewing for %s failed !", domain) + logger.error(stack.getvalue()) logger.error(str(e)) if email: logger.error("Sending email with details to root ...") - _email_renewing_failed(domain, e) + _email_renewing_failed(domain, e, stack.getvalue()) ############################################################################### @@ -303,7 +308,7 @@ def _install_cron(): _set_permissions(cron_job_file, "root", "root", 0755) -def _email_renewing_failed(domain, exception_message): +def _email_renewing_failed(domain, exception_message, stack): from_ = "certmanager@%s (Certificate Manager)" % domain to_ = "root" subject_ = "Certificate renewing attempt for %s failed!" % domain @@ -313,6 +318,7 @@ def _email_renewing_failed(domain, exception_message): At attempt for renewing the certificate for domain %s failed with the following error : +%s %s Here's the tail of /var/log/yunohost/yunohost-cli.log, which might help to @@ -322,7 +328,7 @@ investigate : -- Certificate Manager -""" % (domain, exception_message, logs) +""" % (domain, exception_message, stack, logs) message = """ From: %s From 5495281e83eaefd1b8aadc63ad65953efd253129 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 17:34:16 +0100 Subject: [PATCH 41/82] [mod] remove the summary code concept and switch to code/verbose duet instead --- src/yunohost/certificate.py | 110 ++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 94be8ee3e..680149506 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -94,11 +94,10 @@ def certificate_status(auth, domain_list, full=False): for domain in domain_list: status = _get_status(domain) - status["summaryCode"] = _summary_code_to_string(status["summaryCode"]) if not full: del status["subject"] - del status["CAname"] + del status["CA_name"] lines.append(status) @@ -128,7 +127,7 @@ def certificate_install_selfsigned(domain_list, force=False): # Check we ain't trying to overwrite a good cert ! status = _get_status(domain) - if status and status["summaryCode"] > 0 and not force: + if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) cert_folder_domain = os.path.join(cert_folder, domain) @@ -183,7 +182,7 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa for domain in yunohost.domain.domain_list(auth)['domains']: status = _get_status(domain) - if status["CAtype"] != "Self-signed": + if status["CA_type"]["code"] != "self-signed": continue domain_list.append(domain) @@ -197,7 +196,7 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa # Is it self-signed ? status = _get_status(domain) - if not force and status["CAtype"] != "Self-signed": + if not force and status["CA_type"]["code"] != "self-signed": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) # Actual install steps @@ -240,7 +239,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Does it has a Let's Encrypt cert ? status = _get_status(domain) - if status["CAtype"] != "Let's Encrypt": + if status["CA_type"]["code"] != "lets-encrypt": continue # Does it expires soon ? @@ -265,7 +264,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? - if status["CAtype"] != "Let's Encrypt": + if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) # Actual renew steps @@ -444,9 +443,9 @@ def _fetch_and_enable_new_certificate(domain): os.symlink(new_cert_folder, live_link) # Check the status of the certificate is now good - status_summary_code = _get_status(domain)["summaryCode"] + status_summary = _get_status(domain)["summary"] - if status_summary_code < 20: + if status_summary["code"] != "great": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) logger.info("Restarting services...") @@ -498,47 +497,72 @@ def _get_status(domain): days_remaining = (valid_up_to - datetime.now()).days if cert_issuer == _name_self_CA(): - CA_type = "Self-signed" + CA_type = { + "code": "self-signed", + "verbose": "Self-signed", + } elif cert_issuer.startswith("Let's Encrypt"): - CA_type = "Let's Encrypt" + CA_type = { + "code": "lets-encrypt", + "verbose": "Let's Encrypt", + } elif cert_issuer.startswith("Fake LE"): - CA_type = "Fake Let's Encrypt" + CA_type = { + "code": "fake-lets-encrypt", + "verbose": "Fake Let's Encrypt", + } else: - CA_type = "Other / Unknown" + CA_type = { + "code": "other-unknown", + "verbose": "Other / Unknown", + } - # Unknown by default - status_summary_code = 0 - - # Critical if days_remaining <= 0: - status_summary_code = -30 + status_summary = { + "code": "critical", + "verbose": "CRITICAL", + } - # Warning, self-signed, browser will display a warning discouraging visitors to enter website - elif CA_type == "Self-signed" or CA_type == "Fake Let's Encrypt": - status_summary_code = -20 + elif CA_type["code"] in ("self-signed","fake-lets-encrypt"): + status_summary = { + "code": "warning", + "verbose": "WARNING", + } - # Attention, certificate will expire soon (should be renewed automatically if Let's Encrypt) elif days_remaining < validity_limit: - status_summary_code = -10 + status_summary = { + "code": "attention", + "verbose": "About to expire", + } - # CA not known, but still a valid certificate, so okay ! - elif CA_type == "Other / Unknown": - status_summary_code = 10 + elif CA_type["code"] == "other-unknown": + status_summary = { + "code": "good", + "verbose": "Good", + } - # Let's Encrypt, great ! - elif CA_type == "Let's Encrypt": - status_summary_code = 20 + elif CA_type["code"] == "lets-encrypt": + status_summary = { + "code": "great", + "verbose": "Great!", + } + + else: + status_summary = { + "code": "unknown", + "verbose": "Unknown?", + } return { "domain": domain, "subject": cert_subject, - "CAname": cert_issuer, - "CAtype": CA_type, + "CA_name": cert_issuer, + "CA_type": CA_type, "validity": days_remaining, - "summaryCode": status_summary_code + "summary": status_summary, } ############################################################################### @@ -629,28 +653,6 @@ def _domain_is_accessible_through_HTTP(ip, domain): return True -def _summary_code_to_string(code): - if code <= -30: - return "CRITICAL" - - if code <= -20: - return "WARNING" - - if code <= -10: - return "Attention" - - if code <= 0: - return "Unknown?" - - if code <= 10: - return "Good" - - if code <= 20: - return "Great!" - - return "Unknown?" - - def _name_self_CA(): cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) return cert.get_subject().CN From 0e7552263d6fe5d4b00f32339656fcc1da8671a2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 17:34:32 +0100 Subject: [PATCH 42/82] [mod] I only need to reload nginx, not restart it --- src/yunohost/certificate.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 680149506..91dd3fc2a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -450,9 +450,11 @@ def _fetch_and_enable_new_certificate(domain): logger.info("Restarting services...") - for service in ("nginx", "postfix", "dovecot", "metronome"): + for service in ("postfix", "dovecot", "metronome"): _run_service_command("restart", service) + _run_service_command("reload", "nginx") + def _prepare_certificate_signing_request(domain, key_file, output_folder): # Init a request From 11d785a22105c2937410d92f40ccff3e4d3c51d2 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:05:37 +0100 Subject: [PATCH 43/82] [mod] remove useless import --- src/yunohost/certificate.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 91dd3fc2a..826b70a89 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -33,7 +33,6 @@ import requests from OpenSSL import crypto from datetime import datetime -from tabulate import tabulate from acme_tiny import get_crt as sign_certificate from moulinette.core import MoulinetteError From f1188782e29ea001f435224bf728afcdef9208c0 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:08:34 +0100 Subject: [PATCH 44/82] [mod] top level constants should be upper case (pep8) --- src/yunohost/certificate.py | 74 ++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 826b70a89..0b583f805 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -46,23 +46,23 @@ from yunohost.service import _run_service_command logger = getActionLogger('yunohost.certmanager') -cert_folder = "/etc/yunohost/certs/" -tmp_folder = "/tmp/acme-challenge-private/" -webroot_folder = "/tmp/acme-challenge-public/" +CERT_FOLDER = "/etc/yunohost/certs/" +TMP_FOLDER = "/tmp/acme-challenge-private/" +WEBROOT_FOLDER = "/tmp/acme-challenge-public/" -selfCA_file = "/etc/ssl/certs/ca-yunohost_crt.pem" -account_key_file = "/etc/yunohost/letsencrypt_account.pem" +SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" +ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" -key_size = 2048 +KEY_SIZE = 2048 -validity_limit = 15 # days +VALIDITY_LIMIT = 15 # days # For tests -#certification_authority = "https://acme-staging.api.letsencrypt.org" +#CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" # For prod -certification_authority = "https://acme-v01.api.letsencrypt.org" +CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" -intermediate_certificate_url = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" +INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" ############################################################################### # Front-end stuff # @@ -129,7 +129,7 @@ def certificate_install_selfsigned(domain_list, force=False): if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - cert_folder_domain = os.path.join(cert_folder, domain) + cert_folder_domain = os.path.join(CERT_FOLDER, domain) if not os.path.exists(cert_folder_domain): os.makedirs(cert_folder_domain) @@ -172,7 +172,7 @@ def certificate_install_selfsigned(domain_list, force=False): def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): - if not os.path.exists(account_key_file): + if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() # If no domains given, consider all yunohost domains with self-signed @@ -242,7 +242,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal continue # Does it expires soon ? - if force or status["validity"] <= validity_limit: + if force or status["validity"] <= VALIDITY_LIMIT: domain_list.append(domain) if len(domain_list) == 0: @@ -259,7 +259,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal status = _get_status(domain) # Does it expires soon ? - if not force or status["validity"] <= validity_limit: + if not force or status["validity"] <= VALIDITY_LIMIT: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it has a Let's Encrypt cert ? @@ -350,7 +350,7 @@ location '/.well-known/acme-challenge' default_type "text/plain"; alias %s; } - ''' % webroot_folder + ''' % WEBROOT_FOLDER # Write the conf if os.path.exists(nginx_conf_file): @@ -374,36 +374,36 @@ def _fetch_and_enable_new_certificate(domain): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") - if not os.path.exists(webroot_folder): - os.makedirs(webroot_folder) + if not os.path.exists(WEBROOT_FOLDER): + os.makedirs(WEBROOT_FOLDER) - if not os.path.exists(tmp_folder): - os.makedirs(tmp_folder) + if not os.path.exists(TMP_FOLDER): + os.makedirs(TMP_FOLDER) - _set_permissions(webroot_folder, "root", "www-data", 0650) - _set_permissions(tmp_folder, "root", "root", 0640) + _set_permissions(WEBROOT_FOLDER, "root", "www-data", 0650) + _set_permissions(TMP_FOLDER, "root", "root", 0640) # Prepare certificate signing request logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) - domain_key_file = "%s/%s.pem" % (tmp_folder, domain) + domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) _set_permissions(domain_key_file, "root", "metronome", 0640) - _prepare_certificate_signing_request(domain, domain_key_file, tmp_folder) + _prepare_certificate_signing_request(domain, domain_key_file, TMP_FOLDER) # Sign the certificate logger.info("Now using ACME Tiny to sign the certificate...") - domain_csr_file = "%s/%s.csr" % (tmp_folder, domain) + domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - signed_certificate = sign_certificate(account_key_file, + signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, domain_csr_file, - webroot_folder, + WEBROOT_FOLDER, log=logger, - CA=certification_authority) + CA=CERTIFICATION_AUTHORITY) - intermediate_certificate = requests.get(intermediate_certificate_url).text + intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text # Now save the key and signed certificate logger.info("Saving the key and signed certificate...") @@ -411,7 +411,7 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s.%s" % (cert_folder, domain, date_tag) + new_cert_folder = "%s/%s.%s" % (CERT_FOLDER, domain, date_tag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -431,7 +431,7 @@ def _fetch_and_enable_new_certificate(domain): logger.info("Enabling the new certificate...") # Replace (if necessary) the link or folder for live cert - live_link = os.path.join(cert_folder, domain) + live_link = os.path.join(CERT_FOLDER, domain) if not os.path.islink(live_link): shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) @@ -480,7 +480,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): - cert_file = os.path.join(cert_folder, domain, "crt.pem") + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): return {} @@ -533,7 +533,7 @@ def _get_status(domain): "verbose": "WARNING", } - elif days_remaining < validity_limit: + elif days_remaining < VALIDITY_LIMIT: status_summary = { "code": "attention", "verbose": "About to expire", @@ -573,13 +573,13 @@ def _get_status(domain): def _generate_account_key(): logger.info("Generating account key ...") - _generate_key(account_key_file) - _set_permissions(account_key_file, "root", "root", 0400) + _generate_key(ACCOUNT_KEY_FILE) + _set_permissions(ACCOUNT_KEY_FILE, "root", "root", 0400) def _generate_key(destination_path): k = crypto.PKey() - k.generate_key(crypto.TYPE_RSA, key_size) + k.generate_key(crypto.TYPE_RSA, KEY_SIZE) with open(destination_path, "w") as f: f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, k)) @@ -596,7 +596,7 @@ def _set_permissions(path, user, group, permissions): def _backup_current_cert(domain): logger.info("Backuping existing certificate for domain " + domain) - cert_folder_domain = os.path.join(cert_folder, domain) + cert_folder_domain = os.path.join(CERT_FOLDER, domain) date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") backup_folder = "%s-backup-%s" % (cert_folder_domain, date_tag) @@ -655,7 +655,7 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): - cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(selfCA_file).read()) + cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) return cert.get_subject().CN From 718011c0ee01137d51fea265ce0b0eb1c435d7af Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:48:52 +0100 Subject: [PATCH 45/82] [mod] use logger string concatenation api --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0b583f805..3e80c48f5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -473,7 +473,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): # Save the request in tmp folder csr_file = output_folder + domain + ".csr" - logger.info("Saving to " + csr_file + " .") + logger.info("Saving to %s.", csr_file) with open(csr_file, "w") as f: f.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)) From bbbac248b6c0f1ce231e89b6454b439609e3d012 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Sun, 30 Oct 2016 18:50:36 +0100 Subject: [PATCH 46/82] [mod] use logger string concatenation api --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3e80c48f5..5d643062f 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -594,7 +594,7 @@ def _set_permissions(path, user, group, permissions): def _backup_current_cert(domain): - logger.info("Backuping existing certificate for domain " + domain) + logger.info("Backuping existing certificate for domain %s", domain) cert_folder_domain = os.path.join(CERT_FOLDER, domain) From 59500c3acc8d6c357ab84ffe684a9a101b9a7e89 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 2 Nov 2016 08:37:47 -0400 Subject: [PATCH 47/82] Adding acme-tiny as a dependency in vendor folder --- src/yunohost/certificate.py | 2 +- src/yunohost/vendor/__init__.py | 0 src/yunohost/vendor/acme_tiny/__init__.py | 0 src/yunohost/vendor/acme_tiny/acme_tiny.py | 198 +++++++++++++++++++++ 4 files changed, 199 insertions(+), 1 deletion(-) create mode 100644 src/yunohost/vendor/__init__.py create mode 100644 src/yunohost/vendor/acme_tiny/__init__.py create mode 100644 src/yunohost/vendor/acme_tiny/acme_tiny.py diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 5d643062f..636a5e72c 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -33,7 +33,7 @@ import requests from OpenSSL import crypto from datetime import datetime -from acme_tiny import get_crt as sign_certificate +from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate from moulinette.core import MoulinetteError from moulinette.utils.log import getActionLogger diff --git a/src/yunohost/vendor/__init__.py b/src/yunohost/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/__init__.py b/src/yunohost/vendor/acme_tiny/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/yunohost/vendor/acme_tiny/acme_tiny.py b/src/yunohost/vendor/acme_tiny/acme_tiny.py new file mode 100644 index 000000000..0f4cb431f --- /dev/null +++ b/src/yunohost/vendor/acme_tiny/acme_tiny.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging +try: + from urllib.request import urlopen # Python 3 +except ImportError: + from urllib2 import urlopen # Python 2 + +#DEFAULT_CA = "https://acme-staging.api.letsencrypt.org" +DEFAULT_CA = "https://acme-v01.api.letsencrypt.org" + +LOGGER = logging.getLogger(__name__) +LOGGER.addHandler(logging.StreamHandler()) +LOGGER.setLevel(logging.INFO) + +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA): + # helper function base64 encode for jose spec + def _b64(b): + return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") + + # parse account key to get public key + log.info("Parsing account key...") + proc = subprocess.Popen(["openssl", "rsa", "-in", account_key, "-noout", "-text"], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate() + if proc.returncode != 0: + raise IOError("OpenSSL Error: {0}".format(err)) + pub_hex, pub_exp = re.search( + r"modulus:\n\s+00:([a-f0-9\:\s]+?)\npublicExponent: ([0-9]+)", + out.decode('utf8'), re.MULTILINE|re.DOTALL).groups() + pub_exp = "{0:x}".format(int(pub_exp)) + pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp + header = { + "alg": "RS256", + "jwk": { + "e": _b64(binascii.unhexlify(pub_exp.encode("utf-8"))), + "kty": "RSA", + "n": _b64(binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex).encode("utf-8"))), + }, + } + accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':')) + thumbprint = _b64(hashlib.sha256(accountkey_json.encode('utf8')).digest()) + + # helper function make signed requests + def _send_signed_request(url, payload): + payload64 = _b64(json.dumps(payload).encode('utf8')) + protected = copy.deepcopy(header) + protected["nonce"] = urlopen(CA + "/directory").headers['Replay-Nonce'] + protected64 = _b64(json.dumps(protected).encode('utf8')) + proc = subprocess.Popen(["openssl", "dgst", "-sha256", "-sign", account_key], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate("{0}.{1}".format(protected64, payload64).encode('utf8')) + if proc.returncode != 0: + raise IOError("OpenSSL Error: {0}".format(err)) + data = json.dumps({ + "header": header, "protected": protected64, + "payload": payload64, "signature": _b64(out), + }) + try: + resp = urlopen(url, data.encode('utf8')) + return resp.getcode(), resp.read() + except IOError as e: + return getattr(e, "code", None), getattr(e, "read", e.__str__)() + + # find domains + log.info("Parsing CSR...") + proc = subprocess.Popen(["openssl", "req", "-in", csr, "-noout", "-text"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + out, err = proc.communicate() + if proc.returncode != 0: + raise IOError("Error loading {0}: {1}".format(csr, err)) + domains = set([]) + common_name = re.search(r"Subject:.*? CN=([^\s,;/]+)", out.decode('utf8')) + if common_name is not None: + domains.add(common_name.group(1)) + subject_alt_names = re.search(r"X509v3 Subject Alternative Name: \n +([^\n]+)\n", out.decode('utf8'), re.MULTILINE|re.DOTALL) + if subject_alt_names is not None: + for san in subject_alt_names.group(1).split(", "): + if san.startswith("DNS:"): + domains.add(san[4:]) + + # get the certificate domains and expiration + log.info("Registering account...") + code, result = _send_signed_request(CA + "/acme/new-reg", { + "resource": "new-reg", + "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", + }) + if code == 201: + log.info("Registered!") + elif code == 409: + log.info("Already registered!") + else: + raise ValueError("Error registering: {0} {1}".format(code, result)) + + # verify each domain + for domain in domains: + log.info("Verifying {0}...".format(domain)) + + # get new challenge + code, result = _send_signed_request(CA + "/acme/new-authz", { + "resource": "new-authz", + "identifier": {"type": "dns", "value": domain}, + }) + if code != 201: + raise ValueError("Error requesting challenges: {0} {1}".format(code, result)) + + # make the challenge file + challenge = [c for c in json.loads(result.decode('utf8'))['challenges'] if c['type'] == "http-01"][0] + token = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token']) + keyauthorization = "{0}.{1}".format(token, thumbprint) + wellknown_path = os.path.join(acme_dir, token) + with open(wellknown_path, "w") as wellknown_file: + wellknown_file.write(keyauthorization) + + # check that the file is in place + wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(domain, token) + try: + resp = urlopen(wellknown_url) + resp_data = resp.read().decode('utf8').strip() + assert resp_data == keyauthorization + except (IOError, AssertionError): + os.remove(wellknown_path) + raise ValueError("Wrote file to {0}, but couldn't download {1}".format( + wellknown_path, wellknown_url)) + + # notify challenge are met + code, result = _send_signed_request(challenge['uri'], { + "resource": "challenge", + "keyAuthorization": keyauthorization, + }) + if code != 202: + raise ValueError("Error triggering challenge: {0} {1}".format(code, result)) + + # wait for challenge to be verified + while True: + try: + resp = urlopen(challenge['uri']) + challenge_status = json.loads(resp.read().decode('utf8')) + except IOError as e: + raise ValueError("Error checking challenge: {0} {1}".format( + e.code, json.loads(e.read().decode('utf8')))) + if challenge_status['status'] == "pending": + time.sleep(2) + elif challenge_status['status'] == "valid": + log.info("{0} verified!".format(domain)) + os.remove(wellknown_path) + break + else: + raise ValueError("{0} challenge did not pass: {1}".format( + domain, challenge_status)) + + # get the new certificate + log.info("Signing certificate...") + proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + csr_der, err = proc.communicate() + code, result = _send_signed_request(CA + "/acme/new-cert", { + "resource": "new-cert", + "csr": _b64(csr_der), + }) + if code != 201: + raise ValueError("Error signing certificate: {0} {1}".format(code, result)) + + # return signed certificate! + log.info("Certificate signed!") + return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( + "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64))) + +def main(argv): + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description=textwrap.dedent("""\ + This script automates the process of getting a signed TLS certificate from + Let's Encrypt using the ACME protocol. It will need to be run on your server + and have access to your private account key, so PLEASE READ THROUGH IT! It's + only ~200 lines, so it won't take long. + + ===Example Usage=== + python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > signed.crt + =================== + + ===Example Crontab Renewal (once per month)=== + 0 0 1 * * python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /usr/share/nginx/html/.well-known/acme-challenge/ > /path/to/signed.crt 2>> /var/log/acme_tiny.log + ============================================== + """) + ) + parser.add_argument("--account-key", required=True, help="path to your Let's Encrypt account private key") + parser.add_argument("--csr", required=True, help="path to your certificate signing request") + parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory") + parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors") + parser.add_argument("--ca", default=DEFAULT_CA, help="certificate authority, default is Let's Encrypt") + + args = parser.parse_args(argv) + LOGGER.setLevel(args.quiet or LOGGER.level) + signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca) + sys.stdout.write(signed_crt) + +if __name__ == "__main__": # pragma: no cover + main(sys.argv[1:]) From f3461b36364a3613e5d50648ce52c355f5d466fa Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Nov 2016 12:24:59 -0500 Subject: [PATCH 48/82] Fixing typo in arguments to match actionmap --- src/yunohost/domain.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 1a296ee97..aadd9086d 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -257,16 +257,16 @@ def domain_dns_conf(domain, ttl=None): return result -def domain_cert_status(auth, domainList, full=False): - return yunohost.certificate.certificate_status(auth, domainList, full) +def domain_cert_status(auth, domain_list, full=False): + return yunohost.certificate.certificate_status(auth, domain_list, full) -def domain_cert_install(auth, domainList, force=False, no_checks=False, self_signed=False): - return yunohost.certificate.certificate_install(auth, domainList, force, no_checks, self_signed) +def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False): + return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed) -def domain_cert_renew(auth, domainList, force=False, no_checks=False, email=False): - return yunohost.certificate.certificate_renew(auth, domainList, force, no_checks, email) +def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False): + return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email) def get_public_ip(protocol=4): From 56cd9610f353c030e6a3c62fcd34879ce907c9e9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Nov 2016 14:02:59 -0500 Subject: [PATCH 49/82] Improving CLI display of cert-status --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 636a5e72c..78c0c2ed5 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -100,7 +100,7 @@ def certificate_status(auth, domain_list, full=False): lines.append(status) - return lines + return { "certificates" : lines } def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): From 90e63edcfe5af42b6655a582157962f02c7592bf Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 7 Nov 2016 14:19:28 -0500 Subject: [PATCH 50/82] Improving/cleaning CLI display of cert-status --- src/yunohost/certificate.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 78c0c2ed5..d2a67495a 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -89,7 +89,7 @@ def certificate_status(auth, domain_list, full=False): if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) - lines = [] + certificates = {} for domain in domain_list: status = _get_status(domain) @@ -97,10 +97,13 @@ def certificate_status(auth, domain_list, full=False): if not full: del status["subject"] del status["CA_name"] + status["CA_type"] = status["CA_type"]["verbose"] + status["summary"] = status["summary"]["verbose"] - lines.append(status) + del status["domain"] + certificates[domain] = status - return { "certificates" : lines } + return { "certificates" : certificates } def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): From 4ddc3aac369cb20ba672553748e04eb218c33b34 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 12:09:02 -0500 Subject: [PATCH 51/82] Display a warning message when letsencrypt is installed, suggesting commands to migrate --- locales/en.json | 3 ++- src/yunohost/certificate.py | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/locales/en.json b/locales/en.json index bdfd1b14a..1a93b72ff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -251,5 +251,6 @@ "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !" + "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", + "certmanager_old_letsencrypt_app_detected" : "Command aborted because the letsencrypt app is conflicting with the yunohost certificate management features." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d2a67495a..f9f5784a0 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -40,7 +40,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain -from yunohost.app import app_ssowatconf +from yunohost.app import app_ssowatconf, app_list from yunohost.service import _run_service_command @@ -78,6 +78,9 @@ def certificate_status(auth, domain_list, full=False): full -- Display more info about the certificates """ + # Check if old letsencrypt_ynh is installed + _check_old_letsencrypt_app() + # If no domains given, consider all yunohost domains if domain_list == []: domain_list = yunohost.domain.domain_list(auth)['domains'] @@ -107,6 +110,7 @@ def certificate_status(auth, domain_list, full=False): def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): + """ Install a Let's Encrypt certificate for given domains (all by default) @@ -117,6 +121,11 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si before attempting the install self-signed -- Instal self-signed certificates instead of Let's Encrypt """ + + # Check if old letsencrypt_ynh is installed + _check_old_letsencrypt_app() + + if self_signed: certificate_install_selfsigned(domain_list, force) else: @@ -234,6 +243,9 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal email -- Emails root if some renewing failed """ + # Check if old letsencrypt_ynh is installed + _check_old_letsencrypt_app() + # If no domains given, consider all yunohost domains with Let's Encrypt # certificates if domain_list == []: @@ -299,6 +311,29 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Back-end stuff # ############################################################################### +def _check_old_letsencrypt_app(): + + installedAppIds = [ app["id"] for app in yunohost.app.app_list(installed=True)["apps"] ] + if ("letsencrypt" not in installedAppIds) : + return + + logger.warning(" ") + logger.warning("Yunohost detected that the 'letsencrypt' app is installed, ") + logger.warning("which conflits with the new certificate management features") + logger.warning("directly integrated in Yunohost. If you wish to use these ") + logger.warning("new features, please run the following commands to migrate ") + logger.warning("your installation :") + logger.warning(" ") + logger.warning(" yunohost app remove letsencrypt") + logger.warning(" yunohost domain cert-install") + logger.warning(" ") + logger.warning("N.B. : this will attempt to re-install certificates for ") + logger.warning("all domains with a Let's Encrypt certificate or self-signed") + logger.warning("certificate.") + logger.warning(" ") + + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) + def _install_cron(): cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" From cbc71f2530853f146cdebbd33498574869be1476 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 12:55:01 -0500 Subject: [PATCH 52/82] Adding a check for the presence of the ssowat header when checking domain is accessible through http --- src/yunohost/certificate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index f9f5784a0..19758c33d 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -685,7 +685,10 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - requests.head("http://" + ip, headers={"Host": domain}) + r = requests.head("http://" + ip, headers={"Host": domain}) + # Check we got the ssowat header in the response + if ("x-sso-wat" not in r.headers.keys()) : + return False except Exception: return False From 6bfe1c80833423b0530f368fddcc05ccdf304629 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 20:59:55 -0500 Subject: [PATCH 53/82] Check that the DNS A record matches the global IP now using dnspython and FDN's DNS --- locales/en.json | 6 ++---- src/yunohost/certificate.py | 31 +++++++++---------------------- 2 files changed, 11 insertions(+), 26 deletions(-) diff --git a/locales/en.json b/locales/en.json index 1a93b72ff..627c11b52 100644 --- a/locales/en.json +++ b/locales/en.json @@ -245,10 +245,8 @@ "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", - "certmanager_error_contacting_dns_api" : "Error contacting the DNS API ({api:s}), reason: {reason:s}. Use --no-checks to disable checks.", - "certmanager_error_parsing_dns" : "Error parsing the return value from the DNS API : {value:s}. Please verify your DNS configuration for domain {domain:s}. Use --no-checks to disable checks.", - "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. Give some time for the DNS to refresh, or use --no-checks to disable checks.", - "certmanager_no_A_dns_record" : "No DNS record of type A found for {domain:s}. You need to configure the DNS for your domain before installing a certificate !", + "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", + "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 19758c33d..02ea73af8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -31,6 +31,8 @@ import grp import smtplib import requests +import dns.resolver + from OpenSSL import crypto from datetime import datetime from yunohost.vendor.acme_tiny.acme_tiny import get_crt as sign_certificate @@ -656,29 +658,14 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: - result = requests.get("https://dns-api.org/A/" + domain) - except Exception as exception: - import traceback - traceback.print_exc(file=sys.stdout) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_contacting_dns_api', api="dns-api.org", reason=exception)) + resolver = dns.resolver.Resolver() + # These are FDN's DNS + resolver.nameservers = [ "80.67.169.12", "80.67.169.40" ] + answers = resolver.query(domain, "A") + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) - try: - dns_ip = result.json() - except Exception as exception: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - - if len(dns_ip) == 0: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - - dns_ip = dns_ip[0] - - if dns_ip.get("error") == "NXDOMAIN": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_A_dns_record', domain=domain)) - - if "value" not in dns_ip: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_parsing_dns', domain=domain, value=result.text)) - - dns_ip = dns_ip["value"] + dns_ip = answers[0] return dns_ip == public_ip From e2e1fce44e3f2ec79f2fbd31bde71e10b6230900 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 21:09:06 -0500 Subject: [PATCH 54/82] Fixing previous commit --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 02ea73af8..e648e5b13 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -665,7 +665,7 @@ def _dns_ip_match_public_ip(public_ip, domain): except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) : raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) - dns_ip = answers[0] + dns_ip = str(answers[0]) return dns_ip == public_ip From a57ebfc4e65897c0405fe21c434e5f4086c5d004 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 21:11:36 -0500 Subject: [PATCH 55/82] Use 3072 bits keys --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index e648e5b13..3e999c2d1 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -55,7 +55,7 @@ WEBROOT_FOLDER = "/tmp/acme-challenge-public/" SELF_CA_FILE = "/etc/ssl/certs/ca-yunohost_crt.pem" ACCOUNT_KEY_FILE = "/etc/yunohost/letsencrypt_account.pem" -KEY_SIZE = 2048 +KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days From 109cbf764147dba0c6dd1041d776cad8c74f20a9 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 22:22:13 -0500 Subject: [PATCH 56/82] Backuping existing certificate (if any) also for self-signed generation --- locales/en.json | 1 + src/yunohost/certificate.py | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 627c11b52..2dec12706 100644 --- a/locales/en.json +++ b/locales/en.json @@ -248,6 +248,7 @@ "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_cannot_read_cert": "Something wrong happened when trying to open current certificate for domain {domain:s} (file : {file:s}), reason: {reason:s}", + "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", "certmanager_old_letsencrypt_app_detected" : "Command aborted because the letsencrypt app is conflicting with the yunohost certificate management features." diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3e999c2d1..430f6d4d2 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -143,10 +143,18 @@ def certificate_install_selfsigned(domain_list, force=False): if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + cert_folder_domain = os.path.join(CERT_FOLDER, domain) - if not os.path.exists(cert_folder_domain): - os.makedirs(cert_folder_domain) + # Backup existing certificate / folder + if os.path.exists(cert_folder_domain) : + if not os.path.islink(cert_folder_domain): + _backup_current_cert(domain) + shutil.rmtree(cert_folder_domain) + else : + os.remove(cert_folder_domain) + + os.makedirs(cert_folder_domain) # Get serial ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' @@ -184,6 +192,16 @@ def certificate_install_selfsigned(domain_list, force=False): _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) + # Check new status indicate a recently created self-signed certificate, + status = _get_status(domain) + + if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: + logger.success(m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + else : + logger.error("Installation of self-signed certificate installation for %s failed !", domain) + logger.error(str(e)) + + def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): if not os.path.exists(ACCOUNT_KEY_FILE): @@ -474,7 +492,7 @@ def _fetch_and_enable_new_certificate(domain): live_link = os.path.join(CERT_FOLDER, domain) if not os.path.islink(live_link): - shutil.rmtree(live_link) # Well, yep, hopefully that's not too dangerous (directory should have been backuped before calling this command) + shutil.rmtree(live_link) # Hopefully that's not too dangerous (directory should have been backuped before calling this command) elif os.path.lexists(live_link): os.remove(live_link) From e1539297a5d7c23024402e68ff560f57f98dd28c Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 22:25:15 -0500 Subject: [PATCH 57/82] Ignore messy stderr from openssl commands during self-signed cert generation --- src/yunohost/certificate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 430f6d4d2..56f6773bd 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -167,9 +167,9 @@ def certificate_install_selfsigned(domain_list, force=False): # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch' + 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch 2>/dev/null' % (cert_folder_domain, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch' + 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch 2>/dev/null' % (cert_folder_domain, ssl_dir, ssl_dir), ] From 80ebaa6895dd9025fa8b6d6bf19179e93b747456 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 22:34:19 -0500 Subject: [PATCH 58/82] Have a subdirectory for cert backups, to not flood /etc/yunohost/certs/ --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 56f6773bd..0788f9e14 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -657,7 +657,7 @@ def _backup_current_cert(domain): cert_folder_domain = os.path.join(CERT_FOLDER, domain) date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - backup_folder = "%s-backup-%s" % (cert_folder_domain, date_tag) + backup_folder = "%s-backups/%s" % (cert_folder_domain, date_tag) shutil.copytree(cert_folder_domain, backup_folder) From 4e9a2c050de2b89c9d10016c09cc5c4975cc1bfd Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 8 Nov 2016 23:29:10 -0500 Subject: [PATCH 59/82] Cleaning / reorganizing the way certificates are stored and enabled --- src/yunohost/certificate.py | 91 +++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0788f9e14..00ab3dbb8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -144,53 +144,48 @@ def certificate_install_selfsigned(domain_list, force=False): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - cert_folder_domain = os.path.join(CERT_FOLDER, domain) + date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") + new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) - # Backup existing certificate / folder - if os.path.exists(cert_folder_domain) : - if not os.path.islink(cert_folder_domain): - _backup_current_cert(domain) - shutil.rmtree(cert_folder_domain) - else : - os.remove(cert_folder_domain) - - os.makedirs(cert_folder_domain) + os.makedirs(new_cert_folder) # Get serial ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' with open(os.path.join(ssl_dir, 'serial'), 'r') as f: serial = f.readline().rstrip() - shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(cert_folder_domain, "openssl.cnf")) + shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(new_cert_folder, "openssl.cnf")) # FIXME : should refactor this to avoid so many os.system() calls... # We should be able to do all this using OpenSSL.crypto and os/shutil command_list = [ - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, cert_folder_domain), + 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, new_cert_folder), 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch 2>/dev/null' - % (cert_folder_domain, ssl_dir, ssl_dir), + % (new_cert_folder, ssl_dir, ssl_dir), 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch 2>/dev/null' - % (cert_folder_domain, ssl_dir, ssl_dir), + % (new_cert_folder, ssl_dir, ssl_dir), ] for command in command_list: if os.system(command) != 0: raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) - os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(cert_folder_domain, "ca.pem")) - shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(cert_folder_domain, "key.pem")) - shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(cert_folder_domain, "crt.pem")) + os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(new_cert_folder, "ca.pem")) + shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(new_cert_folder, "key.pem")) + shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(new_cert_folder, "crt.pem")) # append ca.pem at the end of crt.pem - with open(os.path.join(cert_folder_domain, "ca.pem"), "r") as ca_pem: - with open(os.path.join(cert_folder_domain, "crt.pem"), "a") as crt_pem: + with open(os.path.join(new_cert_folder, "ca.pem"), "r") as ca_pem: + with open(os.path.join(new_cert_folder, "crt.pem"), "a") as crt_pem: crt_pem.write("\n") crt_pem.write(ca_pem.read()) - _set_permissions(cert_folder_domain, "root", "root", 0755) - _set_permissions(os.path.join(cert_folder_domain, "key.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(cert_folder_domain, "crt.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(cert_folder_domain, "openssl.cnf"), "root", "root", 0600) + _set_permissions(new_cert_folder, "root", "root", 0755) + _set_permissions(os.path.join(new_cert_folder, "key.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(new_cert_folder, "crt.pem"), "root", "metronome", 0640) + _set_permissions(os.path.join(new_cert_folder, "openssl.cnf"), "root", "root", 0600) + + _enable_certificate(domain, new_cert_folder) # Check new status indicate a recently created self-signed certificate, status = _get_status(domain) @@ -239,7 +234,6 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa if not no_checks: _check_domain_is_correctly_configured(domain) - _backup_current_cert(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) _install_cron() @@ -308,7 +302,6 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: _check_domain_is_correctly_configured(domain) - _backup_current_cert(domain) _fetch_and_enable_new_certificate(domain) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -469,7 +462,7 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s.%s" % (CERT_FOLDER, domain, date_tag) + new_cert_folder = "%s/%s-history/%s-letsencrypt" % (CERT_FOLDER, domain, date_tag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -486,18 +479,7 @@ def _fetch_and_enable_new_certificate(domain): _set_permissions(domain_cert_file, "root", "metronome", 0640) - logger.info("Enabling the new certificate...") - - # Replace (if necessary) the link or folder for live cert - live_link = os.path.join(CERT_FOLDER, domain) - - if not os.path.islink(live_link): - shutil.rmtree(live_link) # Hopefully that's not too dangerous (directory should have been backuped before calling this command) - - elif os.path.lexists(live_link): - os.remove(live_link) - - os.symlink(new_cert_folder, live_link) + _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good status_summary = _get_status(domain)["summary"] @@ -505,13 +487,6 @@ def _fetch_and_enable_new_certificate(domain): if status_summary["code"] != "great": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) - logger.info("Restarting services...") - - for service in ("postfix", "dovecot", "metronome"): - _run_service_command("restart", service) - - _run_service_command("reload", "nginx") - def _prepare_certificate_signing_request(domain, key_file, output_folder): # Init a request @@ -651,6 +626,32 @@ def _set_permissions(path, user, group, permissions): os.chmod(path, permissions) +def _enable_certificate(domain, new_cert_folder) : + logger.info("Enabling the certificate for domain %s ...", domain) + + live_link = os.path.join(CERT_FOLDER, domain) + + # If a live link (or folder) already exists + if os.path.exists(live_link) : + # If it's not a link ... expect if to be a folder + if not os.path.islink(live_link): + # Backup it and remove it + _backup_current_cert(domain) + shutil.rmtree(live_link) + # Else if it's a link, simply delete it + elif os.path.lexists(live_link): + os.remove(live_link) + + os.symlink(new_cert_folder, live_link) + + logger.info("Restarting services...") + + for service in ("postfix", "dovecot", "metronome"): + _run_service_command("restart", service) + + _run_service_command("reload", "nginx") + + def _backup_current_cert(domain): logger.info("Backuping existing certificate for domain %s", domain) From 937cccf8131aa48158722dcb58dd0182174b342c Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 9 Nov 2016 08:38:52 +0100 Subject: [PATCH 60/82] [mod] remove useless import --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 00ab3dbb8..02ccbd380 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -42,7 +42,7 @@ from moulinette.utils.log import getActionLogger import yunohost.domain -from yunohost.app import app_ssowatconf, app_list +from yunohost.app import app_ssowatconf from yunohost.service import _run_service_command From 9e5b2743db03e94c8c6cb45d6693823425e0127e Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Wed, 9 Nov 2016 08:38:58 +0100 Subject: [PATCH 61/82] [mod] pep8 --- src/yunohost/certificate.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 02ccbd380..81a459482 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -108,11 +108,11 @@ def certificate_status(auth, domain_list, full=False): del status["domain"] certificates[domain] = status - return { "certificates" : certificates } + return {"certificates" : certificates} def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): - + """ Install a Let's Encrypt certificate for given domains (all by default) @@ -126,7 +126,7 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si # Check if old letsencrypt_ynh is installed _check_old_letsencrypt_app() - + if self_signed: certificate_install_selfsigned(domain_list, force) @@ -325,9 +325,9 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal ############################################################################### def _check_old_letsencrypt_app(): + installedAppIds = [app["id"] for app in yunohost.app.app_list(installed=True)["apps"]] - installedAppIds = [ app["id"] for app in yunohost.app.app_list(installed=True)["apps"] ] - if ("letsencrypt" not in installedAppIds) : + if "letsencrypt" not in installedAppIds: return logger.warning(" ") @@ -344,9 +344,10 @@ def _check_old_letsencrypt_app(): logger.warning("all domains with a Let's Encrypt certificate or self-signed") logger.warning("certificate.") logger.warning(" ") - + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) + def _install_cron(): cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" @@ -679,9 +680,9 @@ def _dns_ip_match_public_ip(public_ip, domain): try: resolver = dns.resolver.Resolver() # These are FDN's DNS - resolver.nameservers = [ "80.67.169.12", "80.67.169.40" ] + resolver.nameservers = ["80.67.169.12", "80.67.169.40"] answers = resolver.query(domain, "A") - except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN) : + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) dns_ip = str(answers[0]) @@ -693,7 +694,7 @@ def _domain_is_accessible_through_HTTP(ip, domain): try: r = requests.head("http://" + ip, headers={"Host": domain}) # Check we got the ssowat header in the response - if ("x-sso-wat" not in r.headers.keys()) : + if "x-sso-wat" not in r.headers.keys(): return False except Exception: return False From a85c79ef320190d7f80136002798eb9adbfa8192 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Nov 2016 19:41:23 -0500 Subject: [PATCH 62/82] Refactored the self-signed cert generation, some steps were overly complicated for no reason --- src/yunohost/certificate.py | 75 +++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 81a459482..d03d7d55b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -30,6 +30,7 @@ import pwd import grp import smtplib import requests +import subprocess import dns.resolver @@ -143,58 +144,74 @@ def certificate_install_selfsigned(domain_list, force=False): if status and status["summary"]["code"] in ('good', 'great') and not force: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - + # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) - os.makedirs(new_cert_folder) - - # Get serial + original_ca_file = '/etc/ssl/certs/ca-yunohost_crt.pem' ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' - with open(os.path.join(ssl_dir, 'serial'), 'r') as f: - serial = f.readline().rstrip() + conf_template = os.path.join(ssl_dir, "openssl.cnf") - shutil.copyfile(os.path.join(ssl_dir, "openssl.cnf"), os.path.join(new_cert_folder, "openssl.cnf")) + csr_file = os.path.join(ssl_dir, "certs", "yunohost_csr.pem") + conf_file = os.path.join(new_cert_folder, "openssl.cnf") + key_file = os.path.join(new_cert_folder, "key.pem") + crt_file = os.path.join(new_cert_folder, "crt.pem") + ca_file = os.path.join(new_cert_folder, "ca.pem") + + # Create output folder for new certificate stuff + os.makedirs(new_cert_folder) + + # Create our conf file, based on template, replacing the occurences of + # "yunohost.org" with the given domain + with open(conf_file, "w") as f : + with open(conf_template, "r") as template : + for line in template : + f.write(line.replace("yunohost.org", domain)) - # FIXME : should refactor this to avoid so many os.system() calls... - # We should be able to do all this using OpenSSL.crypto and os/shutil - command_list = [ - 'sed -i "s/yunohost.org/%s/g" %s/openssl.cnf' % (domain, new_cert_folder), - 'openssl req -new -config %s/openssl.cnf -days 3650 -out %s/certs/yunohost_csr.pem -keyout %s/certs/yunohost_key.pem -nodes -batch 2>/dev/null' - % (new_cert_folder, ssl_dir, ssl_dir), - 'openssl ca -config %s/openssl.cnf -days 3650 -in %s/certs/yunohost_csr.pem -out %s/certs/yunohost_crt.pem -batch 2>/dev/null' - % (new_cert_folder, ssl_dir, ssl_dir), - ] + # Use OpenSSL command line to create a certificate signing request, + # and self-sign the cert + commands = [] + commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + % (conf_file, csr_file, key_file)) + commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" + % (conf_file, csr_file, crt_file)) - for command in command_list: - if os.system(command) != 0: + for command in commands : + p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + if p.returncode != 0: + logger.warning(out) raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + else : + logger.info(out) - os.symlink('/etc/ssl/certs/ca-yunohost_crt.pem', os.path.join(new_cert_folder, "ca.pem")) - shutil.copyfile(os.path.join(ssl_dir, "certs", "yunohost_key.pem"), os.path.join(new_cert_folder, "key.pem")) - shutil.copyfile(os.path.join(ssl_dir, "newcerts", "%s.pem" % serial), os.path.join(new_cert_folder, "crt.pem")) + # Link the CA cert (not sure it's actually needed in practice though, + # since we append it at the end of crt.pem. For instance for Let's + # Encrypt certs, we only need the crt.pem and key.pem) + os.symlink(original_ca_file, ca_file) - # append ca.pem at the end of crt.pem - with open(os.path.join(new_cert_folder, "ca.pem"), "r") as ca_pem: - with open(os.path.join(new_cert_folder, "crt.pem"), "a") as crt_pem: + # Append ca.pem at the end of crt.pem + with open(ca_file, "r") as ca_pem: + with open(crt_file, "a") as crt_pem: crt_pem.write("\n") crt_pem.write(ca_pem.read()) + # Set appropriate permissions _set_permissions(new_cert_folder, "root", "root", 0755) - _set_permissions(os.path.join(new_cert_folder, "key.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(new_cert_folder, "crt.pem"), "root", "metronome", 0640) - _set_permissions(os.path.join(new_cert_folder, "openssl.cnf"), "root", "root", 0600) + _set_permissions(key_file, "root", "metronome", 0640) + _set_permissions(crt_file, "root", "metronome", 0640) + _set_permissions(conf_file, "root", "root", 0600) + # Actually enable the certificate we created _enable_certificate(domain, new_cert_folder) - # Check new status indicate a recently created self-signed certificate, + # Check new status indicate a recently created self-signed certificate status = _get_status(domain) if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: logger.success(m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) else : logger.error("Installation of self-signed certificate installation for %s failed !", domain) - logger.error(str(e)) From 11c626881a7ac0dcdf27d587899ba0662221d1a2 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 9 Nov 2016 20:19:56 -0500 Subject: [PATCH 63/82] Adding other DNS resolvers from FFDN --- src/yunohost/certificate.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d03d7d55b..8feb9a771 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -696,8 +696,16 @@ def _check_domain_is_correctly_configured(domain): def _dns_ip_match_public_ip(public_ip, domain): try: resolver = dns.resolver.Resolver() - # These are FDN's DNS - resolver.nameservers = ["80.67.169.12", "80.67.169.40"] + resolver.nameservers = [] + # FFDN DNS resolvers + # See https://www.ffdn.org/wiki/doku.php?id=formations:dns + resolver.nameservers.append("80.67.169.12") # FDN + resolver.nameservers.append("80.67.169.40") # + resolver.nameservers.append("89.234.141.66") # ARN + resolver.nameservers.append("141.255.128.100") # Aquilenet + resolver.nameservers.append("141.255.128.101") # + resolver.nameservers.append("89.234.186.18") # Grifon + resolver.nameservers.append("80.67.188.188") # LDN answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) From bba92e4d4104a0ca5e81ad0c0582e5eac98bd6df Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Sun, 20 Nov 2016 20:36:58 -0500 Subject: [PATCH 64/82] Small tweaks for the web interface --- data/actionsmap/yunohost.yml | 8 ++++---- src/yunohost/certificate.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 8f08a98b8..3729e3aa3 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -308,7 +308,7 @@ domain: ### certificate_status() cert-status: action_help: List status of current certificates (all by default). - api: GET /certs/status/ + api: GET /domains/cert-status/ configuration: authenticate: all authenticator: ldap-anonymous @@ -323,7 +323,7 @@ domain: ### certificate_install() cert-install: action_help: Install Let's Encrypt certificates for given domains (all by default). - api: POST /certs/enable/ + api: POST /domains/cert-install/ configuration: authenticate: all authenticator: ldap-anonymous @@ -344,7 +344,7 @@ domain: ### certificate_renew() cert-renew: action_help: Renew the Let's Encrypt certificates for given domains (all by default). - api: POST /certs/renew/ + api: POST /domains/cert-renew/ configuration: authenticate: all authenticator: ldap-anonymous @@ -361,7 +361,7 @@ domain: --no-checks: help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true - + ### domain_info() # info: # action_help: Get domain informations diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8feb9a771..8b3db0283 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -103,6 +103,7 @@ def certificate_status(auth, domain_list, full=False): if not full: del status["subject"] del status["CA_name"] + del status["ACME_eligible"] status["CA_type"] = status["CA_type"]["verbose"] status["summary"] = status["summary"]["verbose"] @@ -157,10 +158,10 @@ def certificate_install_selfsigned(domain_list, force=False): key_file = os.path.join(new_cert_folder, "key.pem") crt_file = os.path.join(new_cert_folder, "crt.pem") ca_file = os.path.join(new_cert_folder, "ca.pem") - + # Create output folder for new certificate stuff os.makedirs(new_cert_folder) - + # Create our conf file, based on template, replacing the occurences of # "yunohost.org" with the given domain with open(conf_file, "w") as f : @@ -168,10 +169,10 @@ def certificate_install_selfsigned(domain_list, force=False): for line in template : f.write(line.replace("yunohost.org", domain)) - # Use OpenSSL command line to create a certificate signing request, + # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert commands = [] - commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" % (conf_file, csr_file, key_file)) commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" % (conf_file, csr_file, crt_file)) @@ -249,7 +250,7 @@ def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=Fa try: if not no_checks: - _check_domain_is_correctly_configured(domain) + _check_domain_is_ready_for_ACME(domain) _configure_for_acme_challenge(auth, domain) _fetch_and_enable_new_certificate(domain) @@ -318,7 +319,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: - _check_domain_is_correctly_configured(domain) + _check_domain_is_ready_for_ACME(domain) _fetch_and_enable_new_certificate(domain) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -608,6 +609,12 @@ def _get_status(domain): "verbose": "Unknown?", } + try : + _check_domain_is_ready_for_ACME(domain) + ACME_eligible = True + except : + ACME_eligible = False + return { "domain": domain, "subject": cert_subject, @@ -615,6 +622,7 @@ def _get_status(domain): "CA_type": CA_type, "validity": days_remaining, "summary": status_summary, + "ACME_eligible": ACME_eligible } ############################################################################### @@ -681,7 +689,7 @@ def _backup_current_cert(domain): shutil.copytree(cert_folder_domain, backup_folder) -def _check_domain_is_correctly_configured(domain): +def _check_domain_is_ready_for_ACME(domain): public_ip = yunohost.domain.get_public_ip() # Check if IP from DNS matches public IP From 5de006f18df96398658809fa6ad5b565f8f83bef Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Nov 2016 10:58:57 -0500 Subject: [PATCH 65/82] Follow up of @julienmalik comments - misc typo/cosmetic fixes --- data/actionsmap/yunohost.yml | 6 +++--- locales/de.json | 2 +- locales/en.json | 8 ++++---- locales/es.json | 2 +- locales/fr.json | 2 +- locales/nl.json | 2 +- locales/pt.json | 2 +- src/yunohost/certificate.py | 26 ++++++++++++++++---------- src/yunohost/domain.py | 2 +- 9 files changed, 29 insertions(+), 23 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 3729e3aa3..18f470a61 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -335,7 +335,7 @@ domain: help: Install even if current certificate is not self-signed action: store_true --no-checks: - help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to install. (Not recommended) + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to install. (Not recommended) action: store_true --self-signed: help: Install self-signed certificate instead of Let's Encrypt @@ -353,13 +353,13 @@ domain: help: Domains for which to renew the certificates nargs: "*" --force: - help: Ignore the validity treshold (30 days) + help: Ignore the validity threshold (30 days) action: store_true --email: help: Send an email to root with logs if some renewing fails action: store_true --no-checks: - help: Does not perform any check that your domain seems correcly configured (DNS, reachability) before attempting to renew. (Not recommended) + help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true ### domain_info() diff --git a/locales/de.json b/locales/de.json index e57315caa..1331c56b4 100644 --- a/locales/de.json +++ b/locales/de.json @@ -57,7 +57,7 @@ "custom_app_url_required": "Es muss eine URL angegeben um deine benutzerdefinierte App {app:s} zu aktualisieren", "custom_appslist_name_required": "Du musst einen Namen für deine benutzerdefinierte Appliste angeben", "dnsmasq_isnt_installed": "dnsmasq scheint nicht installiert zu sein. Bitte führe 'apt-get remove bind9 && apt-get install dnsmasq' aus", - "certmanager_domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", + "domain_cert_gen_failed": "Zertifikat konnte nicht erzeugt werden", "domain_created": "Domain erfolgreich erzeugt", "domain_creation_failed": "Konnte Domain nicht erzeugen", "domain_deleted": "Domain erfolgreich gelöscht", diff --git a/locales/en.json b/locales/en.json index 2dec12706..b3723810e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -237,13 +237,13 @@ "yunohost_configured": "YunoHost has been configured", "yunohost_installing": "Installing YunoHost...", "yunohost_not_installed": "YunoHost is not or not correctly installed. Please execute 'yunohost tools postinstall'.", - "certmanager_domain_cert_gen_failed": "Unable to generate certificate", + "domain_cert_gen_failed": "Unable to generate certificate", "certmanager_attempt_to_replace_valid_cert" : "You are attempting to overwrite a good and valid certificate for domain {domain:s} ! (Use --force to bypass)", "certmanager_domain_unknown": "Unknown domain {domain:s}", - "certmanager_domain_cert_not_selfsigned" : "The certificate of domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", + "certmanager_domain_cert_not_selfsigned" : "The certificate for domain {domain:s} is not self-signed. Are you sure you want to replace it ? (Use --force)", "certmanager_certificate_fetching_or_enabling_failed": "Sounds like enabling the new certificate for {domain:s} failed somehow...", - "certmanager_attempt_to_renew_nonLE_cert" : "The certificate of domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", - "certmanager_attempt_to_renew_valid_cert" : "The certificate of domain {domain:s} is not about to expire ! Use --force to bypass", + "certmanager_attempt_to_renew_nonLE_cert" : "The certificate for domain {domain:s} is not issued by Let's Encrypt. Cannot renew it automatically !", + "certmanager_attempt_to_renew_valid_cert" : "The certificate for domain {domain:s} is not about to expire ! Use --force to bypass", "certmanager_domain_http_not_working": "It seems that the domain {domain:s} cannot be accessed through HTTP. Please check your DNS and nginx configuration is okay.", "certmanager_error_no_A_record" : "No DNS 'A' record found for {domain:s}. You need to make your domain name point to your machine to be able to install a Let's Encrypt certificate ! (If you know what you are doing, use --no-checks to disable those checks.)", "certmanager_domain_dns_ip_differs_from_public_ip" : "The DNS 'A' record for domain {domain:s} is different from this server IP. If you recently modified your A record, please wait for it to propagate (some DNS propagation checkers are available online). (If you know what you are doing, use --no-checks to disable those checks.)", diff --git a/locales/es.json b/locales/es.json index fdd04d10f..549cbe29a 100644 --- a/locales/es.json +++ b/locales/es.json @@ -72,7 +72,7 @@ "diagnostic_monitor_system_error": "No se puede monitorizar el sistema: {error}", "diagnostic_no_apps": "Aplicación no instalada", "dnsmasq_isnt_installed": "Parece que dnsmasq no está instalado, ejecuta 'apt-get remove bind9 && apt-get install dnsmasq'", - "certmanager_domain_cert_gen_failed": "No se pudo crear el certificado", + "domain_cert_gen_failed": "No se pudo crear el certificado", "domain_created": "El dominio ha sido creado", "domain_creation_failed": "No se pudo crear el dominio", "domain_deleted": "El dominio ha sido eliminado", diff --git a/locales/fr.json b/locales/fr.json index 6691b2f28..7898de57f 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -73,7 +73,7 @@ "diagnostic_monitor_system_error": "Impossible de superviser le système : {error}", "diagnostic_no_apps": "Aucune application installée", "dnsmasq_isnt_installed": "dnsmasq ne semble pas être installé, veuillez lancer « apt-get remove bind9 && apt-get install dnsmasq »", - "certmanager_domain_cert_gen_failed": "Impossible de générer le certificat", + "domain_cert_gen_failed": "Impossible de générer le certificat", "domain_created": "Le domaine a été créé", "domain_creation_failed": "Impossible de créer le domaine", "domain_deleted": "Le domaine a été supprimé", diff --git a/locales/nl.json b/locales/nl.json index 57b05e309..c2bfed31e 100644 --- a/locales/nl.json +++ b/locales/nl.json @@ -37,7 +37,7 @@ "custom_app_url_required": "U moet een URL opgeven om uw aangepaste app {app:s} bij te werken", "custom_appslist_name_required": "U moet een naam opgeven voor uw aangepaste app-lijst", "dnsmasq_isnt_installed": "dnsmasq lijkt niet geïnstalleerd te zijn, voer alstublieft het volgende commando uit: 'apt-get remove bind9 && apt-get install dnsmasq'", - "certmanager_domain_cert_gen_failed": "Kan certificaat niet genereren", + "domain_cert_gen_failed": "Kan certificaat niet genereren", "domain_created": "Domein succesvol aangemaakt", "domain_creation_failed": "Kan domein niet aanmaken", "domain_deleted": "Domein succesvol verwijderd", diff --git a/locales/pt.json b/locales/pt.json index b9c9e4bce..d3796d2e9 100644 --- a/locales/pt.json +++ b/locales/pt.json @@ -36,7 +36,7 @@ "backup_output_directory_not_empty": "A pasta de destino não se encontra vazia", "custom_app_url_required": "Deve proporcionar uma URL para atualizar a sua aplicação personalizada {app:s}", "custom_appslist_name_required": "Deve fornecer um nome para a sua lista de aplicações personalizada", - "certmanager_domain_cert_gen_failed": "Não foi possível gerar o certificado", + "domain_cert_gen_failed": "Não foi possível gerar o certificado", "domain_created": "Domínio criado com êxito", "domain_creation_failed": "Não foi possível criar o domínio", "domain_deleted": "Domínio removido com êxito", diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 8b3db0283..961cf18fb 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -82,6 +82,8 @@ def certificate_status(auth, domain_list, full=False): """ # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore _check_old_letsencrypt_app() # If no domains given, consider all yunohost domains @@ -127,16 +129,18 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si """ # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore _check_old_letsencrypt_app() if self_signed: - certificate_install_selfsigned(domain_list, force) + _certificate_install_selfsigned(domain_list, force) else: - certificate_install_letsencrypt(auth, domain_list, force, no_checks) + _certificate_install_letsencrypt(auth, domain_list, force, no_checks) -def certificate_install_selfsigned(domain_list, force=False): +def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: # Check we ain't trying to overwrite a good cert ! @@ -182,7 +186,7 @@ def certificate_install_selfsigned(domain_list, force=False): out, _ = p.communicate() if p.returncode != 0: logger.warning(out) - raise MoulinetteError(errno.EIO, m18n.n('certmanager_domain_cert_gen_failed')) + raise MoulinetteError(errno.EIO, m18n.n('domain_cert_gen_failed')) else : logger.info(out) @@ -216,7 +220,7 @@ def certificate_install_selfsigned(domain_list, force=False): -def certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): +def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -276,6 +280,8 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal """ # Check if old letsencrypt_ynh is installed + # TODO / FIXME - Remove this in the future once the letsencrypt app is + # not used anymore _check_old_letsencrypt_app() # If no domains given, consider all yunohost domains with Let's Encrypt @@ -283,12 +289,12 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: - # Does it has a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert ? status = _get_status(domain) if status["CA_type"]["code"] != "lets-encrypt": continue - # Does it expires soon ? + # Does it expire soon ? if force or status["validity"] <= VALIDITY_LIMIT: domain_list.append(domain) @@ -305,11 +311,11 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal status = _get_status(domain) - # Does it expires soon ? + # Does it expire soon ? if not force or status["validity"] <= VALIDITY_LIMIT: raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) - # Does it has a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert ? if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) @@ -383,7 +389,7 @@ def _email_renewing_failed(domain, exception_message, stack): logs = _tail(50, "/var/log/yunohost/yunohost-cli.log") text = """ -At attempt for renewing the certificate for domain %s failed with the following +An attempt for renewing the certificate for domain %s failed with the following error : %s diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index aadd9086d..7d34aff53 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -114,7 +114,7 @@ def domain_add(auth, domain, dyndns=False): m18n.n('domain_dyndns_root_unknown')) try: - yunohost.certificate.certificate_install_selfsigned([domain], False) + yunohost.certificate._certificate_install_selfsigned([domain], False) try: auth.validate_uniqueness({'virtualdomain': domain}) From c57e343d45bc0f1b0b8f40947c857e8e0a50b415 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Mon, 21 Nov 2016 11:24:55 -0500 Subject: [PATCH 66/82] Looking for ssowat header in https (workaround for when app is installed on root domain) --- src/yunohost/certificate.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 961cf18fb..cca582ed7 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -731,8 +731,11 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - r = requests.head("http://" + ip, headers={"Host": domain}) - # Check we got the ssowat header in the response + # Check HTTP reachability + requests.head("http://" + ip, headers={"Host": domain}) + + # Check we got the ssowat header (in HTTPS) + r = requests.head("https://" + ip, headers={"Host": domain}, verify=False) if "x-sso-wat" not in r.headers.keys(): return False except Exception: From dc731c3af56736e97e4713456f5239aeba66ed40 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:13:39 -0500 Subject: [PATCH 67/82] Using a single generic skipped regex for acme challenge in ssowat conf --- src/yunohost/app.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 83fc8614c..a658a0c34 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -1031,22 +1031,8 @@ def app_ssowatconf(auth): for domain in domains: skipped_urls.extend([domain + '/yunohost/admin', domain + '/yunohost/api']) - # Authorize ACME challenge url if a domain seems configured for it... - for domain in domains: - - # Check ACME challenge file is present in nginx conf - nginx_acme_challenge_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain - - if not os.path.isfile(nginx_acme_challenge_conf_file): - continue - - # Check the file contains the ACME challenge uri - if not '/.well-known/acme-challenge' in open(nginx_acme_challenge_conf_file).read(): - continue - - # If so, then authorize the ACME challenge uri to unprotected regex - unprotected_regex.append(domain + "/%.well%-known/acme%-challenge/.*$") - + # Authorize ACME challenge url + skipped_regex.append("^[^/]*/%.well%-known/acme%-challenge/.*$") conf_dict = { 'portal_domain': main_domain, From fb29bb879b860de7c214ccacac9d164b647d5773 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:23:14 -0500 Subject: [PATCH 68/82] Removing check for ssowat header when testing HTTP reachability --- src/yunohost/certificate.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index cca582ed7..22346a460 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -731,13 +731,7 @@ def _dns_ip_match_public_ip(public_ip, domain): def _domain_is_accessible_through_HTTP(ip, domain): try: - # Check HTTP reachability requests.head("http://" + ip, headers={"Host": domain}) - - # Check we got the ssowat header (in HTTPS) - r = requests.head("https://" + ip, headers={"Host": domain}, verify=False) - if "x-sso-wat" not in r.headers.keys(): - return False except Exception: return False From be061522e6818a14d4d0d23280292b45a593cc93 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:32:33 -0500 Subject: [PATCH 69/82] Moving full letsencrypt app conflict warning to locales/en.json --- locales/en.json | 2 +- src/yunohost/certificate.py | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/locales/en.json b/locales/en.json index b3723810e..10dc41edb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -251,5 +251,5 @@ "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_old_letsencrypt_app_detected" : "Command aborted because the letsencrypt app is conflicting with the yunohost certificate management features." + "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 22346a460..7a3ded247 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -354,21 +354,6 @@ def _check_old_letsencrypt_app(): if "letsencrypt" not in installedAppIds: return - logger.warning(" ") - logger.warning("Yunohost detected that the 'letsencrypt' app is installed, ") - logger.warning("which conflits with the new certificate management features") - logger.warning("directly integrated in Yunohost. If you wish to use these ") - logger.warning("new features, please run the following commands to migrate ") - logger.warning("your installation :") - logger.warning(" ") - logger.warning(" yunohost app remove letsencrypt") - logger.warning(" yunohost domain cert-install") - logger.warning(" ") - logger.warning("N.B. : this will attempt to re-install certificates for ") - logger.warning("all domains with a Let's Encrypt certificate or self-signed") - logger.warning("certificate.") - logger.warning(" ") - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) From 0132cf037f05955055b22b9d91909b9b8c3a8f87 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 20:38:33 -0500 Subject: [PATCH 70/82] Moving DNS resolvers IP to constant var at beginning of file --- src/yunohost/certificate.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 7a3ded247..ec733f657 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -67,6 +67,18 @@ CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" +DNS_RESOLVERS = [ + # FFDN DNS resolvers + # See https://www.ffdn.org/wiki/doku.php?id=formations:dns + "80.67.169.12", # FDN + "80.67.169.40", # + "89.234.141.66", # ARN + "141.255.128.100", # Aquilenet + "141.255.128.101", # + "89.234.186.18", # Grifon + "80.67.188.188" # LDN +] + ############################################################################### # Front-end stuff # ############################################################################### @@ -695,16 +707,7 @@ def _check_domain_is_ready_for_ACME(domain): def _dns_ip_match_public_ip(public_ip, domain): try: resolver = dns.resolver.Resolver() - resolver.nameservers = [] - # FFDN DNS resolvers - # See https://www.ffdn.org/wiki/doku.php?id=formations:dns - resolver.nameservers.append("80.67.169.12") # FDN - resolver.nameservers.append("80.67.169.40") # - resolver.nameservers.append("89.234.141.66") # ARN - resolver.nameservers.append("141.255.128.100") # Aquilenet - resolver.nameservers.append("141.255.128.101") # - resolver.nameservers.append("89.234.186.18") # Grifon - resolver.nameservers.append("80.67.188.188") # LDN + resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) From a6353703bd9a6bb3a12edb72ad052d97608b841a Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 22:24:54 -0500 Subject: [PATCH 71/82] Catching exceptions from acme-tiny --- locales/en.json | 4 +++- src/yunohost/certificate.py | 24 ++++++++++++++++-------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/locales/en.json b/locales/en.json index 10dc41edb..85b25efe9 100644 --- a/locales/en.json +++ b/locales/en.json @@ -251,5 +251,7 @@ "certmanager_cert_install_success_selfsigned" : "Successfully installed a self-signed certificate for domain {domain:s} !", "certmanager_cert_install_success" : "Successfully installed Let's Encrypt certificate for domain {domain:s} !", "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", - "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate." + "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", + "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", + "certmanager_cert_signing_failed" : "Signing the new certificate failed." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index ec733f657..99e6bd014 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -31,7 +31,6 @@ import grp import smtplib import requests import subprocess - import dns.resolver from OpenSSL import crypto @@ -470,11 +469,20 @@ def _fetch_and_enable_new_certificate(domain): domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, - domain_csr_file, - WEBROOT_FOLDER, - log=logger, - CA=CERTIFICATION_AUTHORITY) + try: + signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, + domain_csr_file, + WEBROOT_FOLDER, + log=logger, + CA=CERTIFICATION_AUTHORITY) + except ValueError as e: + if ("urn:acme:error:rateLimited" in str(e)) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) + else : + raise + except Exception as e: + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) + logger.error(str(e)) intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text @@ -612,10 +620,10 @@ def _get_status(domain): "verbose": "Unknown?", } - try : + try: _check_domain_is_ready_for_ACME(domain) ACME_eligible = True - except : + except: ACME_eligible = False return { From ed16cd7f5aed448cd76a3da99f82f75732ec2f18 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Tue, 22 Nov 2016 23:44:23 -0500 Subject: [PATCH 72/82] Adding an option to use the staging Let's Encrypt CA, sort of a dry-run --- data/actionsmap/yunohost.yml | 7 ++++++ src/yunohost/certificate.py | 47 +++++++++++++++++++++++++----------- src/yunohost/domain.py | 8 +++--- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/data/actionsmap/yunohost.yml b/data/actionsmap/yunohost.yml index 18f470a61..c1126ece0 100644 --- a/data/actionsmap/yunohost.yml +++ b/data/actionsmap/yunohost.yml @@ -340,6 +340,9 @@ domain: --self-signed: help: Install self-signed certificate instead of Let's Encrypt action: store_true + --staging: + help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. + action: store_true ### certificate_renew() cert-renew: @@ -361,6 +364,10 @@ domain: --no-checks: help: Does not perform any check that your domain seems correctly configured (DNS, reachability) before attempting to renew. (Not recommended) action: store_true + --staging: + help: Use the fake/staging Let's Encrypt certification authority. The new certificate won't actually be enabled - it is only intended to test the main steps of the procedure. + action: store_true + ### domain_info() # info: diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 99e6bd014..3db94e8b8 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -60,9 +60,9 @@ KEY_SIZE = 3072 VALIDITY_LIMIT = 15 # days # For tests -#CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" +STAGING_CERTIFICATION_AUTHORITY = "https://acme-staging.api.letsencrypt.org" # For prod -CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" +PRODUCTION_CERTIFICATION_AUTHORITY = "https://acme-v01.api.letsencrypt.org" INTERMEDIATE_CERTIFICATE_URL = "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" @@ -126,7 +126,7 @@ def certificate_status(auth, domain_list, full=False): return {"certificates" : certificates} -def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False): +def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): """ Install a Let's Encrypt certificate for given domains (all by default) @@ -148,7 +148,7 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(auth, domain_list, force, no_checks) + _certificate_install_letsencrypt(auth, domain_list, force, no_checks, staging) def _certificate_install_selfsigned(domain_list, force=False): @@ -231,7 +231,7 @@ def _certificate_install_selfsigned(domain_list, force=False): -def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False): +def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): if not os.path.exists(ACCOUNT_KEY_FILE): _generate_account_key() @@ -258,6 +258,9 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if not force and status["CA_type"]["code"] != "self-signed": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) + if (staging): + logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + # Actual install steps for domain in domain_list: @@ -268,7 +271,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F _check_domain_is_ready_for_ACME(domain) _configure_for_acme_challenge(auth, domain) - _fetch_and_enable_new_certificate(domain) + _fetch_and_enable_new_certificate(domain, staging) _install_cron() logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) @@ -278,7 +281,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F logger.error(str(e)) -def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False): +def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): """ Renew Let's Encrypt certificate for given domains (all by default) @@ -330,6 +333,9 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + if (staging): + logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + # Actual renew steps for domain in domain_list: logger.info("Now attempting renewing of certificate for domain %s !", domain) @@ -337,7 +343,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal try: if not no_checks: _check_domain_is_ready_for_ACME(domain) - _fetch_and_enable_new_certificate(domain) + _fetch_and_enable_new_certificate(domain, staging) logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) @@ -442,7 +448,7 @@ location '/.well-known/acme-challenge' app_ssowatconf(auth) -def _fetch_and_enable_new_certificate(domain): +def _fetch_and_enable_new_certificate(domain, staging=False): # Make sure tmp folder exists logger.debug("Making sure tmp folders exists...") @@ -469,20 +475,25 @@ def _fetch_and_enable_new_certificate(domain): domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) + if (staging): + certification_authority = STAGING_CERTIFICATION_AUTHORITY + else: + certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY + try: signed_certificate = sign_certificate(ACCOUNT_KEY_FILE, domain_csr_file, WEBROOT_FOLDER, log=logger, - CA=CERTIFICATION_AUTHORITY) + CA=certification_authority) except ValueError as e: - if ("urn:acme:error:rateLimited" in str(e)) : + if ("urn:acme:error:rateLimited" in str(e)): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) - else : + else: raise except Exception as e: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text @@ -492,7 +503,12 @@ def _fetch_and_enable_new_certificate(domain): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s-history/%s-letsencrypt" % (CERT_FOLDER, domain, date_tag) + if (staging): + folder_flag = "staging" + else: + folder_flag = "letsencrypt" + + new_cert_folder = "%s/%s-history/%s-%s" % (CERT_FOLDER, domain, date_tag, folder_flag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -509,6 +525,9 @@ def _fetch_and_enable_new_certificate(domain): _set_permissions(domain_cert_file, "root", "metronome", 0640) + if (staging): + return + _enable_certificate(domain, new_cert_folder) # Check the status of the certificate is now good diff --git a/src/yunohost/domain.py b/src/yunohost/domain.py index 7d34aff53..415585087 100644 --- a/src/yunohost/domain.py +++ b/src/yunohost/domain.py @@ -261,12 +261,12 @@ def domain_cert_status(auth, domain_list, full=False): return yunohost.certificate.certificate_status(auth, domain_list, full) -def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False): - return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed) +def domain_cert_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): + return yunohost.certificate.certificate_install(auth, domain_list, force, no_checks, self_signed, staging) -def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False): - return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email) +def domain_cert_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): + return yunohost.certificate.certificate_renew(auth, domain_list, force, no_checks, email, staging) def get_public_ip(protocol=4): From e66a7085202a33b9f4688a28a994b02974a28ee3 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 23 Nov 2016 11:46:52 -0500 Subject: [PATCH 73/82] Misc tweaks on exceptions --- locales/en.json | 3 ++- src/yunohost/certificate.py | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/locales/en.json b/locales/en.json index 85b25efe9..637671a2d 100644 --- a/locales/en.json +++ b/locales/en.json @@ -253,5 +253,6 @@ "certmanager_cert_renew_success" : "Successfully renewed Let's Encrypt certificate for domain {domain:s} !", "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", - "certmanager_cert_signing_failed" : "Signing the new certificate failed." + "certmanager_cert_signing_failed" : "Signing the new certificate failed.", + "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})" } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3db94e8b8..d892e6f21 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -155,10 +155,11 @@ def _certificate_install_selfsigned(domain_list, force=False): for domain in domain_list: # Check we ain't trying to overwrite a good cert ! - status = _get_status(domain) + if (not force) : + status = _get_status(domain) - if status and status["summary"]["code"] in ('good', 'great') and not force: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + if status["summary"]["code"] in ('good', 'great') : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") @@ -490,7 +491,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): if ("urn:acme:error:rateLimited" in str(e)): raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) else: - raise + logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) except Exception as e: logger.error(str(e)) raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) @@ -565,7 +567,7 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - return {} + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_cert_file', domain=domain, file=cert_file)) try: cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) From 195c675c590b5b260883f5ad90e0958e5e9f3ec7 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Wed, 23 Nov 2016 21:36:34 -0500 Subject: [PATCH 74/82] More exception handling, this time for previous acme challenge conf already existing in nginx --- locales/en.json | 3 ++- src/yunohost/certificate.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/locales/en.json b/locales/en.json index 637671a2d..efeb66e69 100644 --- a/locales/en.json +++ b/locales/en.json @@ -254,5 +254,6 @@ "certmanager_old_letsencrypt_app_detected" : "\nYunohost detected that the 'letsencrypt' app is installed, which conflits with the new built-in certificate management features in Yunohost. If you wish to use the new built-in features, please run the following commands to migrate your installation :\n\n yunohost app remove letsencrypt\n yunohost domain cert-install\n\nN.B. : this will attempt to re-install certificates for all domains with a Let's Encrypt certificate or self-signed certificate.", "certmanager_hit_rate_limit" :"Too many certificates already issued for exact set of domains {domain:s} recently. Please try again later. See https://letsencrypt.org/docs/rate-limits/ for more details.", "certmanager_cert_signing_failed" : "Signing the new certificate failed.", - "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})" + "certmanager_no_cert_file" : "Unable to read certificate file for domain {domain:s} (file : {file:s})", + "certmanager_conflicting_nginx_file": "Unable to prepare domain for ACME challenge : the nginx configuration file {filepath:s} is conflicting and should be removed first." } diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d892e6f21..d9110b79b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -32,6 +32,7 @@ import smtplib import requests import subprocess import dns.resolver +import glob from OpenSSL import crypto from datetime import datetime @@ -152,15 +153,9 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si def _certificate_install_selfsigned(domain_list, force=False): + for domain in domain_list: - # Check we ain't trying to overwrite a good cert ! - if (not force) : - status = _get_status(domain) - - if status["summary"]["code"] in ('good', 'great') : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) - # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) @@ -175,6 +170,14 @@ def _certificate_install_selfsigned(domain_list, force=False): crt_file = os.path.join(new_cert_folder, "crt.pem") ca_file = os.path.join(new_cert_folder, "ca.pem") + # Check we ain't trying to overwrite a good cert ! + current_cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") + if (not force) and (os.path.isfile(current_cert_file)): + status = _get_status(domain) + + if status["summary"]["code"] in ('good', 'great') : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + # Create output folder for new certificate stuff os.makedirs(new_cert_folder) @@ -261,7 +264,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F if (staging): logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") - + # Actual install steps for domain in domain_list: @@ -336,7 +339,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if (staging): logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") - + # Actual renew steps for domain in domain_list: logger.info("Now attempting renewing of certificate for domain %s !", domain) @@ -421,7 +424,9 @@ Subject: %s def _configure_for_acme_challenge(auth, domain): - nginx_conf_file = "/etc/nginx/conf.d/%s.d/000-acmechallenge.conf" % domain + + nginx_conf_folder = "/etc/nginx/conf.d/%s.d" % domain + nginx_conf_file = "%s/000-acmechallenge.conf" % nginx_conf_folder nginx_configuration = ''' location '/.well-known/acme-challenge' @@ -431,6 +436,15 @@ location '/.well-known/acme-challenge' } ''' % WEBROOT_FOLDER + # Check there isn't a conflicting file for the acme-challenge well-known uri + for path in glob.glob('%s/*.conf' % nginx_conf_folder): + if (path == nginx_conf_file) : + continue + with open(path) as f: + contents = f.read() + if ('/.well-known/acme-challenge' in contents) : + raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_conflicting_nginx_file', filepath=path)) + # Write the conf if os.path.exists(nginx_conf_file): logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") @@ -564,6 +578,7 @@ def _prepare_certificate_signing_request(domain, key_file, output_folder): def _get_status(domain): + cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): From 1b20899f0efeb81f945f28fce83f77e5d0afb5f0 Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 24 Nov 2016 10:40:01 -0500 Subject: [PATCH 75/82] Daily renew cron job instead of weekly --- src/yunohost/certificate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index d9110b79b..81f2f0e2b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -379,7 +379,7 @@ def _check_old_letsencrypt_app(): def _install_cron(): - cron_job_file = "/etc/cron.weekly/yunohost-certificate-renew" + cron_job_file = "/etc/cron.daily/yunohost-certificate-renew" with open(cron_job_file, "w") as f: f.write("#!/bin/bash\n") From ddcc57eb9db20e55fcb2bc91ba6b70054dd3182f Mon Sep 17 00:00:00 2001 From: Alexandre Aubin Date: Thu, 24 Nov 2016 10:59:25 -0500 Subject: [PATCH 76/82] pep8 --- src/yunohost/certificate.py | 144 ++++++++++++++++++++++-------------- 1 file changed, 89 insertions(+), 55 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 81f2f0e2b..b9fcb9b3e 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -73,8 +73,8 @@ DNS_RESOLVERS = [ "80.67.169.12", # FDN "80.67.169.40", # "89.234.141.66", # ARN - "141.255.128.100", # Aquilenet - "141.255.128.101", # + "141.255.128.100", # Aquilenet + "141.255.128.101", "89.234.186.18", # Grifon "80.67.188.188" # LDN ] @@ -107,7 +107,8 @@ def certificate_status(auth, domain_list, full=False): for domain in domain_list: # Is it in Yunohost domain list ? if domain not in yunohost_domains_list: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) certificates = {} @@ -124,11 +125,10 @@ def certificate_status(auth, domain_list, full=False): del status["domain"] certificates[domain] = status - return {"certificates" : certificates} + return {"certificates": certificates} def certificate_install(auth, domain_list, force=False, no_checks=False, self_signed=False, staging=False): - """ Install a Let's Encrypt certificate for given domains (all by default) @@ -145,11 +145,11 @@ def certificate_install(auth, domain_list, force=False, no_checks=False, self_si # not used anymore _check_old_letsencrypt_app() - if self_signed: _certificate_install_selfsigned(domain_list, force) else: - _certificate_install_letsencrypt(auth, domain_list, force, no_checks, staging) + _certificate_install_letsencrypt( + auth, domain_list, force, no_checks, staging) def _certificate_install_selfsigned(domain_list, force=False): @@ -158,7 +158,8 @@ def _certificate_install_selfsigned(domain_list, force=False): # Paths of files and folder we'll need date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - new_cert_folder = "%s/%s-history/%s-selfsigned" % (CERT_FOLDER, domain, date_tag) + new_cert_folder = "%s/%s-history/%s-selfsigned" % ( + CERT_FOLDER, domain, date_tag) original_ca_file = '/etc/ssl/certs/ca-yunohost_crt.pem' ssl_dir = '/usr/share/yunohost/yunohost-config/ssl/yunoCA' @@ -175,17 +176,18 @@ def _certificate_install_selfsigned(domain_list, force=False): if (not force) and (os.path.isfile(current_cert_file)): status = _get_status(domain) - if status["summary"]["code"] in ('good', 'great') : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_replace_valid_cert', domain=domain)) + if status["summary"]["code"] in ('good', 'great'): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_replace_valid_cert', domain=domain)) # Create output folder for new certificate stuff os.makedirs(new_cert_folder) # Create our conf file, based on template, replacing the occurences of # "yunohost.org" with the given domain - with open(conf_file, "w") as f : - with open(conf_template, "r") as template : - for line in template : + with open(conf_file, "w") as f: + with open(conf_template, "r") as template: + for line in template: f.write(line.replace("yunohost.org", domain)) # Use OpenSSL command line to create a certificate signing request, @@ -196,13 +198,15 @@ def _certificate_install_selfsigned(domain_list, force=False): commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" % (conf_file, csr_file, crt_file)) - for command in commands : - p = subprocess.Popen(command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + for command in commands: + p = subprocess.Popen( + command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) out, _ = p.communicate() if p.returncode != 0: logger.warning(out) - raise MoulinetteError(errno.EIO, m18n.n('domain_cert_gen_failed')) - else : + raise MoulinetteError( + errno.EIO, m18n.n('domain_cert_gen_failed')) + else: logger.info(out) # Link the CA cert (not sure it's actually needed in practice though, @@ -229,10 +233,11 @@ def _certificate_install_selfsigned(domain_list, force=False): status = _get_status(domain) if status and status["CA_type"]["code"] == "self-signed" and status["validity"] > 3648: - logger.success(m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) - else : - logger.error("Installation of self-signed certificate installation for %s failed !", domain) - + logger.success( + m18n.n("certmanager_cert_install_success_selfsigned", domain=domain)) + else: + logger.error( + "Installation of self-signed certificate installation for %s failed !", domain) def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=False, staging=False): @@ -255,20 +260,24 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F for domain in domain_list: yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] if domain not in yunohost_domains_list: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) # Is it self-signed ? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_cert_not_selfsigned', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_cert_not_selfsigned', domain=domain)) if (staging): - logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + logger.warning( + "Please note that you used the --staging option, and that no new certificate will actually be enabled !") # Actual install steps for domain in domain_list: - logger.info("Now attempting install of certificate for domain %s!", domain) + logger.info( + "Now attempting install of certificate for domain %s!", domain) try: if not no_checks: @@ -278,7 +287,8 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F _fetch_and_enable_new_certificate(domain, staging) _install_cron() - logger.success(m18n.n("certmanager_cert_install_success", domain=domain)) + logger.success( + m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: logger.error("Certificate installation for %s failed !", domain) @@ -325,31 +335,37 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal # Is it in Yunohost dmomain list ? if domain not in yunohost.domain.domain_list(auth)['domains']: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_unknown', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_unknown', domain=domain)) status = _get_status(domain) # Does it expire soon ? if not force or status["validity"] <= VALIDITY_LIMIT: - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_valid_cert', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_renew_valid_cert', domain=domain)) # Does it have a Let's Encrypt cert ? if status["CA_type"]["code"] != "lets-encrypt": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_attempt_to_renew_nonLE_cert', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) if (staging): - logger.warning("Please note that you used the --staging option, and that no new certificate will actually be enabled !") + logger.warning( + "Please note that you used the --staging option, and that no new certificate will actually be enabled !") # Actual renew steps for domain in domain_list: - logger.info("Now attempting renewing of certificate for domain %s !", domain) + logger.info( + "Now attempting renewing of certificate for domain %s !", domain) try: if not no_checks: _check_domain_is_ready_for_ACME(domain) _fetch_and_enable_new_certificate(domain, staging) - logger.success(m18n.n("certmanager_cert_renew_success", domain=domain)) + logger.success( + m18n.n("certmanager_cert_renew_success", domain=domain)) except Exception as e: import traceback @@ -375,7 +391,8 @@ def _check_old_letsencrypt_app(): if "letsencrypt" not in installedAppIds: return - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_old_letsencrypt_app_detected')) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_old_letsencrypt_app_detected')) def _install_cron(): @@ -436,21 +453,25 @@ location '/.well-known/acme-challenge' } ''' % WEBROOT_FOLDER - # Check there isn't a conflicting file for the acme-challenge well-known uri + # Check there isn't a conflicting file for the acme-challenge well-known + # uri for path in glob.glob('%s/*.conf' % nginx_conf_folder): - if (path == nginx_conf_file) : + if (path == nginx_conf_file): continue with open(path) as f: contents = f.read() - if ('/.well-known/acme-challenge' in contents) : - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_conflicting_nginx_file', filepath=path)) + if ('/.well-known/acme-challenge' in contents): + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_conflicting_nginx_file', filepath=path)) # Write the conf if os.path.exists(nginx_conf_file): - logger.info("Nginx configuration file for ACME challenge already exists for domain, skipping.") + logger.info( + "Nginx configuration file for ACME challenge already exists for domain, skipping.") return - logger.info("Adding Nginx configuration file for Acme challenge for domain %s.", domain) + logger.info( + "Adding Nginx configuration file for Acme challenge for domain %s.", domain) with open(nginx_conf_file, "w") as f: f.write(nginx_configuration) @@ -477,7 +498,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _set_permissions(TMP_FOLDER, "root", "root", 0640) # Prepare certificate signing request - logger.info("Prepare key and certificate signing request (CSR) for %s...", domain) + logger.info( + "Prepare key and certificate signing request (CSR) for %s...", domain) domain_key_file = "%s/%s.pem" % (TMP_FOLDER, domain) _generate_key(domain_key_file) @@ -503,13 +525,16 @@ def _fetch_and_enable_new_certificate(domain, staging=False): CA=certification_authority) except ValueError as e: if ("urn:acme:error:rateLimited" in str(e)): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_hit_rate_limit', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_hit_rate_limit', domain=domain)) else: logger.error(str(e)) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cert_signing_failed')) except Exception as e: logger.error(str(e)) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cert_signing_failed')) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cert_signing_failed')) intermediate_certificate = requests.get(INTERMEDIATE_CERTIFICATE_URL).text @@ -524,7 +549,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): else: folder_flag = "letsencrypt" - new_cert_folder = "%s/%s-history/%s-%s" % (CERT_FOLDER, domain, date_tag, folder_flag) + new_cert_folder = "%s/%s-history/%s-%s" % ( + CERT_FOLDER, domain, date_tag, folder_flag) os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655) @@ -550,7 +576,8 @@ def _fetch_and_enable_new_certificate(domain, staging=False): status_summary = _get_status(domain)["summary"] if status_summary["code"] != "great": - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_certificate_fetching_or_enabling_failed', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_certificate_fetching_or_enabling_failed', domain=domain)) def _prepare_certificate_signing_request(domain, key_file, output_folder): @@ -582,14 +609,17 @@ def _get_status(domain): cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") if not os.path.isfile(cert_file): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_no_cert_file', domain=domain, file=cert_file)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_no_cert_file', domain=domain, file=cert_file)) try: - cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(cert_file).read()) + cert = crypto.load_certificate( + crypto.FILETYPE_PEM, open(cert_file).read()) except Exception as exception: import traceback traceback.print_exc(file=sys.stdout) - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_cannot_read_cert', domain=domain, file=cert_file, reason=exception)) cert_subject = cert.get_subject().CN cert_issuer = cert.get_issuer().CN @@ -626,7 +656,7 @@ def _get_status(domain): "verbose": "CRITICAL", } - elif CA_type["code"] in ("self-signed","fake-lets-encrypt"): + elif CA_type["code"] in ("self-signed", "fake-lets-encrypt"): status_summary = { "code": "warning", "verbose": "WARNING", @@ -699,13 +729,13 @@ def _set_permissions(path, user, group, permissions): os.chmod(path, permissions) -def _enable_certificate(domain, new_cert_folder) : +def _enable_certificate(domain, new_cert_folder): logger.info("Enabling the certificate for domain %s ...", domain) live_link = os.path.join(CERT_FOLDER, domain) # If a live link (or folder) already exists - if os.path.exists(live_link) : + if os.path.exists(live_link): # If it's not a link ... expect if to be a folder if not os.path.islink(live_link): # Backup it and remove it @@ -741,11 +771,13 @@ def _check_domain_is_ready_for_ACME(domain): # Check if IP from DNS matches public IP if not _dns_ip_match_public_ip(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) # Check if domain seems to be accessible through HTTP ? if not _domain_is_accessible_through_HTTP(public_ip, domain): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_domain_http_not_working', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_domain_http_not_working', domain=domain)) def _dns_ip_match_public_ip(public_ip, domain): @@ -754,7 +786,8 @@ def _dns_ip_match_public_ip(public_ip, domain): resolver.nameservers = DNS_RESOLVERS answers = resolver.query(domain, "A") except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): - raise MoulinetteError(errno.EINVAL, m18n.n('certmanager_error_no_A_record', domain=domain)) + raise MoulinetteError(errno.EINVAL, m18n.n( + 'certmanager_error_no_A_record', domain=domain)) dns_ip = str(answers[0]) @@ -771,7 +804,8 @@ def _domain_is_accessible_through_HTTP(ip, domain): def _name_self_CA(): - cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) + cert = crypto.load_certificate( + crypto.FILETYPE_PEM, open(SELF_CA_FILE).read()) return cert.get_subject().CN From 7f046f088003ed9effe56ad90abac833f35fefc9 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:32:07 +0100 Subject: [PATCH 77/82] [mod] no space before a ? in english --- src/yunohost/certificate.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b9fcb9b3e..3660698ea 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -105,7 +105,7 @@ def certificate_status(auth, domain_list, full=False): else: yunohost_domains_list = yunohost.domain.domain_list(auth)['domains'] for domain in domain_list: - # Is it in Yunohost domain list ? + # Is it in Yunohost domain list? if domain not in yunohost_domains_list: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_unknown', domain=domain)) @@ -263,13 +263,13 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_unknown', domain=domain)) - # Is it self-signed ? + # Is it self-signed? status = _get_status(domain) if not force and status["CA_type"]["code"] != "self-signed": raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_cert_not_selfsigned', domain=domain)) - if (staging): + if staging: logger.warning( "Please note that you used the --staging option, and that no new certificate will actually be enabled !") @@ -317,12 +317,12 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal if domain_list == []: for domain in yunohost.domain.domain_list(auth)['domains']: - # Does it have a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert? status = _get_status(domain) if status["CA_type"]["code"] != "lets-encrypt": continue - # Does it expire soon ? + # Does it expire soon? if force or status["validity"] <= VALIDITY_LIMIT: domain_list.append(domain) @@ -333,19 +333,19 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal else: for domain in domain_list: - # Is it in Yunohost dmomain list ? + # Is it in Yunohost dmomain list? if domain not in yunohost.domain.domain_list(auth)['domains']: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_unknown', domain=domain)) status = _get_status(domain) - # Does it expire soon ? + # Does it expire soon? if not force or status["validity"] <= VALIDITY_LIMIT: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_valid_cert', domain=domain)) - # Does it have a Let's Encrypt cert ? + # Does it have a Let's Encrypt cert? if status["CA_type"]["code"] != "lets-encrypt": raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) @@ -774,7 +774,7 @@ def _check_domain_is_ready_for_ACME(domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_dns_ip_differs_from_public_ip', domain=domain)) - # Check if domain seems to be accessible through HTTP ? + # Check if domain seems to be accessible through HTTP? if not _domain_is_accessible_through_HTTP(public_ip, domain): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_domain_http_not_working', domain=domain)) From 5b73ab448f33194ced9a8b5e3afcb74490f05186 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:32:24 +0100 Subject: [PATCH 78/82] [mod] create list in one step --- src/yunohost/certificate.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 3660698ea..0133a3387 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -192,11 +192,12 @@ def _certificate_install_selfsigned(domain_list, force=False): # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert - commands = [] - commands.append("openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" - % (conf_file, csr_file, key_file)) - commands.append("openssl ca -config %s -days 3650 -in %s -out %s -batch" - % (conf_file, csr_file, crt_file)) + commands = [ + "openssl req -new -config %s -days 3650 -out %s -keyout %s -nodes -batch" + % (conf_file, csr_file, key_file), + "openssl ca -config %s -days 3650 -in %s -out %s -batch" + % (conf_file, csr_file, crt_file), + ] for command in commands: p = subprocess.Popen( From 86eb9a2405324cab5d9a84335d52aab21ca46580 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:34:12 +0100 Subject: [PATCH 79/82] [fix] avoid reverse order log display on web admin --- src/yunohost/certificate.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index 0133a3387..eec261f20 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -292,8 +292,7 @@ def _certificate_install_letsencrypt(auth, domain_list, force=False, no_checks=F m18n.n("certmanager_cert_install_success", domain=domain)) except Exception as e: - logger.error("Certificate installation for %s failed !", domain) - logger.error(str(e)) + logger.error("Certificate installation for %s failed !\nException: %s", domain, e) def certificate_renew(auth, domain_list, force=False, no_checks=False, email=False, staging=False): From 2f4f2546127cf497b1f2511437c98f6a9acd5f65 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:42:22 +0100 Subject: [PATCH 80/82] [mod] pep8 --- src/yunohost/certificate.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index eec261f20..b9e37dc79 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -173,7 +173,7 @@ def _certificate_install_selfsigned(domain_list, force=False): # Check we ain't trying to overwrite a good cert ! current_cert_file = os.path.join(CERT_FOLDER, domain, "crt.pem") - if (not force) and (os.path.isfile(current_cert_file)): + if not force and os.path.isfile(current_cert_file): status = _get_status(domain) if status["summary"]["code"] in ('good', 'great'): @@ -350,7 +350,7 @@ def certificate_renew(auth, domain_list, force=False, no_checks=False, email=Fal raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_attempt_to_renew_nonLE_cert', domain=domain)) - if (staging): + if staging: logger.warning( "Please note that you used the --staging option, and that no new certificate will actually be enabled !") @@ -456,11 +456,14 @@ location '/.well-known/acme-challenge' # Check there isn't a conflicting file for the acme-challenge well-known # uri for path in glob.glob('%s/*.conf' % nginx_conf_folder): - if (path == nginx_conf_file): + + if path == nginx_conf_file: continue + with open(path) as f: contents = f.read() - if ('/.well-known/acme-challenge' in contents): + + if '/.well-known/acme-challenge' in contents: raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_conflicting_nginx_file', filepath=path)) @@ -512,7 +515,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): domain_csr_file = "%s/%s.csr" % (TMP_FOLDER, domain) - if (staging): + if staging: certification_authority = STAGING_CERTIFICATION_AUTHORITY else: certification_authority = PRODUCTION_CERTIFICATION_AUTHORITY @@ -524,7 +527,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): log=logger, CA=certification_authority) except ValueError as e: - if ("urn:acme:error:rateLimited" in str(e)): + if "urn:acme:error:rateLimited" in str(e): raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_hit_rate_limit', domain=domain)) else: @@ -544,7 +547,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): # Create corresponding directory date_tag = datetime.now().strftime("%Y%m%d.%H%M%S") - if (staging): + if staging: folder_flag = "staging" else: folder_flag = "letsencrypt" @@ -567,7 +570,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): _set_permissions(domain_cert_file, "root", "metronome", 0640) - if (staging): + if staging: return _enable_certificate(domain, new_cert_folder) From 4a9b89d12e0ff22379e74bb7dfa85cd4b441de69 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:42:36 +0100 Subject: [PATCH 81/82] [mod] merge double with --- src/yunohost/certificate.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index b9e37dc79..fb982980b 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -185,10 +185,9 @@ def _certificate_install_selfsigned(domain_list, force=False): # Create our conf file, based on template, replacing the occurences of # "yunohost.org" with the given domain - with open(conf_file, "w") as f: - with open(conf_template, "r") as template: - for line in template: - f.write(line.replace("yunohost.org", domain)) + with open(conf_file, "w") as f, open(conf_template, "r") as template: + for line in template: + f.write(line.replace("yunohost.org", domain)) # Use OpenSSL command line to create a certificate signing request, # and self-sign the cert @@ -216,10 +215,9 @@ def _certificate_install_selfsigned(domain_list, force=False): os.symlink(original_ca_file, ca_file) # Append ca.pem at the end of crt.pem - with open(ca_file, "r") as ca_pem: - with open(crt_file, "a") as crt_pem: - crt_pem.write("\n") - crt_pem.write(ca_pem.read()) + with open(ca_file, "r") as ca_pem, open(crt_file, "a") as crt_pem: + crt_pem.write("\n") + crt_pem.write(ca_pem.read()) # Set appropriate permissions _set_permissions(new_cert_folder, "root", "root", 0755) From f0c29147dd65c5f819f3ade4e5238549cc9c0fe4 Mon Sep 17 00:00:00 2001 From: Laurent Peuch Date: Mon, 28 Nov 2016 14:42:42 +0100 Subject: [PATCH 82/82] [mod] style --- src/yunohost/certificate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/yunohost/certificate.py b/src/yunohost/certificate.py index fb982980b..f8a927e08 100644 --- a/src/yunohost/certificate.py +++ b/src/yunohost/certificate.py @@ -201,7 +201,9 @@ def _certificate_install_selfsigned(domain_list, force=False): for command in commands: p = subprocess.Popen( command.split(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + out, _ = p.communicate() + if p.returncode != 0: logger.warning(out) raise MoulinetteError( @@ -532,8 +534,10 @@ def _fetch_and_enable_new_certificate(domain, staging=False): logger.error(str(e)) raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) + except Exception as e: logger.error(str(e)) + raise MoulinetteError(errno.EINVAL, m18n.n( 'certmanager_cert_signing_failed')) @@ -552,6 +556,7 @@ def _fetch_and_enable_new_certificate(domain, staging=False): new_cert_folder = "%s/%s-history/%s-%s" % ( CERT_FOLDER, domain, date_tag, folder_flag) + os.makedirs(new_cert_folder) _set_permissions(new_cert_folder, "root", "root", 0655)