9ddfc06
diff -up ./acme_tiny.py.chain ./acme_tiny.py
880c508
--- ./acme_tiny.py.chain	2017-05-16 03:57:46.000000000 -0400
880c508
+++ ./acme_tiny.py	2017-11-22 12:18:56.963653336 -0500
9ddfc06
@@ -1,4 +1,4 @@
9ddfc06
-#!/usr/bin/env python
bce93d0
+#!/usr/bin/python
9ddfc06
 import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging
9ddfc06
 try:
9ddfc06
     from urllib.request import urlopen # Python 3
9ddfc06
@@ -12,7 +12,7 @@ LOGGER = logging.getLogger(__name__)
9ddfc06
 LOGGER.addHandler(logging.StreamHandler())
9ddfc06
 LOGGER.setLevel(logging.INFO)
9ddfc06
 
9ddfc06
-def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA):
9ddfc06
+def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, chain=False):
9ddfc06
     # helper function base64 encode for jose spec
9ddfc06
     def _b64(b):
9ddfc06
         return base64.urlsafe_b64encode(b).decode('utf8').replace("=", "")
9ddfc06
@@ -57,9 +57,9 @@ def get_crt(account_key, csr, acme_dir,
9ddfc06
         })
9ddfc06
         try:
9ddfc06
             resp = urlopen(url, data.encode('utf8'))
9ddfc06
-            return resp.getcode(), resp.read()
9ddfc06
+            return resp.getcode(), resp.read(), resp.info()
9ddfc06
         except IOError as e:
9ddfc06
-            return getattr(e, "code", None), getattr(e, "read", e.__str__)()
9ddfc06
+            return getattr(e, "code", None), getattr(e, "read", e.__str__)(), None
9ddfc06
 
9ddfc06
     # find domains
9ddfc06
     log.info("Parsing CSR...")
880c508
@@ -80,9 +80,9 @@ def get_crt(account_key, csr, acme_dir,
9ddfc06
 
9ddfc06
     # get the certificate domains and expiration
9ddfc06
     log.info("Registering account...")
9ddfc06
-    code, result = _send_signed_request(CA + "/acme/new-reg", {
9ddfc06
+    code, result, headers = _send_signed_request(CA + "/acme/new-reg", {
9ddfc06
         "resource": "new-reg",
880c508
-        "agreement": "https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf",
880c508
+        "agreement": "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf",
9ddfc06
     })
880c508
     if code == 201:
880c508
         log.info("Registered!")
9ddfc06
@@ -96,7 +96,7 @@ def get_crt(account_key, csr, acme_dir,
9ddfc06
         log.info("Verifying {0}...".format(domain))
9ddfc06
 
9ddfc06
         # get new challenge
9ddfc06
-        code, result = _send_signed_request(CA + "/acme/new-authz", {
9ddfc06
+        code, result, headers = _send_signed_request(CA + "/acme/new-authz", {
9ddfc06
             "resource": "new-authz",
9ddfc06
             "identifier": {"type": "dns", "value": domain},
9ddfc06
         })
9ddfc06
@@ -123,7 +123,7 @@ def get_crt(account_key, csr, acme_dir,
9ddfc06
                 wellknown_path, wellknown_url))
9ddfc06
 
9ddfc06
         # notify challenge are met
9ddfc06
-        code, result = _send_signed_request(challenge['uri'], {
9ddfc06
+        code, result, headers = _send_signed_request(challenge['uri'], {
9ddfc06
             "resource": "challenge",
9ddfc06
             "keyAuthorization": keyauthorization,
9ddfc06
         })
9ddfc06
@@ -153,17 +153,32 @@ def get_crt(account_key, csr, acme_dir,
9ddfc06
     proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"],
9ddfc06
         stdout=subprocess.PIPE, stderr=subprocess.PIPE)
9ddfc06
     csr_der, err = proc.communicate()
9ddfc06
-    code, result = _send_signed_request(CA + "/acme/new-cert", {
9ddfc06
+    code, result, headers = _send_signed_request(CA + "/acme/new-cert", {
9ddfc06
         "resource": "new-cert",
9ddfc06
         "csr": _b64(csr_der),
9ddfc06
     })
9ddfc06
     if code != 201:
9ddfc06
         raise ValueError("Error signing certificate: {0} {1}".format(code, result))
9ddfc06
 
9ddfc06
+    certchain = [result]
9ddfc06
+    if chain:
9ddfc06
+        def parse_link_header(line):
9b5f083
+            m = re.search(r"^<([^>]*)>(?:\s*;\s*(.*))?$", line)
9ddfc06
+            return (m.group(1), dict([(a[0],a[1].strip('"'))
9ddfc06
+                for a in [attr.split("=") 
9ddfc06
+                    for attr in m.group(2).split("\s*;\s*")]]))
9ddfc06
+
9ddfc06
+        up = [
9ddfc06
+          link for link, attr in [
9b5f083
+            parse_link_header(l) for l in headers.get_all("Link")
9ddfc06
+          ] if attr['rel'] == 'up'
9ddfc06
+        ]
9ddfc06
+        certchain += [urlopen(url).read() for url in up]
9ddfc06
+
9ddfc06
     # return signed certificate!
9ddfc06
     log.info("Certificate signed!")
9ddfc06
-    return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
9ddfc06
-        "\n".join(textwrap.wrap(base64.b64encode(result).decode('utf8'), 64)))
9ddfc06
+    return "".join(["""-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
9ddfc06
+                    "\n".join(textwrap.wrap(base64.b64encode(cert).decode('utf8'), 64))) for cert in certchain])
9ddfc06
 
9ddfc06
 def main(argv):
9ddfc06
     parser = argparse.ArgumentParser(
9ddfc06
@@ -188,11 +203,19 @@ def main(argv):
9ddfc06
     parser.add_argument("--acme-dir", required=True, help="path to the .well-known/acme-challenge/ directory")
9ddfc06
     parser.add_argument("--quiet", action="store_const", const=logging.ERROR, help="suppress output except for errors")
9ddfc06
     parser.add_argument("--ca", default=DEFAULT_CA, help="certificate authority, default is Let's Encrypt")
9ddfc06
+    parser.add_argument("--chain", action="store_true", 
9ddfc06
+        help="fetch and append intermediate certs to output")
9ddfc06
 
9ddfc06
     args = parser.parse_args(argv)
9ddfc06
     LOGGER.setLevel(args.quiet or LOGGER.level)
9ddfc06
-    signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca)
9ddfc06
-    sys.stdout.write(signed_crt)
9ddfc06
+    try:
9ddfc06
+        signed_crt = get_crt(args.account_key, args.csr, args.acme_dir,
9ddfc06
+            log=LOGGER, CA=args.ca, chain=args.chain)
9ddfc06
+        sys.stdout.write(signed_crt)
9ddfc06
+    except Exception as e:
9ddfc06
+        #if not args.quiet: raise e
9ddfc06
+        LOGGER.error(e)
9ddfc06
+        sys.exit(1)
9ddfc06
 
9ddfc06
 if __name__ == "__main__": # pragma: no cover
9ddfc06
     main(sys.argv[1:])