pax_global_header00006660000000000000000000000064132027504140014510gustar00rootroot0000000000000052 comment=4ed13950c0a9cf61f1ca81ff1874cde1cf48ab32 acme-tiny-master/000077500000000000000000000000001320275041400142335ustar00rootroot00000000000000acme-tiny-master/.gitignore000066400000000000000000000012761320275041400162310ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ acme-tiny-master/.travis.yml000066400000000000000000000005451320275041400163500ustar00rootroot00000000000000sudo: required dist: trusty language: python python: - "2.7" - "3.3" - "3.4" - "3.5" - "3.6" - "nightly" before_install: - sudo apt-get install fuse - sudo chmod a+r /etc/fuse.conf install: - pip install -r tests/requirements.txt script: - coverage run --source ./ --omit ./tests/server.py -m unittest tests after_success: - coveralls acme-tiny-master/LICENSE000066400000000000000000000020721320275041400152410ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2015 Daniel Roesler Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. acme-tiny-master/README.md000066400000000000000000000175511320275041400155230ustar00rootroot00000000000000# acme-tiny [![Build Status](https://travis-ci.org/diafygi/acme-tiny.svg)](https://travis-ci.org/diafygi/acme-tiny) [![Coverage Status](https://coveralls.io/repos/diafygi/acme-tiny/badge.svg?branch=master&service=github)](https://coveralls.io/github/diafygi/acme-tiny?branch=master) This is a tiny, auditable script that you can throw on your server to issue and renew [Let's Encrypt](https://letsencrypt.org/) certificates. Since it has to be run on your server and have access to your private Let's Encrypt account key, I tried to make it as tiny as possible (currently less than 200 lines). The only prerequisites are python and openssl. **PLEASE READ THE SOURCE CODE! YOU MUST TRUST IT WITH YOUR PRIVATE KEYS!** ##Donate If this script is useful to you, please donate to the EFF. I don't work there, but they do fantastic work. [https://eff.org/donate/](https://eff.org/donate/) ## How to use this script If you already have a Let's Encrypt issued certificate and just want to renew, you should only have to do Steps 3 and 6. ### Step 1: Create a Let's Encrypt account private key (if you haven't already) You must have a public key registered with Let's Encrypt and sign your requests with the corresponding private key. If you don't understand what I just said, this script likely isn't for you! Please use the official Let's Encrypt [client](https://github.com/letsencrypt/letsencrypt). To accomplish this you need to initially create a key, that can be used by acme-tiny, to register a account for you and sign all following requests. ``` openssl genrsa 4096 > account.key ``` #### Use existing Let's Encrypt key Alternatively you can convert your key, previously generated by the original Let's Encrypt client. The private account key from the Let's Encrypt client is saved in the [JWK](https://tools.ietf.org/html/rfc7517) format. `acme-tiny` is using the PEM key format. To convert the key, you can use the tool [conversion script](https://gist.github.com/JonLundy/f25c99ee0770e19dc595) by JonLundy: ```sh # Download the script wget -O - "https://gist.githubusercontent.com/JonLundy/f25c99ee0770e19dc595/raw/6035c1c8938fae85810de6aad1ecf6e2db663e26/conv.py" > conv.py # Copy your private key to your working directory cp /etc/letsencrypt/accounts/acme-v01.api.letsencrypt.org/directory//private_key.json private_key.json # Create a DER encoded private key openssl asn1parse -noout -out private_key.der -genconf <(python conv.py private_key.json) # Convert to PEM openssl rsa -in private_key.der -inform der > account.key ``` ### Step 2: Create a certificate signing request (CSR) for your domains. The ACME protocol (what Let's Encrypt uses) requires a CSR file to be submitted to it, even for renewals. You can use the same CSR for multiple renewals. NOTE: you can't use your account private key as your domain private key! ``` #generate a domain private key (if you haven't already) openssl genrsa 4096 > domain.key ``` ``` #for a single domain openssl req -new -sha256 -key domain.key -subj "/CN=yoursite.com" > domain.csr #for multiple domains (use this one if you want both www.yoursite.com and yoursite.com) openssl req -new -sha256 -key domain.key -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf <(printf "[SAN]\nsubjectAltName=DNS:yoursite.com,DNS:www.yoursite.com")) > domain.csr ``` ### Step 3: Make your website host challenge files You must prove you own the domains you want a certificate for, so Let's Encrypt requires you host some files on them. This script will generate and write those files in the folder you specify, so all you need to do is make sure that this folder is served under the ".well-known/acme-challenge/" url path. NOTE: Let's Encrypt will perform a plain HTTP request to port 80 on your server, so you must serve the challenge files via HTTP (a redirect to HTTPS is fine too). ``` #make some challenge folder (modify to suit your needs) mkdir -p /var/www/challenges/ ``` ```nginx #example for nginx server { listen 80; server_name yoursite.com www.yoursite.com; location /.well-known/acme-challenge/ { alias /var/www/challenges/; try_files $uri =404; } ...the rest of your config } ``` ### Step 4: Get a signed certificate! Now that you have setup your server and generated all the needed files, run this script on your server with the permissions needed to write to the above folder and read your private account key and CSR. ``` #run the script on your server python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /var/www/challenges/ > ./signed.crt ``` ### Step 5: Install the certificate The signed https certificate that is output by this script can be used along with your private key to run an https server. You need to include them in the https settings in your web server's configuration. Here's an example on how to configure an nginx server: ``` #NOTE: For nginx, you need to append the Let's Encrypt intermediate cert to your cert wget -O - https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem > intermediate.pem cat signed.crt intermediate.pem > chained.pem ``` ```nginx server { listen 443; server_name yoursite.com, www.yoursite.com; ssl on; ssl_certificate /path/to/chained.pem; ssl_certificate_key /path/to/domain.key; ssl_session_timeout 5m; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA; ssl_session_cache shared:SSL:50m; ssl_dhparam /path/to/server.dhparam; ssl_prefer_server_ciphers on; ...the rest of your config } server { listen 80; server_name yoursite.com, www.yoursite.com; location /.well-known/acme-challenge/ { alias /var/www/challenges/; try_files $uri =404; } ...the rest of your config } ``` ### Step 6: Setup an auto-renew cronjob Congrats! Your website is now using https! Unfortunately, Let's Encrypt certificates only last for 90 days, so you need to renew them often. No worries! It's automated! Just make a bash script and add it to your crontab (see below for example script). Example of a `renew_cert.sh`: ```sh #!/usr/bin/sh python /path/to/acme_tiny.py --account-key /path/to/account.key --csr /path/to/domain.csr --acme-dir /var/www/challenges/ > /tmp/signed.crt || exit wget -O - https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem > intermediate.pem cat /tmp/signed.crt intermediate.pem > /path/to/chained.pem service nginx reload ``` ``` #example line in your crontab (runs once per month) 0 0 1 * * /path/to/renew_cert.sh 2>> /var/log/acme_tiny.log ``` ## Permissions The biggest problem you'll likely come across while setting up and running this script is permissions. You want to limit access to your account private key and challenge web folder as much as possible. I'd recommend creating a user specifically for handling this script, the account private key, and the challenge folder. Then add the ability for that user to write to your installed certificate file (e.g. `/path/to/chained.pem`) and reload your webserver. That way, the cron script will do its thing, overwrite your old certificate, and reload your webserver without having permission to do anything else. **BE SURE TO:** * Backup your account private key (e.g. `account.key`) * Don't allow this script to be able to read your domain private key! * Don't allow this script to be run as root! ## Feedback/Contributing This project has a very, very limited scope and codebase. I'm happy to receive bug reports and pull requests, but please don't add any new features. This script must stay under 200 lines of code to ensure it can be easily audited by anyone who wants to run it. If you want to add features for your own setup to make things easier for you, please do! It's open source, so feel free to fork it and modify as necessary. acme-tiny-master/acme_tiny.py000066400000000000000000000217331320275041400165630ustar00rootroot00000000000000#!/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?=\s?([^\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": json.loads(urlopen(CA + "/directory").read().decode('utf8'))['meta']['terms-of-service'], }) 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:]) acme-tiny-master/tests/000077500000000000000000000000001320275041400153755ustar00rootroot00000000000000acme-tiny-master/tests/README.md000066400000000000000000000040371320275041400166600ustar00rootroot00000000000000# How to test acme-tiny Testing acme-tiny requires a bit of setup since it interacts with other servers (Let's Encrypt's staging server) to test issuing fake certificates. This readme explains how to setup and test acme-tiny yourself. ## Setup instructions 1. Make a test subdomain for a server you control. Set it as an environmental variable on your local test setup. * On your local: `export TRAVIS_DOMAIN=travis-ci.gethttpsforfree.com` 2. Generate a shared secret between your local test setup and your server. * `openssl rand -base64 32` * On your local: `export TRAVIS_SESSION=""` 3. Copy and run the test suite mini-server on your server: * `scp server.py ubuntu@travis-ci.gethttpsforfree.com` * `ssh ubuntu@travis-ci.gethttpsforfree.com` * `export TRAVIS_SESSION=""` * `sudo server.py` 4. Install the test requirements on your local (FUSE and optionally coveralls). * `sudo apt-get install fuse` * `virtualenv /tmp/venv` * `source /tmp/venv/bin/activate` * `pip install -r requirements.txt` 5. Run the test suit on your local. * `cd /path/to/acme-tiny` * `coverage run --source ./ --omit ./tests/server.py -m unittest tests` ## Why use FUSE? Acme-tiny writes the challenge files for certificate issuance. In order to do full integration tests, we actually need to serve correct challenge files to the Let's Encrypt staging server on a real domain that they can verify. However, Travis-CI doesn't have domains associated with their test VMs, so we need to send the files to the remote server that does have a real domain. The test suite uses FUSE to do this. It creates a FUSE folder that simulates being a real folder to acme-tiny. When acme-tiny writes the challenge files in the mock folder, FUSE POSTs those files to the real server (which is running the included server.py), and the server starts serving them. That way, both acme-tiny and Let's Encrypt staging can verify and issue the test certificate. This technique allows for high test coverage on automated test runners (e.g. Travis-CI). acme-tiny-master/tests/__init__.py000066400000000000000000000000441320275041400175040ustar00rootroot00000000000000from .test_module import TestModule acme-tiny-master/tests/monkey.py000066400000000000000000000062131320275041400172530ustar00rootroot00000000000000import os, sys from tempfile import NamedTemporaryFile from subprocess import Popen from fuse import FUSE, Operations, LoggingMixIn try: from urllib.request import urlopen # Python 3 except ImportError: from urllib2 import urlopen # Python 2 # domain with server.py running on it for testing DOMAIN = os.getenv("TRAVIS_DOMAIN", "travis-ci.gethttpsforfree.com") # generate account and domain keys def gen_keys(): # good account key account_key = NamedTemporaryFile() Popen(["openssl", "genrsa", "-out", account_key.name, "2048"]).wait() # weak 1024 bit key weak_key = NamedTemporaryFile() Popen(["openssl", "genrsa", "-out", weak_key.name, "1024"]).wait() # good domain key domain_key = NamedTemporaryFile() domain_csr = NamedTemporaryFile() Popen(["openssl", "req", "-newkey", "rsa:2048", "-nodes", "-keyout", domain_key.name, "-subj", "/CN={0}".format(DOMAIN), "-out", domain_csr.name]).wait() # subject alt-name domain san_csr = NamedTemporaryFile() san_conf = NamedTemporaryFile() san_conf.write(open("/etc/ssl/openssl.cnf").read().encode("utf8")) san_conf.write("\n[SAN]\nsubjectAltName=DNS:{0}\n".format(DOMAIN).encode("utf8")) san_conf.seek(0) Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key.name, "-subj", "/", "-reqexts", "SAN", "-config", san_conf.name, "-out", san_csr.name]).wait() # invalid domain csr invalid_csr = NamedTemporaryFile() Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key.name, "-subj", "/CN=\xC3\xA0\xC2\xB2\xC2\xA0_\xC3\xA0\xC2\xB2\xC2\xA0.com", "-out", invalid_csr.name]).wait() # nonexistent domain csr nonexistent_csr = NamedTemporaryFile() Popen(["openssl", "req", "-new", "-sha256", "-key", domain_key.name, "-subj", "/CN=404.gethttpsforfree.com", "-out", nonexistent_csr.name]).wait() # account-signed domain csr account_csr = NamedTemporaryFile() Popen(["openssl", "req", "-new", "-sha256", "-key", account_key.name, "-subj", "/CN={0}".format(DOMAIN), "-out", account_csr.name]).wait() return { "account_key": account_key, "weak_key": weak_key, "domain_key": domain_key, "domain_csr": domain_csr, "san_csr": san_csr, "invalid_csr": invalid_csr, "nonexistent_csr": nonexistent_csr, "account_csr": account_csr, } # fake a folder structure to catch the key authorization file FS = {} class Passthrough(LoggingMixIn, Operations): # pragma: no cover def getattr(self, path, fh=None): f = FS.get(path, None) if f is None: return super(Passthrough, self).getattr(path, fh=fh) return f def write(self, path, buf, offset, fh): urlopen("http://{0}/.well-known/acme-challenge/?{1}".format(DOMAIN, os.getenv("TRAVIS_SESSION", "not_set")), buf) return len(buf) def create(self, path, mode, fi=None): FS[path] = {"st_mode": 33204} return 0 def unlink(self, path): del(FS[path]) return 0 if __name__ == "__main__": # pragma: no cover FUSE(Passthrough(), sys.argv[1], nothreads=True, foreground=True) acme-tiny-master/tests/requirements.txt000066400000000000000000000000321320275041400206540ustar00rootroot00000000000000coveralls fusepy argparse acme-tiny-master/tests/server.py000066400000000000000000000021521320275041400172550ustar00rootroot00000000000000import os, re, hmac from wsgiref.simple_server import make_server KEY_AUTHORIZATION = {"uri": "/not_set", "data": ""} TRAVIS_SESSION = os.getenv("TRAVIS_SESSION", "not_yet_set") def app(req, resp): if req['REQUEST_METHOD'] == "POST": if hmac.compare_digest(req['QUERY_STRING'], TRAVIS_SESSION): body_len = min(int(req['CONTENT_LENGTH']), 90) body = req['wsgi.input'].read(body_len).decode("utf8") body = re.sub(r"[^A-Za-z0-9_\-\.]", "_", body) KEY_AUTHORIZATION['uri'] = "/{0}".format(body.split(".", 1)[0]) KEY_AUTHORIZATION['data'] = body resp('201 Created', []) return ["".encode("utf8")] else: resp("403 Forbidden", []) return ["403 Forbidden".encode("utf8")] else: if hmac.compare_digest(req['PATH_INFO'], KEY_AUTHORIZATION['uri']): resp('200 OK', []) return [KEY_AUTHORIZATION['data'].encode("utf8")] else: resp("404 Not Found", []) return ["404 Not Found".encode("utf8")] make_server("localhost", 8888, app).serve_forever() acme-tiny-master/tests/test_module.py000066400000000000000000000131421320275041400202740ustar00rootroot00000000000000import unittest, os, sys, tempfile from subprocess import Popen, PIPE try: from StringIO import StringIO # Python 2 except ImportError: from io import StringIO # Python 3 import acme_tiny from .monkey import gen_keys KEYS = gen_keys() class TestModule(unittest.TestCase): "Tests for acme_tiny.get_crt()" def setUp(self): self.CA = "https://acme-staging.api.letsencrypt.org" self.tempdir = tempfile.mkdtemp() self.fuse_proc = Popen(["python", "tests/monkey.py", self.tempdir]) def tearDown(self): self.fuse_proc.terminate() self.fuse_proc.wait() os.rmdir(self.tempdir) def test_success_cn(self): """ Successfully issue a certificate via common name """ old_stdout = sys.stdout sys.stdout = StringIO() result = acme_tiny.main([ "--account-key", KEYS['account_key'].name, "--csr", KEYS['domain_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) sys.stdout.seek(0) crt = sys.stdout.read().encode("utf8") sys.stdout = old_stdout out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt) self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8")) def test_success_san(self): """ Successfully issue a certificate via subject alt name """ old_stdout = sys.stdout sys.stdout = StringIO() result = acme_tiny.main([ "--account-key", KEYS['account_key'].name, "--csr", KEYS['san_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) sys.stdout.seek(0) crt = sys.stdout.read().encode("utf8") sys.stdout = old_stdout out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt) self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8")) def test_success_cli(self): """ Successfully issue a certificate via command line interface """ crt, err = Popen([ "python", "acme_tiny.py", "--account-key", KEYS['account_key'].name, "--csr", KEYS['domain_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ], stdout=PIPE, stderr=PIPE).communicate() out, err = Popen(["openssl", "x509", "-text", "-noout"], stdin=PIPE, stdout=PIPE, stderr=PIPE).communicate(crt) self.assertIn("Issuer: CN=Fake LE Intermediate", out.decode("utf8")) def test_missing_account_key(self): """ OpenSSL throws an error when the account key is missing """ try: result = acme_tiny.main([ "--account-key", "/foo/bar", "--csr", KEYS['domain_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) except Exception as e: result = e self.assertIsInstance(result, IOError) self.assertIn("Error opening Private Key", result.args[0]) def test_missing_csr(self): """ OpenSSL throws an error when the CSR is missing """ try: result = acme_tiny.main([ "--account-key", KEYS['account_key'].name, "--csr", "/foo/bar", "--acme-dir", self.tempdir, "--ca", self.CA, ]) except Exception as e: result = e self.assertIsInstance(result, IOError) self.assertIn("Error loading /foo/bar", result.args[0]) def test_weak_key(self): """ Let's Encrypt rejects weak keys """ try: result = acme_tiny.main([ "--account-key", KEYS['weak_key'].name, "--csr", KEYS['domain_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) except Exception as e: result = e self.assertIsInstance(result, ValueError) self.assertIn("key too small", result.args[0]) def test_invalid_domain(self): """ Let's Encrypt rejects invalid domains """ try: result = acme_tiny.main([ "--account-key", KEYS['account_key'].name, "--csr", KEYS['invalid_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) except Exception as e: result = e self.assertIsInstance(result, ValueError) self.assertIn("Invalid character in DNS name", result.args[0]) def test_nonexistant_domain(self): """ Should be unable verify a nonexistent domain """ try: result = acme_tiny.main([ "--account-key", KEYS['account_key'].name, "--csr", KEYS['nonexistent_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) except Exception as e: result = e self.assertIsInstance(result, ValueError) self.assertIn("but couldn't download", result.args[0]) def test_account_key_domain(self): """ Can't use the account key for the CSR """ try: result = acme_tiny.main([ "--account-key", KEYS['account_key'].name, "--csr", KEYS['account_csr'].name, "--acme-dir", self.tempdir, "--ca", self.CA, ]) except Exception as e: result = e self.assertIsInstance(result, ValueError) self.assertIn("certificate public key must be different than account key", result.args[0])