-
Notifications
You must be signed in to change notification settings - Fork 572
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4 Also send contact at register time if available (some CAs mandate this)
- Loading branch information
1 parent
0a9afb2
commit e0df39f
Showing
2 changed files
with
44 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -122,6 +122,21 @@ and read your private account key and CSR. | |
python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /var/www/challenges/ > ./signed_chain.crt | ||
``` | ||
|
||
If your ACME CA mandates externalAccountBinding (eAB), provide those parameters like so: | ||
|
||
``` | ||
# Run the script on your server | ||
python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /var/www/challenges/ --eabkid 'PAtzxcSFQMQSdm9SLJTxCt0hwvvl5yNKPfnWBWqPk8o' --eabhmackey 'ZndUSkZvVldvMEFiRzQ5VWNCdERtNkNBNnBTcTl4czNKVEVxdUZiaEdpZXZNUVJBVmRuSFREcDJYX2s3X0NxTA' > ./signed_chain.crt | ||
``` | ||
|
||
Some ACME CA mandate a contact at registration: | ||
|
||
``` | ||
# Run the script on your server | ||
python acme_tiny.py --account-key ./account.key --csr ./domain.csr --acme-dir /var/www/challenges/ --contact [email protected] --eabkid 'PAtzxcSFQMQSdm9SLJTxCt0hwvvl5yNKPfnWBWqPk8o' --eabhmackey 'ZndUSkZvVldvMEFiRzQ5VWNCdERtNkNBNnBTcTl4czNKVEVxdUZiaEdpZXZNUVJBVmRuSFREcDJYX2s3X0NxTA' > ./signed_chain.crt | ||
``` | ||
|
||
|
||
### Step 5: Install the certificate | ||
|
||
The signed https certificate chain that is output by this script can be used along | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
#!/usr/bin/env python | ||
# Copyright Daniel Roesler, under MIT license, see LICENSE at github.com/diafygi/acme-tiny | ||
import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging | ||
import argparse, subprocess, json, os, sys, base64, binascii, time, hashlib, re, copy, textwrap, logging, hmac | ||
try: | ||
from urllib.request import urlopen, Request # Python 3 | ||
except ImportError: | ||
|
@@ -13,7 +13,7 @@ | |
LOGGER.addHandler(logging.StreamHandler()) | ||
LOGGER.setLevel(logging.INFO) | ||
|
||
def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None): | ||
def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None, eabkid=None, eabhmackey=None): | ||
directory, acct_headers, alg, jwk = None, None, None, None # global variables | ||
|
||
# helper functions - base64 encode for jose spec | ||
|
@@ -70,6 +70,17 @@ def _poll_until_not(url, pending_statuses, err_msg): | |
result, _, _ = _send_signed_request(url, None, err_msg) | ||
return result | ||
|
||
# helper function - build the eAB: externalAccountBinding | ||
def _build_eab(url, eabkid, eabhmackey, jwk): | ||
try: # Decode to verify HMAC b64 is good. Pad b64 string with '='. Py3 strips extra pad. | ||
eabhmackey = base64.urlsafe_b64decode(eabhmackey.strip() + '==') # hmac broken anyway if len%4 != 0 | ||
except (binascii.Error, TypeError): # T-E = py2 (incorrect b64 padding) | ||
log.info("Error verifying eAB HMAC.") | ||
protected64 = _b64(json.dumps({"alg": "HS256", "kid": eabkid, "url": url}).encode('utf-8')) | ||
payload64 = _b64(json.dumps(jwk).encode('utf-8')) | ||
signed64 = _b64(hmac.new(eabhmackey, (protected64 + "." + payload64).encode('utf-8'), digestmod=hashlib.sha256).digest()) | ||
return {"protected": protected64, "payload": payload64, "signature": signed64} | ||
|
||
# parse account key to get public key | ||
log.info("Parsing account key...") | ||
out = _cmd(["openssl", "rsa", "-in", account_key, "-noout", "-text"], err_msg="OpenSSL Error") | ||
|
@@ -109,11 +120,19 @@ def _poll_until_not(url, pending_statuses, err_msg): | |
# create account, update contact details (if any), and set the global key identifier | ||
log.info("Registering account...") | ||
reg_payload = {"termsOfServiceAgreed": True} | ||
account, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering") | ||
if eabkid and eabhmackey: # https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4 | ||
log.info("Building externalAccountBinding...") | ||
reg_payload['externalAccountBinding'] = _build_eab(directory['newAccount'], eabkid, eabhmackey, jwk) | ||
if contact: # some providers, e.g. buypass mandate contact at registration | ||
reg_payload["contact"] = contact | ||
response, code, acct_headers = _send_signed_request(directory['newAccount'], reg_payload, "Error registering") | ||
log.info("Registered!" if code == 201 else "Already registered!") | ||
if contact is not None: | ||
account, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details") | ||
log.info("Updated contact details:\n{0}".format("\n".join(account['contact']))) | ||
if contact and code == 200: # 200 == already reg --> update | ||
response, _, _ = _send_signed_request(acct_headers['Location'], {"contact": contact}, "Error updating contact details") | ||
log.info("Updated contact details:\n{0}".format("\n".join(response['contact']))) | ||
# https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.3 : #userActionRequired only for TOS in RFC8555 | ||
if code == 403 and response['type'] == 'urn:ietf:params:acme:error:userActionRequired': | ||
log.info("You must agree to updated TOS:\n", response['instance']) | ||
|
||
# create a new order | ||
log.info("Creating new order...") | ||
|
@@ -171,7 +190,7 @@ def main(argv=None): | |
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. | ||
account key, so PLEASE READ THROUGH IT! It's only ~220 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_chain.crt | ||
|
@@ -188,10 +207,12 @@ def main(argv=None): | |
parser.add_argument("--directory-url", default=DEFAULT_DIRECTORY_URL, help="certificate authority directory url, default is Let's Encrypt") | ||
parser.add_argument("--ca", default=DEFAULT_CA, help="DEPRECATED! USE --directory-url INSTEAD!") | ||
parser.add_argument("--contact", metavar="CONTACT", default=None, nargs="*", help="Contact details (e.g. mailto:[email protected]) for your account-key") | ||
parser.add_argument("--eabkid", metavar="KID", default=None, help="Key Identifier for External Account Binding") | ||
parser.add_argument("--eabhmackey", metavar="HMAC", default=None, help="HMAC key for External Account Binding") | ||
|
||
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, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact) | ||
signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, log=LOGGER, CA=args.ca, disable_check=args.disable_check, directory_url=args.directory_url, contact=args.contact, eabkid=args.eabkid, eabhmackey=args.eabhmackey) | ||
sys.stdout.write(signed_crt) | ||
|
||
if __name__ == "__main__": # pragma: no cover | ||
|