new tool to purchase and install a SSL certificate using Gandi.net's API

This commit is contained in:
Joshua Tauberer 2014-06-23 10:53:09 +00:00
parent 30c416ff6e
commit d4ce50de86
5 changed files with 209 additions and 24 deletions

155
management/buy_certificate.py Executable file
View File

@ -0,0 +1,155 @@
#!/usr/bin/python3
# Helps you purchase a SSL certificate from Gandi.net using
# their API.
#
# Before you begin:
# 1) Create an account on Gandi.net.
# 2) Pre-pay $16 into your account at https://www.gandi.net/prepaid/operations. Wait until the payment goes through.
# 3) Activate your API key first on the test platform (wait a while, refresh the page) and then activate the production API at https://www.gandi.net/admin/api_key.
import sys, re, os.path, urllib.request
import xmlrpc.client
import rtyaml
from utils import load_environment, shell
from web_update import get_web_domains, get_domain_ssl_files, get_web_root
from whats_next import check_certificate
def buy_ssl_certificate(api_key, domain, command, env):
if domain != env['PUBLIC_HOSTNAME'] \
and domain not in get_web_domains(env):
raise ValueError("Domain is not %s or a domain we're serving a website for." % env['PUBLIC_HOSTNAME'])
# Initialize.
gandi = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/')
try:
existing_certs = gandi.cert.list(api_key)
except Exception as e:
if "Invalid API key" in str(e):
print("Invalid API key. Check that you copied the API Key correctly from https://www.gandi.net/admin/api_key.")
sys.exit(1)
else:
raise
# Where is the SSL cert stored?
ssl_key, ssl_certificate, ssl_csr_path = get_domain_ssl_files(domain, env)
# Have we already created a cert for this domain?
for cert in existing_certs:
if cert['cn'] == domain:
break
else:
# No existing cert found. Purchase one.
if command != 'purchase':
print("No certificate or order found yet. If you haven't yet purchased a certificate, run ths script again with the 'purchase' command. Otherwise wait a moment and try again.")
sys.exit(1)
else:
# Start an order for a single standard SSL certificate.
# Use DNS validation. Web-based validation won't work because they
# require a file on HTTP but not HTTPS w/o redirects and we don't
# serve anything plainly over HTTP. Email might be another way but
# DNS is easier to automate.
op = gandi.cert.create(api_key, {
"csr": open(ssl_csr_path).read(),
"dcv_method": "dns",
"duration": 1, # year?
"package": "cert_std_1_0_0",
})
print("An SSL certificate has been ordered.")
print()
print(op)
print()
print("In a moment please run this script again with the 'setup' command.")
if cert['status'] == 'pending':
# Get the information we need to update our DNS with a code so that
# Gandi can verify that we own the domain.
dcv = gandi.cert.get_dcv_params(api_key, {
"csr": open(ssl_csr_path).read(),
"cert_id": cert['id'],
"dcv_method": "dns",
"duration": 1, # year?
"package": "cert_std_1_0_0",
})
if dcv["dcv_method"] != "dns":
raise Exception("Certificate ordered with an unknown validation method.")
# Update our DNS data.
dns_config = env['STORAGE_ROOT'] + '/dns/custom.yaml'
if os.path.exists(dns_config):
dns_records = rtyaml.load(open(dns_config))
else:
dns_records = { }
qname = dcv['md5'] + '.' + domain
value = dcv['sha1'] + '.comodoca.com.'
dns_records[qname] = { "CNAME": value }
with open(dns_config, 'w') as f:
f.write(rtyaml.dump(dns_records))
shell('check_call', ['tools/dns_update'])
# Okay, done with this step.
print("DNS has been updated. Gandi will check within 60 minutes.")
print()
print("See https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
elif cert['status'] == 'valid':
# The certificate is ready.
# Check before we overwrite something we shouldn't.
if os.path.exists(ssl_certificate):
cert_status = check_certificate(ssl_certificate)
if cert_status != "SELF-SIGNED":
print("Please back up and delete the file %s so I can save your new certificate." % ssl_certificate)
sys.exit(1)
# Form the certificate.
# The certificate comes as a long base64-encoded string. Break in
# into lines in the usual way.
pem = "-----BEGIN CERTIFICATE-----\n"
pem += "\n".join(chunk for chunk in re.split(r"(.{64})", cert['cert']) if chunk != "")
pem += "\n-----END CERTIFICATE-----\n\n"
# Append intermediary certificates.
pem += urllib.request.urlopen("https://www.gandi.net/static/CAs/GandiStandardSSLCA.pem").read().decode("ascii")
# Write out.
with open(ssl_certificate, "w") as f:
f.write(pem)
print("The certificate has been installed in %s. Restarting services..." % ssl_certificate)
# Restart dovecot and if this is for PUBLIC_HOSTNAME.
if domain == env['PUBLIC_HOSTNAME']:
shell('check_call', ["/usr/sbin/service", "dovecot", "restart"])
shell('check_call', ["/usr/sbin/service", "postfix", "restart"])
# Restart nginx in all cases.
shell('check_call', ["/usr/sbin/service", "nginx", "restart"])
else:
print("The certificate has an unknown status. Please check https://www.gandi.net/admin/ssl/%d/details for the status of this order." % cert['id'])
if __name__ == "__main__":
if len(sys.argv) < 4:
print("Usage: python management/buy_certificate.py gandi_api_key domain_name {purchase, setup}")
sys.exit(1)
api_key = sys.argv[1]
domain_name = sys.argv[2]
cmd = sys.argv[3]
buy_ssl_certificate(api_key, domain_name, cmd, load_environment())

View File

@ -43,11 +43,9 @@ def do_web_update(env):
def make_domain_config(domain, template, env): def make_domain_config(domain, template, env):
# How will we configure this domain. # How will we configure this domain.
# Where will its root directory be for static files? Try STORAGE_ROOT/web/domain_name # Where will its root directory be for static files?
# if it exists, but fall back to STORAGE_ROOT/web/default.
for test_domain in (domain, 'default'): root = get_web_root(domain, env)
root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain))
if os.path.exists(root): break
# What private key and SSL certificate will we use for this domain? # What private key and SSL certificate will we use for this domain?
ssl_key, ssl_certificate, csr_path = get_domain_ssl_files(domain, env) ssl_key, ssl_certificate, csr_path = get_domain_ssl_files(domain, env)
@ -64,6 +62,13 @@ def make_domain_config(domain, template, env):
nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate) nginx_conf = nginx_conf.replace("$SSL_CERTIFICATE", ssl_certificate)
return nginx_conf return nginx_conf
def get_web_root(domain, env):
# Try STORAGE_ROOT/web/domain_name if it exists, but fall back to STORAGE_ROOT/web/default.
for test_domain in (domain, 'default'):
root = os.path.join(env["STORAGE_ROOT"], "www", safe_domain_name(test_domain))
if os.path.exists(root): break
return root
def get_domain_ssl_files(domain, env): def get_domain_ssl_files(domain, env):
# What SSL private key will we use? Allow the user to override this, but # What SSL private key will we use? Allow the user to override this, but
# in many cases using the same private key for all domains would be fine. # in many cases using the same private key for all domains would be fine.

View File

@ -4,6 +4,8 @@
# SSL certificates have been signed, etc., and if not tells the user # SSL certificates have been signed, etc., and if not tells the user
# what to do next. # what to do next.
__ALL__ = ['check_certificate']
import os, os.path, re, subprocess import os, os.path, re, subprocess
import dns.reversename, dns.resolver import dns.reversename, dns.resolver
@ -175,23 +177,11 @@ def check_ssl_cert(domain, env):
print_error("The SSL certificate file for this domain is missing.") print_error("The SSL certificate file for this domain is missing.")
return return
# Check that the certificate is good. In order to verify with openssl, we need to split out any # Check that the certificate is good.
# intermediary certificates in the chain (if any) from our certificate (at the top).
cert = open(ssl_certificate).read() cert_status = check_certificate(ssl_certificate)
mycert, chaincerts = re.match(r'(-*BEGIN CERTIFICATE-*.*?-*END CERTIFICATE-*)(.*)', cert, re.S).groups()
# This command returns a non-zero exit status in most cases, so trap errors. if cert_status == "SELF-SIGNED":
retcode, verifyoutput = shell('check_output', [
"openssl",
"verify", "-verbose",
"-purpose", "sslserver", "-policy_check",]
+ ([] if chaincerts.strip() == "" else ["-untrusted", "/dev/stdin"])
+ [ssl_certificate],
input=chaincerts.encode('ascii'),
trap=True)
if "self signed" in verifyoutput:
fingerprint = shell('check_output', [ fingerprint = shell('check_output', [
"openssl", "openssl",
"x509", "x509",
@ -216,15 +206,48 @@ def check_ssl_cert(domain, env):
If you receive intermediate certificates, use a text editor and paste your certificate on top and then the intermediate certificates If you receive intermediate certificates, use a text editor and paste your certificate on top and then the intermediate certificates
below it. Save the file and place it onto this machine at %s.""" % ssl_certificate) below it. Save the file and place it onto this machine at %s.""" % ssl_certificate)
elif cert_status == "OK":
elif retcode == 0:
print_ok("SSL certificate is signed.") print_ok("SSL certificate is signed.")
else: else:
print_error("The SSL certificate has a problem:") print_error("The SSL certificate has a problem:")
print("") print("")
print(verifyoutput.strip()) print(cert_status)
print("") print("")
def check_certificate(ssl_certificate):
# Use openssl verify to check the status of a certificate.
# In order to verify with openssl, we need to split out any
# intermediary certificates in the chain (if any) from our
# certificate (at the top). They need to be passed separately.
cert = open(ssl_certificate).read()
m = re.match(r'(-*BEGIN CERTIFICATE-*.*?-*END CERTIFICATE-*)(.*)', cert, re.S)
if m == None:
return "The certificate file is an invalid PEM certificate."
mycert, chaincerts = m.groups()
# This command returns a non-zero exit status in most cases, so trap errors.
retcode, verifyoutput = shell('check_output', [
"openssl",
"verify", "-verbose",
"-purpose", "sslserver", "-policy_check",]
+ ([] if chaincerts.strip() == "" else ["-untrusted", "/dev/stdin"])
+ [ssl_certificate],
input=chaincerts.encode('ascii'),
trap=True)
if "self signed" in verifyoutput:
# Certificate is self-signed.
return "SELF-SIGNED"
elif retcode == 0:
# Certificate is OK.
return "OK"
else:
return verifyoutput.strip()
def print_ok(message): def print_ok(message):
print_block(message, first_line="") print_block(message, first_line="")

View File

@ -22,8 +22,8 @@ if [ -d $STORAGE_ROOT/www/static ]; then mv $STORAGE_ROOT/www/static $STORAGE_RO
mkdir -p $STORAGE_ROOT/www/default mkdir -p $STORAGE_ROOT/www/default
if [ ! -f STORAGE_ROOT/www/default/index.html ]; then if [ ! -f STORAGE_ROOT/www/default/index.html ]; then
cp conf/www_default.html $STORAGE_ROOT/www/default/index.html cp conf/www_default.html $STORAGE_ROOT/www/default/index.html
chown -R $STORAGE_USER $STORAGE_ROOT/www/default/index.html
fi fi
chown -R $STORAGE_USER $STORAGE_ROOT/www
# Create an init script to start the PHP FastCGI daemon and keep it # Create an init script to start the PHP FastCGI daemon and keep it
# running after a reboot. Allows us to serve Roundcube for webmail. # running after a reboot. Allows us to serve Roundcube for webmail.

2
tools/dns_update Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
curl -s -d POSTDATA --user $(</var/lib/mailinabox/api.key): http://127.0.0.1:10222/dns/update