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