Automating Let's Encrypt No Sudo for 9 Domains
Let's Encrypt Nosudo Scripts 0.1 [sig]
Let's Encrypt is a free SSL certificate authority that is designed to let users encrypt their website correctly. This has let me save around $81 creating certificates for all my domains (9 domains with Let's Encrypt, one without). Let's Encrypt was designed for the overly-trusting user who is willing to run code they download off github as root. Experience and paranoia teaches us not to run untrusted code as root or even as a user that isn't fully sandboxed. How do we deal with this? This technical document is for the admin who can read code and find vulnerabilities in Bash, Python, and protocols, not for the faint of heart.
Let's Encrypt Nosudo was designed for that. It takes a few hours to sign 10 certificates, so maybe 30 minutes per cert. But Let's Encrypt only issues certs with duration of 3 months which means that every 2-3 months you have to spend 30 minutes per cert. If you have 9 certs, that's a huge time investment. So like me you want to automate Let's Encrypt so that you don't have to spend 5 hours every 2-3 months. This is what these scripts are for.
The first file is generate-csr.sh which comes from letsencrypt/examples/generate-csr.sh. I have modified it to create 4096-bit keys and output it in PEM instead of DER because letsencrypt-nosudo and most programs support PEM but not DER.
/usr/local/bin/generate-csr.sh:
#!/bin/sh
# This script generates a simple SAN CSR to be used with Let's Encrypt
# CA. Mostly intended for "auth --csr" testing, but, since it's easily
# auditable, feel free to adjust it and use it on your production web
# server.
if [ "$#" -lt 1 ]
then
echo "Usage: $0 domain [domain...]" >&2
exit 1
fi
domains="DNS:$1"
shift
for x in "$@"
do
domains="$domains,DNS:$x"
done
SAN="$domains" openssl req -config "${OPENSSL_CNF:-openssl.cnf}" \
-new -nodes -subj '/' -reqexts san \
-out "${CSR_PATH:-csr.der}" \
-keyout "${KEY_PATH:-key.pem}" \
-newkey rsa:4096 \
-outform PEM
# 512 or 1024 too low for Boulder, 2048 is smallest for tests
echo "You can now run: letsencrypt auth --csr ${CSR_PATH:-csr.der}"
The second file is sign_csr.py from my fork of letsencrypt-nosudo. The main difference between my fork and their fork is the '--registered'
flag. When you first run sign_csr.py
, it registers your user. It doesn't make any sense for you to register after the first time, so that flag saves you a few signatures. It is by far the most suspect of the whole process, but it doesn't need sudo and it doesn't need to be run as a valuable user, so you can run it in a very tight sandbox if you wish. Instead, I vetted it line by line. My signed version e03fa08ccb2b752b003ded62ad38bed54a7497b0
is perfect as I can see. Even a full man-in-the-middle would not benefit an attacker from my brief threat model. Remember that the worst case scenario for encrypting a domain that was not previous encrypted is that you will send data to the server in a way that is as bad as not encrypting it (confidentiality, integrity, availability).
#!/usr/bin/env python
import argparse, subprocess, json, os, urllib2, sys, base64, binascii, time, \
hashlib, tempfile, re, copy, textwrap
def sign_csr(pubkey, csr, email=None, file_based=False, already_registered=False):
"""Use the ACME protocol to get an ssl certificate signed by a
certificate authority.
:param string pubkey: Path to the user account public key.
:param string csr: Path to the certificate signing request.
:param string email: An optional user account contact email
(defaults to webmaster@<shortest_domain>)
:param bool file_based: An optional flag indicating that the
hosting should be file-based rather
than providing a simple python HTTP
server.
:returns: Signed Certificate (PEM format)
:rtype: string
"""
#CA = "https://acme-staging.api.letsencrypt.org"
CA = "https://acme-v01.api.letsencrypt.org"
TERMS = "https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
nonce_req = urllib2.Request("{}/directory".format(CA))
nonce_req.get_method = lambda : 'HEAD'
def _b64(b):
"Shortcut function to go from bytes to jwt base64 string"
return base64.urlsafe_b64encode(b).replace("=", "")
# Step 1: Get account public key
sys.stderr.write("Reading pubkey file...\n")
proc = subprocess.Popen(["openssl", "rsa", "-pubin", "-in", pubkey, "-noout", "-text"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
out, err = proc.communicate()
if proc.returncode != 0:
raise IOError("Error loading {}".format(pubkey))
pub_hex, pub_exp = re.search(
"Modulus(?: \((?:2048|4096) bit\)|)\:\s+00:([a-f0-9\:\s]+?)Exponent\: ([0-9]+)",
out, re.MULTILINE|re.DOTALL).groups()
pub_mod = binascii.unhexlify(re.sub("(\s|:)", "", pub_hex))
pub_mod64 = _b64(pub_mod)
pub_exp = int(pub_exp)
pub_exp = "{0:x}".format(pub_exp)
pub_exp = "0{}".format(pub_exp) if len(pub_exp) % 2 else pub_exp
pub_exp = binascii.unhexlify(pub_exp)
pub_exp64 = _b64(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("Found public key!\n")
# Step 2: Get the domain names to be certified
sys.stderr.write("Reading csr file...\n")
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 {}".format(csr))
domains = set([])
common_name = re.search("Subject:.*? CN=([^\s,;/]+)", out)
if common_name is not None:
domains.add(common_name.group(1))
subject_alt_names = re.search("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("Found domains {}\n".format(", ".join(domains)))
# Step 3: Ask user for contact email
if not email:
default_email = "webmaster@{}".format(min(domains, key=len))
stdout = sys.stdout
sys.stdout = sys.stderr
input_email = raw_input("STEP 1: What is your contact email? ({}) ".format(default_email))
email = input_email if input_email else default_email
sys.stdout = stdout
# Step 4: Generate the payloads that need to be signed
# registration
sys.stderr.write("Building request payloads...\n")
reg_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
reg_raw = json.dumps({
"resource": "new-reg",
"contact": ["mailto:{}".format(email)],
"agreement": TERMS,
}, sort_keys=True, indent=4)
reg_b64 = _b64(reg_raw)
reg_protected = copy.deepcopy(header)
reg_protected.update({"nonce": reg_nonce})
reg_protected64 = _b64(json.dumps(reg_protected, sort_keys=True, indent=4))
if not already_registered:
reg_file = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".json")
reg_file.write("{}.{}".format(reg_protected64, reg_b64))
reg_file.flush()
reg_file_name = os.path.basename(reg_file.name)
reg_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="register_", suffix=".sig")
reg_file_sig_name = os.path.basename(reg_file_sig.name)
#end if
# need signature for each domain identifiers
ids = []
tests = []
for domain in domains:
sys.stderr.write("Building request for {}...\n".format(domain))
id_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
id_raw = json.dumps({
"resource": "new-authz",
"identifier": {
"type": "dns",
"value": domain,
},
}, sort_keys=True)
id_b64 = _b64(id_raw)
id_protected = copy.deepcopy(header)
id_protected.update({"nonce": id_nonce})
id_protected64 = _b64(json.dumps(id_protected, sort_keys=True, indent=4))
id_file = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".json")
id_file.write("{}.{}".format(id_protected64, id_b64))
id_file.flush()
id_file_name = os.path.basename(id_file.name)
id_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="domain_", suffix=".sig")
id_file_sig_name = os.path.basename(id_file_sig.name)
ids.append({
"domain": domain,
"protected64": id_protected64,
"data64": id_b64,
"file": id_file,
"file_name": id_file_name,
"sig": id_file_sig,
"sig_name": id_file_sig_name,
})
# challenge request
#test_path = _b64(os.urandom(16))
test_raw = json.dumps({
"resource": "challenge",
"type": "simpleHttp",
"tls": False,
}, sort_keys=True, indent=4)
test_b64 = _b64(test_raw)
test_protected = copy.deepcopy(header)
test_protected.update({"nonce": urllib2.urlopen(nonce_req).headers['Replay-Nonce']})
test_protected64 = _b64(json.dumps(test_protected, sort_keys=True, indent=4))
test_file = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".json")
test_file.write("{}.{}".format(test_protected64, test_b64))
test_file.flush()
test_file_name = os.path.basename(test_file.name)
test_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".sig")
test_file_sig_name = os.path.basename(test_file_sig.name)
tests.append({
"protected64": test_protected64,
"data64": test_b64,
"file": test_file,
"file_name": test_file_name,
"sig": test_file_sig,
"sig_name": test_file_sig_name,
})
# need signature for the final certificate issuance
sys.stderr.write("Building request for CSR...\n")
proc = subprocess.Popen(["openssl", "req", "-in", csr, "-outform", "DER"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
csr_der, err = proc.communicate()
csr_der64 = _b64(csr_der)
csr_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
csr_raw = json.dumps({
"resource": "new-cert",
"csr": csr_der64,
}, sort_keys=True, indent=4)
csr_b64 = _b64(csr_raw)
csr_protected = copy.deepcopy(header)
csr_protected.update({"nonce": csr_nonce})
csr_protected64 = _b64(json.dumps(csr_protected, sort_keys=True, indent=4))
csr_file = tempfile.NamedTemporaryFile(dir=".", prefix="cert_", suffix=".json")
csr_file.write("{}.{}".format(csr_protected64, csr_b64))
csr_file.flush()
csr_file_name = os.path.basename(csr_file.name)
csr_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="cert_", suffix=".sig")
csr_file_sig_name = os.path.basename(csr_file_sig.name)
# Step 5: Ask the user to sign the registration and requests
regsig = ''
if not already_registered:
regsig = 'openssl dgst -sha256 -sign user.key -out {} {}'.format(reg_file_sig_name, reg_file_name)
#end if
sys.stderr.write("""\
STEP 2: You need to sign some files (replace 'user.key' with your user private key).
{}
{}
openssl dgst -sha256 -sign user.key -out {} {}
""".format(
regsig,
"\n".join("openssl dgst -sha256 -sign user.key -out {} {}".format(i['sig_name'], i['file_name']) for i in ids),
csr_file_sig_name, csr_file_name))
stdout = sys.stdout
sys.stdout = sys.stderr
raw_input("Press Enter when you've run the above commands in a new terminal window...")
sys.stdout = stdout
# Step 6: Load the signatures
if not already_registered:
reg_file_sig.seek(0)
reg_sig64 = _b64(reg_file_sig.read())
#end if
for n, i in enumerate(ids):
i['sig'].seek(0)
i['sig64'] = _b64(i['sig'].read())
# Step 7: Register the user
if not already_registered:
sys.stderr.write("Registering {}...\n".format(email))
reg_data = json.dumps({
"header": header,
"protected": reg_protected64,
"payload": reg_b64,
"signature": reg_sig64,
}, sort_keys=True, indent=4)
reg_url = "{}/acme/new-reg".format(CA)
try:
resp = urllib2.urlopen(reg_url, reg_data)
result = json.loads(resp.read())
except urllib2.HTTPError as e:
err = e.read()
# skip already registered accounts
if "Registration key is already in use" in err:
sys.stderr.write("Already registered. Skipping...\n")
else:
sys.stderr.write("Error: reg_data:\n")
sys.stderr.write("POST {}\n".format(reg_url))
sys.stderr.write(reg_data)
sys.stderr.write("\n")
sys.stderr.write(err)
sys.stderr.write("\n")
raise
#end if
# Step 8: Request challenges for each domain
responses = []
tests = []
for n, i in enumerate(ids):
sys.stderr.write("Requesting challenges for {}...\n".format(i['domain']))
id_data = json.dumps({
"header": header,
"protected": i['protected64'],
"payload": i['data64'],
"signature": i['sig64'],
}, sort_keys=True, indent=4)
id_url = "{}/acme/new-authz".format(CA)
try:
resp = urllib2.urlopen(id_url, id_data)
result = json.loads(resp.read())
except urllib2.HTTPError as e:
sys.stderr.write("Error: id_data:\n")
sys.stderr.write("POST {}\n".format(id_url))
sys.stderr.write(id_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
sys.stderr.write('res: {}\n'.format(result))
challenge = [c for c in result['challenges'] if c['type'] == "http-01"][0]
keyauthorization = "{}.{}".format(challenge['token'], thumbprint)
# challenge request
sys.stderr.write("Building challenge responses for {}...\n".format(i['domain']))
test_nonce = urllib2.urlopen(nonce_req).headers['Replay-Nonce']
test_raw = json.dumps({
"resource": "challenge",
"keyAuthorization": keyauthorization,
}, sort_keys=True, indent=4)
test_b64 = _b64(test_raw)
test_protected = copy.deepcopy(header)
test_protected.update({"nonce": test_nonce})
test_protected64 = _b64(json.dumps(test_protected, sort_keys=True, indent=4))
test_file = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".json")
test_file.write("{}.{}".format(test_protected64, test_b64))
test_file.flush()
test_file_name = os.path.basename(test_file.name)
test_file_sig = tempfile.NamedTemporaryFile(dir=".", prefix="challenge_", suffix=".sig")
test_file_sig_name = os.path.basename(test_file_sig.name)
tests.append({
"uri": challenge['uri'],
"protected64": test_protected64,
"data64": test_b64,
"file": test_file,
"file_name": test_file_name,
"sig": test_file_sig,
"sig_name": test_file_sig_name,
"token": challenge['token'],
})
# challenge response for server
responses.append({
"uri": ".well-known/acme-challenge/{}".format(challenge['token']),
"data": keyauthorization,
})
# Step 9: Ask the user to sign the challenge responses
sys.stderr.write("""\
STEP 3: You need to sign some more files (replace 'user.key' with your user private key).
{}
""".format(
"\n".join("openssl dgst -sha256 -sign user.key -out {} {}".format(
i['sig_name'], i['file_name']) for i in tests)))
stdout = sys.stdout
sys.stdout = sys.stderr
raw_input("Press Enter when you've run the above commands in a new terminal window...")
sys.stdout = stdout
# Step 10: Load the response signatures
for n, i in enumerate(ids):
tests[n]['sig'].seek(0)
tests[n]['sig64'] = _b64(tests[n]['sig'].read())
# Step 11: Ask the user to host the token on their server
for n, i in enumerate(ids):
if file_based:
sys.stderr.write("""\
STEP {}: Please update your server to serve the following file at this URL:
--------------
URL: http://{}/{}
File contents: \"{}\"
--------------
Notes:
- Do not include the quotes in the file.
- The file should be one line without any spaces.
""".format(n + 4, i['domain'], responses[n]['uri'], responses[n]['data']))
stdout = sys.stdout
sys.stdout = sys.stderr
raw_input("Press Enter when you've got the file hosted on your server...")
sys.stdout = stdout
else:
sys.stderr.write("""\
STEP {}: You need to run this command on {} (don't stop the python command until the next step).
sudo python -c "import BaseHTTPServer; \\
h = BaseHTTPServer.BaseHTTPRequestHandler; \\
h.do_GET = lambda r: r.send_response(200) or r.end_headers() or r.wfile.write('{}'); \\
s = BaseHTTPServer.HTTPServer(('0.0.0.0', 80), h); \\
s.serve_forever()"
""".format(n + 4, i['domain'], responses[n]['data']))
stdout = sys.stdout
sys.stdout = sys.stderr
raw_input("Press Enter when you've got the python command running on your server...")
sys.stdout = stdout
# Step 12: Let the CA know you're ready for the challenge
sys.stderr.write("Requesting verification for {}...\n".format(i['domain']))
test_data = json.dumps({
"header": header,
"protected": tests[n]['protected64'],
"payload": tests[n]['data64'],
"signature": tests[n]['sig64'],
}, sort_keys=True, indent=4)
test_url = tests[n]['uri']
try:
resp = urllib2.urlopen(test_url, test_data)
test_result = json.loads(resp.read())
except urllib2.HTTPError as e:
sys.stderr.write("Error: test_data:\n")
sys.stderr.write("POST {}\n".format(test_url))
sys.stderr.write(test_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
# Step 13: Wait for CA to mark test as valid
sys.stderr.write("Waiting for {} challenge to pass...\n".format(i['domain']))
while True:
try:
resp = urllib2.urlopen(test_url)
challenge_status = json.loads(resp.read())
except urllib2.HTTPError as e:
sys.stderr.write("Error: test_data:\n")
sys.stderr.write("GET {}\n".format(test_url))
sys.stderr.write(test_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
if challenge_status['status'] == "pending":
time.sleep(2)
elif challenge_status['status'] == "valid":
sys.stderr.write("Passed {} challenge!\n".format(i['domain']))
break
else:
raise KeyError("'{}' challenge did not pass: {}".format(i['domain'], challenge_status))
# Step 14: Get the certificate signed
sys.stderr.write("Requesting signature...\n")
csr_file_sig.seek(0)
csr_sig64 = _b64(csr_file_sig.read())
csr_data = json.dumps({
"header": header,
"protected": csr_protected64,
"payload": csr_b64,
"signature": csr_sig64,
}, sort_keys=True, indent=4)
csr_url = "{}/acme/new-cert".format(CA)
try:
resp = urllib2.urlopen(csr_url, csr_data)
signed_der = resp.read()
except urllib2.HTTPError as e:
sys.stderr.write("Error: csr_data:\n")
sys.stderr.write("POST {}\n".format(csr_url))
sys.stderr.write(csr_data)
sys.stderr.write("\n")
sys.stderr.write(e.read())
sys.stderr.write("\n")
raise
# Step 15: Convert the signed cert from DER to PEM
sys.stderr.write("Certificate signed!\n")
sys.stderr.write("You can stop running the python command on your server (Ctrl+C works).\n")
signed_der64 = base64.b64encode(signed_der)
signed_pem = """\
-----BEGIN CERTIFICATE-----
{}
-----END CERTIFICATE-----
""".format("\n".join(textwrap.wrap(signed_der64, 64)))
return signed_pem
if __name__ == "__main__":
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
description="""\
Get a SSL certificate signed by a Let's Encrypt (ACME) certificate authority and
output that signed certificate. You do NOT need to run this script on your
server and this script does not ask for your private keys. It will print out
commands that you need to run with your private key or on your server as root,
which gives you a chance to review the commands instead of trusting this script.
NOTE: YOUR ACCOUNT KEY NEEDS TO BE DIFFERENT FROM YOUR DOMAIN KEY.
Prerequisites:
* openssl
* python
Example: Generate an account keypair, a domain key and csr, and have the domain csr signed.
--------------
$ openssl genrsa 4096 > user.key
$ openssl rsa -in user.key -pubout > user.pub
$ openssl genrsa 4096 > domain.key
$ openssl req -new -sha256 -key domain.key -subj "/CN=example.com" > domain.csr
$ python sign_csr.py --public-key user.pub domain.csr > signed.crt
--------------
""")
parser.add_argument("-p", "--public-key", required=True, help="path to your account public key")
parser.add_argument("-e", "--email", default=None, help="contact email, default is webmaster@<shortest_domain>")
parser.add_argument("-f", "--file-based", action='store_true', help="if set, a file-based response is used")
parser.add_argument("-r", "--registered", action='store_true', help="Use this flag if you are already registered to save one signature and one unnecessary request.")
parser.add_argument("csr_path", help="path to your certificate signing request")
args = parser.parse_args()
signed_crt = sign_csr(args.public_key, args.csr_path, email=args.email, file_based=args.file_based, already_registered=args.registered)
sys.stdout.write(signed_crt)
As you can probably see, I took quite a lot of care not to rewrite every line so that as the original author makes improvements my version will be able to merge those. In fact, I merged quite a few changes today before doing my certificate signing.
The next file is openssl.cnf
required by generate-csr.sh
. If you don't have this in the correct directory, you simply won't be able to get generate-csr.sh
to generate a csr + key. That's a nice fail case. It comes from letsencrypt unchanged. If you have an openssl.cnf
that you want to use, you only need to add the last two lines [ san ]
and subjectAltName=${ENV::SAN}
.
[ req ]
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
[ san ]
subjectAltName=${ENV::SAN}
The next file is the first domain I am signing. Sono.us is a url-shortener for AltSci.com that I use kinda like an RSS for my work. It lacks an RSS of course which I plan to fix in the future. You can see what I'm trying to do and where it goes wrong pretty easily. You have to run commands provided to you by the script. One way to do that is to have the script run those commands. Another way is to create another shell and copy/paste those commands. That's what I do. So step 3, 4, and 5 are all done in the separate shell and guided by the script's output. Instead of running this script (which would actually work), I recommend executing the lines you understand one by one. If you don't understand what's going on, you should not be running this code. Under no circumstances, love, money, or war should you be running this without understanding it. Anyway, on to it.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=sono.us_"$(date +%Y-%m-%d)".csr
export KEY_PATH=sono.us_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh sono.us www.sono.us || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# The rest has to be done at the command of the script, so...
exit 1
# Step 3: Sign domain and cert with user key.
openssl dgst -sha256 -sign ../user.key -out domain_iAeMyj.sig domain_vE2zKX.json
openssl dgst -sha256 -sign ../user.key -out domain_e6dMMs.sig domain_pZuSfI.json
openssl dgst -sha256 -sign ../user.key -out cert_lq8U0d.sig cert_sg_noO.json
# Step 4: Sign challenge with user key
openssl dgst -sha256 -sign ../user.key -out challenge_zPzZAb.sig challenge_iSYWZf.json
openssl dgst -sha256 -sign ../user.key -out challenge_WabMS7.sig challenge_F36XCq.json
# Step 5: Proof of ownership
#mkdir -p /home/www/sono.us/.well-known/acme-challenge/
#cat >/home/www/sono.us/.well-known/acme-challenge/F7V2H0SQUTnPXaGDB-78f4m4sCBlwqyAydSLvLVjY-g
#F7V2H0SQUTnPXaGDB-78f4m4sCBlwqyAydSLvLVjY-g.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >/home/www/sono.us/.well-known/acme-challenge/nfHcWhXtnu5gAAn5q1eMLqAs2UzWcZtISKynqAta7VM
#nfHcWhXtnu5gAAn5q1eMLqAs2UzWcZtISKynqAta7VM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
# Step 6: Save output certificate
cat >"${CSR_PATH%.csr}.crt"
# Step 7: Copy cert and key.
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo chown root:root /etc/ssl/private/"$KEY_PATH"
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../sono.us_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../sono.us_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists sono.us_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
# Temporary fix:
sudo mv -i /etc/ssl/private/sono.us.key /etc/ssl/private/sono.us_2015-10-27.key
sudo ln -s sono.us_2015-12-04.key /etc/ssl/private/sono.us.key
# permanent fix:
sudo rm /etc/ssl/private/sono.us.key
sudo ln -s "$KEY_PATH" /etc/ssl/private/sono.us.key
So now you see what I did and how it worked. A bit of funny business with apache had to happen to make this work, but it worked. If you understand that, then you're done. The rest are just the same thing but for the rest of my domains. BikeIM is a very important project I'm currently working on daily but remains unpublished due to not wanting to flood the community with partially finished products that aren't ready to compete. The website has a preview of things to come though, a fairly old IRC/HTTP server+bot completely in Python. It lacks crypto, but the rewrite of BikeIM I'm working on is up to its gills in crypto.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=bikeim.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=bikeim.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh bikeim.com www.bikeim.com || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# mkdir -p /home/www/bikeim/.well-known/acme-challenge
#cat >/home/www/bikeim/.well-known/acme-challenge/6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM
#6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >/home/www/bikeim/.well-known/acme-challenge/t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8
#t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
cat >"${CSR_PATH%.csr}.crt"
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
sudo ln -sf "$KEY_PATH" /etc/ssl/private/bikeim.com.key
sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/bikeim.com.crt
rm -r /home/www/bikeim/.well-known/
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../bikeim.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../bikeim.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists bikeim.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
sudo /etc/init.d/apache2 reload
sudo -k
AltSci Cell is my main blog and where I published my quick note about Let's Encrypt and it's where I write things that aren't fit for AltSci.com. It's one of the most important things I'm encrypting with Let's Encrypt if you can believe it. That's why I sign all my posts with PGP.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=cell-game.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=cell-game.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh cell-game.com www.cell-game.com || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
#cat >~/altsci/cell/.well-known/acme-challenge/6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM
#6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >~/altsci/cell/.well-known/acme-challenge/t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8
#t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
cat >cell-game.com_2015-12-04.crt
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
sudo ln -sf "$KEY_PATH" /etc/ssl/private/cell-game.com.key
sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/cell-game.com.crt
rm -r ~/altsci/cell/.well-known/
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../cell-game.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../cell-game.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists cell-game.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
Hack Mars is a simple website for a game I wrote around ten years ago. It still runs but I decided that I would first finish AltSci Cell. Cell was never finished, so Hack Mars too went unfinished. You can probably guess a trend, but all of these unfinished projects were ambitious and only failed because I didn't spend enough time on just one of them. The result was actually more than I should have hoped for. If you want to see the end result of Hack Mars, Javantea's Fate, and AltSci Cell, let me know.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=hackmars.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=hackmars.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh hackmars.com www.hackmars.com || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# mkdir -p /home/www/hackmars/.well-known/acme-challenge
#cat >/home/www/hackmars/.well-known/acme-challenge/6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM
#6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >/home/www/hackmars/.well-known/acme-challenge/t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8
#t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
cat >"${CSR_PATH%.csr}.crt"
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
sudo ln -sf "$KEY_PATH" /etc/ssl/private/hackmars.com.key
sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/hackmars.com.crt
rm -r /home/www/hackmars/.well-known
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../hackmars.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../hackmars.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists hackmars.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
sudo /etc/init.d/apache2 reload
sudo -k
J4va.com is a simple blog about the failings and benefits of the language Java. Because my name is Javantea a lot of people expect me to like Java, I don't. I was actually Javantea before Java existed, so I think they should be forced to change their name. This domain allows me to own Java in a homographic sort of way. I haven't updated it since Java was blocked by practically every legitimate browser and then Oracle and the browsers decided finally to make it require permission to even run an applet. There have been dozens of 0-days on Java and few have cared. Java still remains worthwhile for Android apps and in certain limited uses on desktop/server systems.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=j4va.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=j4va.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh j4va.com www.j4va.com || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# mkdir -p /home/www/j4va/.well-known/acme-challenge
#cat >/home/www/j4va/.well-known/acme-challenge/6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM
#6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >/home/www/j4va/.well-known/acme-challenge/t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8
#t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
cat >"${CSR_PATH%.csr}.crt"
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
sudo ln -sf "$KEY_PATH" /etc/ssl/private/j4va.com.key
sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/j4va.com.crt
rm -r /home/www/j4va/.well-known/acme-challenge
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../j4va.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../j4va.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists j4va.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
Javantea.com is the second oldest of my domains joined by Javantea.org for the only 4-domain certificate in the bunch. I decided it makes good sense to have all the Javantea.* domains use a single certificate because they are the same website. Even when they aren't, they should still use the same certificate. If I were to split these domains up, keys/certificates can and should be regenerated.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=javantea.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=javantea.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh javantea.com www.javantea.com javantea.org www.javantea.org || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# mkdir -p /home/www/javantea/.well-known/acme-challenge
#cat >/home/www/javantea/.well-known/acme-challenge/6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM
#6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >/home/www/javantea/.well-known/acme-challenge/t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8
#t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
cat >"${CSR_PATH%.csr}.crt"
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
sudo ln -sf "$KEY_PATH" /etc/ssl/private/javantea.com.key
sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/javantea.com.crt
rm -r /home/www/javantea/.well-known/
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../javantea.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../javantea.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists javantea.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
sudo /etc/init.d/apache2 reload
sudo -k
Small Wide World is by far my favorite released software. It's a graph optimization library with lots of interesting examples. You should download it or at least play around with the web interface for a few minutes.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=small-wide-world.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=small-wide-world.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh small-wide-world.com www.small-wide-world.com || exit 2
# Step 2: Sign CSR
/usr/local/bin/sign_csr.py --registered --file-based -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# mkdir -p /home/www/altsci/concepts/smallwideworld/.well-known/acme-challenge
#cat >/home/www/altsci/concepts/smallwideworld/.well-known/acme-challenge/6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM
#6PFRGvqJBkEFIokkS9x1X-GIeTmbVXUPusqY1ZXptvM.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
#cat >/home/www/altsci/concepts/smallwideworld/.well-known/acme-challenge/t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8
#t-S38Kh9oFyv9lvdASgQYIp_aYL_CDFGTvbHhfNXIF8.duIKaKyCkI0IA1Z9KNweXU2uKde7Gq4FU2Q26BB4FYk
cat >"${CSR_PATH%.csr}.crt"
sudo mv -i "$KEY_PATH" /etc/ssl/private/
sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/
sudo ln -sf "$KEY_PATH" /etc/ssl/private/small-wide-world.com.key
sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/acerts/small-wide-world.com.crt
rm -r /home/www/altsci/concepts/smallwideworld/.well-known
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../small-wide-world.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../small-wide-world.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists small-wide-world.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
sudo /etc/init.d/apache2 reload
sudo -k
mail.AltSci.com / Suzy.AltSci.com is my secondary mail server behind a firewall. If you want to see this certificate, you have to do a tiny bit of nmap -sT -p 1- suzy.altsci.com
or nmap -sT -p 1- -6 suzy.altsci.com
to find it. Since port 25 is blocked, I use a different port. Having a valid certificate for your mail server is a good idea even if almost no one ever validates them. Postfix recently added certificate pinning, so I'm going to experiment a little with that. I'm going to reuse the certificate for my web server as well but it's been down for months due to technical difficulty and lack of time. As you can see, I switched off the --file-based
flag and used the Python server because my web server is still down. I could have created a server with sudo python3 -m http.server 80
but for almost no benefit I decided against it.
# I needed to add two lines to openssl.cnf but the rest was fine.
#[ san ]
#subjectAltName=${ENV::SAN}
# Step 1: generate the KEY/CSR
mkdir le_"$(date +%Y-%m-%d)"
pushd "$_" || exit 1
if [ "$(find .)" != "." ]; then
echo "Remove the files from this directory: le_"$(date +%Y-%m-%d)" and run again."
popd
exit 1
fi
export CSR_PATH=mail.altsci.com_"$(date +%Y-%m-%d)".csr
export KEY_PATH=mail.altsci.com_"$(date +%Y-%m-%d)".key
export OPENSSL_CNF=../openssl.cnf
/usr/local/bin/generate-csr.sh mail.altsci.com suzy.altsci.com || exit 2
# Step 2: Sign CSR
# Not using --file-based because suzy has been down for months.
/usr/local/bin/sign_csr.py --registered -e 'jvoss@altsci.com' -p ../user.pub "$CSR_PATH" || exit 3
# scp -P221 *.json altsci.com:letsencrypt/le_2015-12-04/
# sign
# scp -P221 altsci.com:letsencrypt/le_2015-12-04/*.sig .
# scp -P221 challenge*.json altsci.com:letsencrypt/le_2015-12-04/
# sign
# scp -P221 altsci.com:letsencrypt/le_2015-12-04/challenge*.sig .
#
cat >"${CSR_PATH%.csr}.crt"
# sudo mv -i "$KEY_PATH" /etc/ssl/postfix/
# sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/postfix/
# sudo cp -i "${CSR_PATH%.csr}.crt" /etc/ssl/apache2/
# sudo ln -sf "$KEY_PATH" /etc/ssl/private/mail.altsci.com.key
# sudo ln -sf "${CSR_PATH%.csr}.crt" /etc/ssl/apache2/mail.altsci.com.crt
# cat ~/recent/pfm/projects/letsencrypt/lets-encrypt-x1-cross-signed.pem |sudo tee -a /etc/ssl/postfix/"${CSR_PATH%.csr}.crt"
# Step 7: Cleanup and Go back to the parent directory
if ![ -e ../mail.altsci.com_"$(date +%Y-%m-%d)".tgz ]; then
tar cf ../mail.altsci.com_"$(date +%Y-%m-%d)".tgz *
rm "$CSR_PATH" "${CSR_PATH%.csr}.crt"
if [ "$(find .)" != "." ]; then
echo "Files left by one of the programs woops."
ls -la
fi
else
echo "Tar file exists mail.altsci.com_"$(date +%Y-%m-%d)".tgz, not going to overwrite it. Cleanup yourself."
fi
popd
And that's all she wrote. Here's a list of the links:
- Let's Encrypt
- Let's Encrypt Nosudo
- Javantea's fork of Let's Encrypt Nosudo
- /usr/local/bin/generate-csr.sh
- /usr/local/bin/sign_csr.py
- openssl.cnf
- Sono.us
- BikeIM
- AltSci Cell
- Hack Mars
- J4va.com
- Javantea.com
- Small Wide World
- auto_sono1.sh
- auto_bikeim1.sh
- auto_cell-game1.sh
- auto_hackmars1.sh
- auto_j4va1.sh
- auto_javantea1.sh
- auto_sww1.sh
- auto_mail1.sh
-
Leave a Reply
Comments: 0
Leave a reply »