diff --git a/core/admin/assets/Dockerfile b/core/admin/assets/Dockerfile index 77196726..762925af 100644 --- a/core/admin/assets/Dockerfile +++ b/core/admin/assets/Dockerfile @@ -8,6 +8,8 @@ COPY package.json ./ RUN set -euxo pipefail \ ; npm config set update-notifier false \ + ; echo "#!/bin/sh" >/usr/local/bin/husky \ + ; chmod +x /usr/local/bin/husky \ ; npm install --no-audit --no-fund \ ; sed -i 's/#007bff/#55a5d9/' node_modules/admin-lte/build/scss/_bootstrap-variables.scss \ ; mkdir assets \ diff --git a/core/base/requirements-prod.txt b/core/base/requirements-prod.txt index 1883b906..5bd0c5a1 100644 --- a/core/base/requirements-prod.txt +++ b/core/base/requirements-prod.txt @@ -12,7 +12,7 @@ cffi==1.16.0 charset-normalizer==3.3.2 click==8.1.7 colorclass==2.2.2 -cryptography==41.0.7 +cryptography==42.0.5 defusedxml==0.7.1 Deprecated==1.2.14 dnspython==2.5.0 diff --git a/core/nginx/conf/nginx.conf b/core/nginx/conf/nginx.conf index 7d8f2c33..214744ee 100644 --- a/core/nginx/conf/nginx.conf +++ b/core/nginx/conf/nginx.conf @@ -330,8 +330,8 @@ mail { {% endif %} {% if TLS and not TLS_ERROR %} {% if TLS_FLAVOR in ['letsencrypt','mail-letsencrypt'] %} - ssl_certificate /certs/letsencrypt/live/mailu/fullchain.pem; - ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/fullchain.pem; + ssl_certificate /certs/letsencrypt/live/mailu/DANE-chain.pem; + ssl_certificate /certs/letsencrypt/live/mailu-ecdsa/DANE-chain.pem; {% endif %} {% if TLS_PERMISSIVE %} ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; diff --git a/core/nginx/config.py b/core/nginx/config.py index 96723007..73cc085c 100755 --- a/core/nginx/config.py +++ b/core/nginx/config.py @@ -5,6 +5,60 @@ import logging as log import sys from socrate import system, conf +from cryptography import x509 +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat +from cryptography.x509.verification import PolicyBuilder, Store, DNSName +from cryptography.x509.oid import NameOID +import hashlib + +ISRG_ROOT_X1 = x509.load_pem_x509_certificate(b'''-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +''') +ISRG_ROOT_X2 = x509.load_pem_x509_certificate(b'''-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- +''') + args = system.set_env() log.basicConfig(stream=sys.stderr, level=args.get("LOG_LEVEL", "WARNING")) @@ -29,21 +83,47 @@ args["TLS"] = { "notls": None }[args["TLS_FLAVOR"]] -def format_for_nginx(fullchain, output): +def format_for_nginx(fullchain, output, strip_CA=args.get('LETSENCRYPT_SHORTCHAIN')): """ We may want to strip ISRG Root X1 out """ if not os.path.exists(fullchain): return - split = '-----END CERTIFICATE-----\n' - with open(fullchain, 'r') as pem: - certs = [f'{cert}{split}' for cert in pem.read().split(split) if cert] - if len(certs)>2 and args.get('LETSENCRYPT_SHORTCHAIN'): - del certs[-1] - with open(output, 'w') as pem: - pem.write(''.join(certs)) + chain=[] + with open(fullchain, 'rb') as f: + chain = x509.load_pem_x509_certificates(f.read()) + builder = PolicyBuilder().store(Store([ISRG_ROOT_X1, ISRG_ROOT_X2])) + verifier = builder.build_server_verifier(DNSName(chain[0].subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value)) + try: + valid_chain = verifier.verify(chain[0], chain[1:]) + except Exception as e: + log.error(e) + valid_chain = chain + log.info(f'The certificate chain looks as follows for {fullchain}:') + indent = ' ' + has_found_PIN = False + for cert in valid_chain: + pubkey_der = cert.public_key().public_bytes(Encoding.DER, PublicFormat.SubjectPublicKeyInfo) + digest = hashlib.sha256(pubkey_der).hexdigest() + log.info(f'{indent}{cert.subject.rfc4514_string()} {digest}') + indent += ' ' + if digest == '0b9fa5a59eed715c26c1020c711b4f6ec42d58b0015e14337a39dad301c5afc3': # ISRG Root X1 + log.info('ISRG X1 PIN FOUND!') + has_found_PIN = True + elif digest == '762195c225586ee6c0237456e2107dc54f1efc21f61a792ebd515913cce68332': # ISRG Root X2 + log.info('ISRG X2 PIN FOUND!') + has_found_PIN = True + if not has_found_PIN: + log.error('Neither ISRG X1 nor ISRG X2 have been found in the certificate chain. Please check your DANE records.') + with open(output, 'wt') as f: + for cert in valid_chain: + if strip_CA and (cert.subject.rfc4514_string() in ['CN=ISRG Root X1,O=Internet Security Research Group,C=US', 'CN=ISRG Root X2,O=Internet Security Research Group,C=US']): + continue + f.write(f'{cert.public_bytes(encoding=Encoding.PEM).decode("ascii").strip()}\n') if args['TLS_FLAVOR'] in ['letsencrypt', 'mail-letsencrypt']: format_for_nginx('/certs/letsencrypt/live/mailu/fullchain.pem', '/certs/letsencrypt/live/mailu/nginx-chain.pem') + format_for_nginx('/certs/letsencrypt/live/mailu/fullchain.pem', '/certs/letsencrypt/live/mailu/DANE-chain.pem', False) format_for_nginx('/certs/letsencrypt/live/mailu-ecdsa/fullchain.pem', '/certs/letsencrypt/live/mailu-ecdsa/nginx-chain.pem') + format_for_nginx('/certs/letsencrypt/live/mailu-ecdsa/fullchain.pem', '/certs/letsencrypt/live/mailu-ecdsa/DANE-chain.pem', False) if args["TLS"] and not all(os.path.exists(file_path) for file_path in args["TLS"]): print("Missing cert or key file, disabling TLS") @@ -55,4 +135,4 @@ conf.jinja("/conf/proxy.conf", args, "/etc/nginx/proxy.conf") conf.jinja("/conf/nginx.conf", args, "/etc/nginx/nginx.conf") conf.jinja("/dovecot_conf/login.lua", args, "/etc/dovecot/login.lua") conf.jinja("/dovecot_conf/proxy.conf", args, "/etc/dovecot/proxy.conf") -os.system("killall -HUP nginx dovecot") +os.system("killall -q -HUP nginx dovecot") diff --git a/towncrier/newsfragments/3187.bugfix b/towncrier/newsfragments/3187.bugfix new file mode 100644 index 00000000..b17de0c3 --- /dev/null +++ b/towncrier/newsfragments/3187.bugfix @@ -0,0 +1 @@ +Ensure we always send ISRG_X1 root when LE is configured. Switch to the non-crossigned version as the other one will expire in September