First Commit

This commit is contained in:
Denny Dai 2015-12-06 13:47:49 +08:00
parent d26cc6c025
commit 68c171f1de
6 changed files with 302 additions and 17 deletions

17
.gitattributes vendored
View File

@ -1,17 +0,0 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The 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.

198
acme_tiny.py Normal file
View File

@ -0,0 +1,198 @@
#!/usr/bin/env python
import argparse, subprocess, json, os, os.path, urllib2, sys, base64, binascii, time, \
hashlib, re, copy, textwrap
#CA = "https://acme-staging.api.letsencrypt.org"
CA = "https://acme-v01.api.letsencrypt.org"
def get_crt(account_key, csr, acme_dir):
# helper function base64 encode for jose spec
def _b64(b):
return base64.urlsafe_b64encode(b).replace("=", "")
# parse account key to get public key
sys.stderr.write("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, re.MULTILINE|re.DOTALL).groups()
pub_mod = binascii.unhexlify(re.sub(r"(\s|:)", "", pub_hex))
pub_mod64 = _b64(pub_mod)
pub_exp = "{0:x}".format(int(pub_exp))
pub_exp = "0{0}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
pub_exp64 = _b64(binascii.unhexlify(pub_exp))
header = {
"alg": "RS256",
"jwk": {
"e": pub_exp64,
"kty": "RSA",
"n": pub_mod64,
},
}
accountkey_json = json.dumps(header['jwk'], sort_keys=True, separators=(',', ':'))
thumbprint = _b64(hashlib.sha256(accountkey_json).digest())
sys.stderr.write("parsed!\n")
# helper function make signed requests
def _send_signed_request(url, payload):
nonce = urllib2.urlopen(CA + "/directory").headers['Replay-Nonce']
payload64 = _b64(json.dumps(payload))
protected = copy.deepcopy(header)
protected.update({"nonce": nonce})
protected64 = _b64(json.dumps(protected))
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))
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 = urllib2.urlopen(url, data)
return resp.getcode(), resp.read()
except urllib2.HTTPError as e:
return e.code, e.read()
# find domains
sys.stderr.write("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,;/]+)", out)
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, 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:])
sys.stderr.write("parsed!\n")
# get the certificate domains and expiration
sys.stderr.write("Registering account...")
code, result = _send_signed_request(CA + "/acme/new-reg", {
"resource": "new-reg",
"agreement": "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf",
})
if code == 201:
sys.stderr.write("registered!\n")
elif code == 409:
sys.stderr.write("already registered!\n")
else:
raise ValueError("Error registering: {0} {1}".format(code, result))
# verify each domain
for domain in domains:
sys.stderr.write("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 registering: {0} {1}".format(code, result))
# make the challenge file
challenge = [c for c in json.loads(result)['challenges'] if c['type'] == "http-01"][0]
challenge['token'] = re.sub(r"[^A-Za-z0-9_\-]", "_", challenge['token'])
keyauthorization = "{0}.{1}".format(challenge['token'], thumbprint)
wellknown_path = os.path.join(acme_dir, challenge['token'])
wellknown_file = open(wellknown_path, "w")
wellknown_file.write(keyauthorization)
wellknown_file.close()
# check that the file is in place
wellknown_url = "http://{0}/.well-known/acme-challenge/{1}".format(
domain, challenge['token'])
try:
resp = urllib2.urlopen(wellknown_url)
assert resp.read().strip() == keyauthorization
except (urllib2.HTTPError, urllib2.URLError, 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 = urllib2.urlopen(challenge['uri'])
challenge_status = json.loads(resp.read())
except urllib2.HTTPError as e:
raise ValueError("Error checking challenge: {0} {1}".format(
e.code, json.loads(e.read())))
if challenge_status['status'] == "pending":
time.sleep(2)
elif challenge_status['status'] == "valid":
sys.stderr.write("verified!\n")
os.remove(wellknown_path)
break
else:
raise ValueError("{0} challenge did not pass: {1}".format(
domain, challenge_status))
# get the new certificate
sys.stderr.write("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!
sys.stderr.write("signed!\n")
return """-----BEGIN CERTIFICATE-----\n{0}\n-----END CERTIFICATE-----\n""".format(
"\n".join(textwrap.wrap(base64.b64encode(result), 64)))
if __name__ == "__main__":
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")
args = parser.parse_args()
signed_crt = get_crt(args.account_key, args.csr, args.acme_dir)
sys.stdout.write(signed_crt)

18
cert.sh Normal file
View File

@ -0,0 +1,18 @@
#!/bin/bash
path=$(pwd)
echo -n "Your Domain:"
read domain
docker-compose up -d
cd $path/certs/
openssl genrsa 4096 > $path/certs/$domain.key
openssl req -new -sha256 -key $domain.key -subj "/CN=$domain" > $path/certs/$domain.csr
cd $path/
python $path/acme_tiny.py --account-key account.key --csr $path/certs/$domain.csr --acme-dir $path/www/ > $path/certs/$domain.crt
wget -O - https://letsencrypt.org/certs/lets-encrypt-x1-cross-signed.pem > $path/certs/letsencrypt-intermediate.pem
cat $path/certs/$domain.crt $path/certs/letsencrypt-intermediate.pem > $path/certs/$domain.pem
rm $path/certs/letsencrypt-intermediate.pem
rm $path/certs/$domain.crt
cat $path/certs/$domain.pem > $path/certs/$domain.crt
rm $path/certs/$domain.pem
rm $path/certs/$domain.csr
docker-compose stop

9
docker-compose.yml Normal file
View File

@ -0,0 +1,9 @@
nginx:
image: nginx
ports:
- "80:80"
- "443:443"
volumes:
- ./www:/usr/share/nginx/html
- ./nginx.conf:/nginx.conf
command: nginx -c /nginx.conf

56
nginx.conf Normal file
View File

@ -0,0 +1,56 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log crit;
pid /var/run/nginx.pid;
events {
worker_connections 51200;
multi_accept on;
use epoll;
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
server_tokens off;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 15;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log off;
error_log off;
gzip on;
gzip_disable "msie6";
open_file_cache max=100;
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html/;
index index.html index.htm index.php;
}
location /.well-known/acme-challenge/ {
alias /usr/share/nginx/html/;
try_files $uri =404;
error_log /var/log/nginx/php_error.log;
access_log /var/log/nginx/php_access.log;
}
}
}
daemon off;