Skip to content

Commit

Permalink
Implement externalAccountBinding
Browse files Browse the repository at this point in the history
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4

Bonus: also implement
https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.3
(which will never happen on Letsencrypt, but may on other CAs)

Send contact at register time if available (some CAs mandate this)

Renamed `account` variable to the more appropriate: `response`. That is
what this variable holds.
  • Loading branch information
systemcrash committed Sep 6, 2021
1 parent ce0bbdb commit a316469
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 7 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 17 additions & 7 deletions acme_tiny.py
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: # pragma: no cover
Expand All @@ -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
Expand Down Expand Up @@ -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...")
Expand Down Expand Up @@ -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:[email protected]) 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
Expand Down

0 comments on commit a316469

Please sign in to comment.