diff --git a/README-fedora.md b/README-fedora.md index afe51af..9f359a9 100644 --- a/README-fedora.md +++ b/README-fedora.md @@ -74,7 +74,7 @@ Use systemctl start acme-tiny ``` to run the service now. The certificate should appear in `/var/lib/acme/certs`, -and errors will be in journalctl. Alternatively (and on EL6), run +and errors will be in journalctl. Alternatively, run `/usr/libexec/acme-tiny/sign` as the acme user, and errors will go to your terminal. @@ -125,9 +125,8 @@ handle [malicious names](https://www.xkcd.com/327/). ## Logging and Error Reporting -On EL6, cron will email the acme user when certs are signed or errors -are encountered. Under systemd, errors and certs signed are logged -with the acme-tiny syslog identifier. +Under systemd, errors and certs signed are logged with the acme-tiny +syslog identifier. ## Virtual Hosts diff --git a/acme-tiny-chain.patch b/acme-tiny-chain.patch deleted file mode 100644 index 5e681d9..0000000 --- a/acme-tiny-chain.patch +++ /dev/null @@ -1,118 +0,0 @@ -diff -up ./acme_tiny.py.chain ./acme_tiny.py ---- ./acme_tiny.py.chain 2017-05-16 03:57:46.000000000 -0400 -+++ ./acme_tiny.py 2017-11-22 12:18:56.963653336 -0500 -@@ -1,4 +1,4 @@ --#!/usr/bin/env python -+#!/usr/bin/python - import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging - try: - from urllib.request import urlopen # Python 3 -@@ -12,7 +12,7 @@ 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): -+def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, chain=False): - # helper function base64 encode for jose spec - def _b64(b): - return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") -@@ -57,9 +57,9 @@ def get_crt(account_key, csr, acme_dir, - }) - try: - resp = urlopen(url, data.encode('utf8')) -- return resp.getcode(), resp.read() -+ return resp.getcode(), resp.read(), resp.info() - except IOError as e: -- return getattr(e, "code", None), getattr(e, "read", e.__str__)() -+ return getattr(e, "code", None), getattr(e, "read", e.__str__)(), None - - # find domains - log.info("Parsing CSR...") -@@ -80,9 +80,9 @@ def get_crt(account_key, csr, acme_dir, - - # get the certificate domains and expiration - log.info("Registering account...") -- code, result = _send_signed_request(CA + "/acme/new-reg", { -+ code, result, headers = _send_signed_request(CA + "/acme/new-reg", { - "resource": "new-reg", -- "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", -+ "agreement": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf", - }) - if code == 201: - log.info("Registered!") -@@ -96,7 +96,7 @@ def get_crt(account_key, csr, acme_dir, - log.info("Verifying {0}...".format(domain)) - - # get new challenge -- code, result = _send_signed_request(CA + "/acme/new-authz", { -+ code, result, headers = _send_signed_request(CA + "/acme/new-authz", { - "resource": "new-authz", - "identifier": {"type": "dns", "value": domain}, - }) -@@ -123,7 +123,7 @@ def get_crt(account_key, csr, acme_dir, - wellknown_path, wellknown_url)) - - # notify challenge are met -- code, result = _send_signed_request(challenge['uri'], { -+ code, result, headers = _send_signed_request(challenge['uri'], { - "resource": "challenge", - "keyAuthorization": keyauthorization, - }) -@@ -153,17 +153,32 @@ def get_crt(account_key, csr, acme_dir, - 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", { -+ code, result, headers = _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)) - -+ certchain = [result] -+ if chain: -+ def parse_link_header(line): -+ m = re.search(r"^<([^>]*)>(?:\s*;\s*(.*))?$", line) -+ return (m.group(1), dict([(a[0],a[1].strip('"')) -+ for a in [attr.split("=") -+ for attr in m.group(2).split("\s*;\s*")]])) -+ -+ up = [ -+ link for link, attr in [ -+ parse_link_header(l) for l in headers.get_all("Link") -+ ] if attr['rel'] == 'up' -+ ] -+ certchain += [urlopen(url).read() for url in up] -+ - # 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))) -+ return "".join(["""-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( -+ "\n".join(textwrap.wrap(base64.b64encode(cert).decode('utf8'), 64))) for cert in certchain]) - - def main(argv): - parser = argparse.ArgumentParser( -@@ -188,11 +203,19 @@ def main(argv): - 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") -+ parser.add_argument("--chain", action="store_true", -+ help="fetch and append intermediate certs to output") - - 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) -+ try: -+ signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, -+ log=LOGGER, CA=args.ca, chain=args.chain) -+ sys.stdout.write(signed_crt) -+ except Exception as e: -+ #if not args.quiet: raise e -+ LOGGER.error(e) -+ sys.exit(1) - - if __name__ == "__main__": # pragma: no cover - main(sys.argv[1:]) diff --git a/acme-tiny-chain2.patch b/acme-tiny-chain2.patch deleted file mode 100644 index 1c46c7d..0000000 --- a/acme-tiny-chain2.patch +++ /dev/null @@ -1,118 +0,0 @@ -diff -up ./acme_tiny.py.chain ./acme_tiny.py ---- ./acme_tiny.py.chain 2017-05-16 03:57:46.000000000 -0400 -+++ ./acme_tiny.py 2017-11-22 15:14:19.270485351 -0500 -@@ -1,4 +1,4 @@ --#!/usr/bin/env python -+#!/usr/bin/python - import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging - try: - from urllib.request import urlopen # Python 3 -@@ -12,7 +12,7 @@ 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): -+def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, chain=False): - # helper function base64 encode for jose spec - def _b64(b): - return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "") -@@ -57,9 +57,9 @@ def get_crt(account_key, csr, acme_dir, - }) - try: - resp = urlopen(url, data.encode('utf8')) -- return resp.getcode(), resp.read() -+ return resp.getcode(), resp.read(), resp.info() - except IOError as e: -- return getattr(e, "code", None), getattr(e, "read", e.__str__)() -+ return getattr(e, "code", None), getattr(e, "read", e.__str__)(), None - - # find domains - log.info("Parsing CSR...") -@@ -80,9 +80,9 @@ def get_crt(account_key, csr, acme_dir, - - # get the certificate domains and expiration - log.info("Registering account...") -- code, result = _send_signed_request(CA + "/acme/new-reg", { -+ code, result, headers = _send_signed_request(CA + "/acme/new-reg", { - "resource": "new-reg", -- "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf", -+ "agreement": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf", - }) - if code == 201: - log.info("Registered!") -@@ -96,7 +96,7 @@ def get_crt(account_key, csr, acme_dir, - log.info("Verifying {0}...".format(domain)) - - # get new challenge -- code, result = _send_signed_request(CA + "/acme/new-authz", { -+ code, result, headers = _send_signed_request(CA + "/acme/new-authz", { - "resource": "new-authz", - "identifier": {"type": "dns", "value": domain}, - }) -@@ -123,7 +123,7 @@ def get_crt(account_key, csr, acme_dir, - wellknown_path, wellknown_url)) - - # notify challenge are met -- code, result = _send_signed_request(challenge['uri'], { -+ code, result, headers = _send_signed_request(challenge['uri'], { - "resource": "challenge", - "keyAuthorization": keyauthorization, - }) -@@ -153,17 +153,32 @@ def get_crt(account_key, csr, acme_dir, - 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", { -+ code, result, headers = _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)) - -+ certchain = [result] -+ if chain: -+ def parse_link_header(line): -+ m = re.search(r"^Link:\s*<([^>]*)>(?:\s*;\s*(.*))?\r\n$", line) -+ return (m.group(1), dict([(a[0],a[1].strip('"')) -+ for a in [attr.split("=") -+ for attr in m.group(2).split("\s*;\s*")]])) -+ -+ up = [ -+ link for link, attr in [ -+ parse_link_header(l) for l in headers.getallmatchingheaders("Link") -+ ] if attr['rel'] == 'up' -+ ] -+ certchain += [urlopen(url).read() for url in up] -+ - # 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))) -+ return "".join(["""-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format( -+ "\n".join(textwrap.wrap(base64.b64encode(cert).decode('utf8'), 64))) for cert in certchain]) - - def main(argv): - parser = argparse.ArgumentParser( -@@ -188,11 +203,19 @@ def main(argv): - 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") -+ parser.add_argument("--chain", action="store_true", -+ help="fetch and append intermediate certs to output") - - 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) -+ try: -+ signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, -+ log=LOGGER, CA=args.ca, chain=args.chain) -+ sys.stdout.write(signed_crt) -+ except Exception as e: -+ #if not args.quiet: raise e -+ LOGGER.error(e) -+ sys.exit(1) - - if __name__ == "__main__": # pragma: no cover - main(sys.argv[1:]) diff --git a/acme-tiny-sign.sh b/acme-tiny-sign.sh index edceb37..6256f5d 100644 --- a/acme-tiny-sign.sh +++ b/acme-tiny-sign.sh @@ -22,7 +22,8 @@ for csr in csr/*.csr; do crt="${csr%%.csr}" tmp="certs/${crt##csr/}.tmp" crt="certs/${crt##csr/}.crt" - if test -s "$crt" && /usr/sbin/cert-check --days="$DAYS" "$crt"; then + secs=$(( "$DAYS" * 24 * 60 * 60 )) + if test -s "$crt" && openssl x509 -in "$crt" -noout -checkend "$secs"; then continue fi if test -w "$crt" || test ! -e "$crt"; then diff --git a/acme-tiny.cron b/acme-tiny.cron deleted file mode 100644 index 206fd39..0000000 --- a/acme-tiny.cron +++ /dev/null @@ -1,3 +0,0 @@ -# Check daily for csrs in /var/lib/acme/csr that need signing or renewing -# within 7 days. -31 2 * * * acme /usr/libexec/acme-tiny/sign 7 diff --git a/acme-tiny.spec b/acme-tiny.spec index 0e77ba1..84bb6f2 100644 --- a/acme-tiny.spec +++ b/acme-tiny.spec @@ -1,10 +1,4 @@ -%if 0%{?rhel} >= 5 && 0%{?rhel} < 7 -%global use_systemd 0 -%else -%global use_systemd 1 -%endif - %if 0%{?fedora} || 0%{?rhel} > 7 # Explicity require python3 on Fedora to help track which packages # no longer need python2. @@ -15,7 +9,7 @@ Name: acme-tiny Version: 4.1.0 -Release: 2%{?dist} +Release: 3%{?dist} Summary: Tiny auditable script to issue, renew Let's Encrypt certificates License: MIT @@ -24,26 +18,16 @@ Source0: https://github.com/diafygi/%{name}/archive/%{version}.tar.gz#/%{name}-% Source1: acme-tiny-sign.sh Source2: cert-check.py Source3: acme.conf -Source4: lets-encrypt-x3-cross-signed.pem -Source5: acme-tiny.cron Source6: acme-tiny.timer Source7: acme-tiny.service Source8: README-fedora.md # simple script hook to kick services when cert is updated Source9: notify.sh -# Fetch and include intermediate cert(s), too. -Patch0: acme-tiny-chain.patch -# Python3 broke getallmatchingheaders() and the fix breaks python2 -Patch1: acme-tiny-chain2.patch Requires(pre): shadow-utils -%if %{use_systemd} # systemd macros are not defined unless systemd is present BuildRequires: systemd %{?systemd_requires} -%else -Requires: cronie -%endif Requires: %{name}-core = %{version}-%{release} BuildArch: noarch %if 0%{?fedora} @@ -81,10 +65,7 @@ unneeded packages. cp -p %{SOURCE1} %{SOURCE2} %{SOURCE8} . sed -i.orig -e '1,1 s,^.*python$,#!/usr/bin/python,' acme_tiny.py %if %{use_python3} -#patch0 -p1 -b .chain sed -i.old -e '1,1 s/python$/python3/' *.py -#else -#patch1 -p1 -b .chain2 %endif %build @@ -104,16 +85,10 @@ ln -sf acme_tiny %{buildroot}%{_sbindir}/%{name} ln -sf %{_libexecdir}/%{name}/sign %{buildroot}%{_sbindir}/acme-tiny-sign install -m 0755 cert-check.py %{buildroot}%{_sbindir}/cert-check install -m 0644 %{SOURCE3} %{buildroot}%{_sysconfdir}/httpd/conf.d -install -m 0644 %{SOURCE4} %{buildroot}%{_sharedstatedir}/acme install -m 0755 %{SOURCE9} %{buildroot}%{_sysconfdir}/%{name} -%if %{use_systemd} mkdir -p %{buildroot}%{_unitdir} install -pm 644 %{SOURCE6} %{buildroot}%{_unitdir} install -pm 644 %{SOURCE7} %{buildroot}%{_unitdir} -%else -mkdir -p %{buildroot}%{_sysconfdir}/cron.d -install -m 0644 %{SOURCE5} %{buildroot}%{_sysconfdir}/cron.d/acme-tiny -%endif %pre getent group acme > /dev/null || groupadd -r acme @@ -122,8 +97,6 @@ getent passwd acme > /dev/null || /usr/sbin/useradd -g acme \ -r -d %{_sharedstatedir}/acme -s /sbin/nologin acme exit 0 -%if %{use_systemd} - %post %systemd_post acme-tiny.service acme-tiny.timer @@ -133,8 +106,6 @@ exit 0 %preun %systemd_preun acme-tiny.service acme-tiny.timer -%endif - %files %{!?_licensedir:%global license %%doc} %license LICENSE @@ -143,11 +114,7 @@ exit 0 %attr(-,acme,acme) %{_sharedstatedir}/acme %{_libexecdir}/%{name} %config(noreplace) %{_sysconfdir}/httpd/conf.d/acme.conf -%if %{use_systemd} %{_unitdir}/* -%else -%config(noreplace) %{_sysconfdir}/cron.d/%{name} -%endif %{_sbindir}/acme-tiny-sign %{_sbindir}/cert-check %{_sbindir}/%{name} @@ -161,11 +128,15 @@ exit 0 %changelog * Thu Apr 9 2020 Stuart D. Gathman 4.1.0-3 - Update README-fedora.md to describe notify.sh +- Apply selected changes from Marcel Metz : +- Use openssl x509 -checkend parameter to determine certificate expiration +- Remove Let's Encrypt intermediate certificate +- Remove cron job used on non systemd systems * Tue Jan 28 2020 Fedora Release Engineering - 4.1.0-2 - Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild -* Fri Oct 11 2019 Tim Jackson - 4.1.0-1 - Update to 4.1.0 * Fri Oct 11 2019 Stuart D. Gathman 4.0.4-5 diff --git a/lets-encrypt-x3-cross-signed.pem b/lets-encrypt-x3-cross-signed.pem deleted file mode 100644 index 0002462..0000000 --- a/lets-encrypt-x3-cross-signed.pem +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/ -MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT -DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow -SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT -GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC -AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF -q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8 -SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0 -Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA -a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj -/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T -AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG -CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv -bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k -c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw -VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC -ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz -MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu -Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF -AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo -uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/ -wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu -X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG -PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6 -KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg== ------END CERTIFICATE-----