diff --git a/README.md b/README.md index 40a4377a..3c14f5a4 100644 --- a/README.md +++ b/README.md @@ -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 aaa@bbb.com --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 diff --git a/acme_tiny.py b/acme_tiny.py index a25edd91..47a6eae8 100755 --- a/acme_tiny.py +++ b/acme_tiny.py @@ -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: # pragma: no cover @@ -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, check_port=None): +def get_crt(account_key, csr, acme_dir, log=LOGGER, CA=DEFAULT_CA, disable_check=False, directory_url=DEFAULT_DIRECTORY_URL, contact=None, check_port=None, eabkid=None, eabhmackey=None): directory, acct_headers, alg, jwk = None, None, None, None # global variables # helper functions - base64 encode for jose spec @@ -108,11 +108,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} if contact is None else {"termsOfServiceAgreed": True, "contact": contact} - 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("{0} Account ID: {1}".format("Registered!" if code == 201 else "Already registered!", acct_headers['Location'])) - 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...") @@ -189,10 +197,12 @@ def main(argv=None): 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:aaa@bbb.com) for your account-key") parser.add_argument("--check-port", metavar="PORT", default=None, help="what port to use when self-checking the challenge file, default is port 80") + 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, check_port=args.check_port) + 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, check_port=args.check_port, eabkid=args.eabkid, eabhmackey=args.eabhmackey) sys.stdout.write(signed_crt) if __name__ == "__main__": # pragma: no cover