...

Text file src/github.com/letsencrypt/boulder/test/chisel2.py

Documentation: github.com/letsencrypt/boulder/test

     1"""
     2A simple client that uses the Python ACME library to run a test issuance against
     3a local Boulder server.
     4Usage:
     5
     6$ virtualenv venv
     7$ . venv/bin/activate
     8$ pip install -r requirements.txt
     9$ python chisel2.py foo.com bar.com
    10"""
    11import json
    12import logging
    13import os
    14import sys
    15import signal
    16import threading
    17import time
    18
    19from cryptography.hazmat.backends import default_backend
    20from cryptography.hazmat.primitives.asymmetric import rsa
    21from cryptography import x509
    22from cryptography.hazmat.primitives import hashes
    23
    24import OpenSSL
    25import josepy
    26
    27from acme import challenges
    28from acme import client as acme_client
    29from acme import crypto_util as acme_crypto_util
    30from acme import errors as acme_errors
    31from acme import messages
    32from acme import standalone
    33
    34logging.basicConfig()
    35logger = logging.getLogger()
    36logger.setLevel(int(os.getenv('LOGLEVEL', 20)))
    37
    38DIRECTORY_V2 = os.getenv('DIRECTORY_V2', 'http://boulder.service.consul:4001/directory')
    39ACCEPTABLE_TOS = os.getenv('ACCEPTABLE_TOS',"https://boulder.service.consul:4431/terms/v7")
    40PORT = os.getenv('PORT', '80')
    41
    42os.environ.setdefault('REQUESTS_CA_BUNDLE', 'test/wfe-tls/minica.pem')
    43
    44import challtestsrv
    45challSrv = challtestsrv.ChallTestServer()
    46
    47def uninitialized_client(key=None):
    48    if key is None:
    49        key = josepy.JWKRSA(key=rsa.generate_private_key(65537, 2048, default_backend()))
    50    net = acme_client.ClientNetwork(key, user_agent="Boulder integration tester")
    51    directory = messages.Directory.from_json(net.get(DIRECTORY_V2).json())
    52    return acme_client.ClientV2(directory, net)
    53
    54def make_client(email=None):
    55    """Build an acme.Client and register a new account with a random key."""
    56    client = uninitialized_client()
    57    tos = client.directory.meta.terms_of_service
    58    if tos == ACCEPTABLE_TOS:
    59        client.net.account = client.new_account(messages.NewRegistration.from_data(email=email,
    60            terms_of_service_agreed=True))
    61    else:
    62        raise Exception("Unrecognized terms of service URL %s" % tos)
    63    return client
    64
    65class NoClientError(ValueError):
    66    """
    67    An error that occurs when no acme.Client is provided to a function that
    68    requires one.
    69    """
    70    pass
    71
    72class EmailRequiredError(ValueError):
    73    """
    74    An error that occurs when a None email is provided to update_email.
    75    """
    76
    77def update_email(client, email):
    78    """
    79    Use a provided acme.Client to update the client's account to the specified
    80    email.
    81    """
    82    if client is None:
    83        raise(NoClientError("update_email requires a valid acme.Client argument"))
    84    if email is None:
    85        raise(EmailRequiredError("update_email requires an email argument"))
    86    if not email.startswith("mailto:"):
    87        email = "mailto:"+ email
    88    acct = client.net.account
    89    updatedAcct = acct.update(body=acct.body.update(contact=(email,)))
    90    return client.update_registration(updatedAcct)
    91
    92
    93def get_chall(authz, typ):
    94    for chall_body in authz.body.challenges:
    95        if isinstance(chall_body.chall, typ):
    96            return chall_body
    97    raise Exception("No %s challenge found" % typ.typ)
    98
    99def make_csr(domains):
   100    key = OpenSSL.crypto.PKey()
   101    key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
   102    pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
   103    return acme_crypto_util.make_csr(pem, domains, False)
   104
   105def http_01_answer(client, chall_body):
   106    """Return an HTTP01Resource to server in response to the given challenge."""
   107    response, validation = chall_body.response_and_validation(client.net.key)
   108    return standalone.HTTP01RequestHandler.HTTP01Resource(
   109          chall=chall_body.chall, response=response,
   110          validation=validation)
   111
   112def auth_and_issue(domains, chall_type="dns-01", email=None, cert_output=None, client=None):
   113    """Make authzs for each of the given domains, set up a server to answer the
   114       challenges in those authzs, tell the ACME server to validate the challenges,
   115       then poll for the authzs to be ready and issue a cert."""
   116    if client is None:
   117        client = make_client(email)
   118
   119    csr_pem = make_csr(domains)
   120    order = client.new_order(csr_pem)
   121    authzs = order.authorizations
   122
   123    if chall_type == "http-01":
   124        cleanup = do_http_challenges(client, authzs)
   125    elif chall_type == "dns-01":
   126        cleanup = do_dns_challenges(client, authzs)
   127    elif chall_type == "tls-alpn-01":
   128        cleanup = do_tlsalpn_challenges(client, authzs)
   129    else:
   130        raise Exception("invalid challenge type %s" % chall_type)
   131
   132    try:
   133        order = client.poll_and_finalize(order)
   134        if cert_output is not None:
   135            with open(cert_output, "w") as f:
   136                f.write(order.fullchain_pem)
   137    finally:
   138        cleanup()
   139
   140    return order
   141
   142def do_dns_challenges(client, authzs):
   143    cleanup_hosts = []
   144    for a in authzs:
   145        c = get_chall(a, challenges.DNS01)
   146        name, value = (c.validation_domain_name(a.body.identifier.value),
   147            c.validation(client.net.key))
   148        cleanup_hosts.append(name)
   149        challSrv.add_dns01_response(name, value)
   150        client.answer_challenge(c, c.response(client.net.key))
   151    def cleanup():
   152        for host in cleanup_hosts:
   153            challSrv.remove_dns01_response(host)
   154    return cleanup
   155
   156def do_http_challenges(client, authzs):
   157    cleanup_tokens = []
   158    challs = [get_chall(a, challenges.HTTP01) for a in authzs]
   159
   160    for chall_body in challs:
   161        # Determine the token and key auth for the challenge
   162        token = chall_body.chall.encode("token")
   163        resp = chall_body.response(client.net.key)
   164        keyauth = resp.key_authorization
   165
   166        # Add the HTTP-01 challenge response for this token/key auth to the
   167        # challtestsrv
   168        challSrv.add_http01_response(token, keyauth)
   169        cleanup_tokens.append(token)
   170
   171        # Then proceed initiating the challenges with the ACME server
   172        client.answer_challenge(chall_body, chall_body.response(client.net.key))
   173
   174    def cleanup():
   175        # Cleanup requires removing each of the HTTP-01 challenge responses for
   176        # the tokens we added.
   177        for token in cleanup_tokens:
   178            challSrv.remove_http01_response(token)
   179    return cleanup
   180
   181def do_tlsalpn_challenges(client, authzs):
   182    cleanup_hosts = []
   183    for a in authzs:
   184        c = get_chall(a, challenges.TLSALPN01)
   185        name, value = (a.body.identifier.value, c.key_authorization(client.net.key))
   186        cleanup_hosts.append(name)
   187        challSrv.add_tlsalpn01_response(name, value)
   188        client.answer_challenge(c, c.response(client.net.key))
   189    def cleanup():
   190        for host in cleanup_hosts:
   191            challSrv.remove_tlsalpn01_response(host)
   192    return cleanup
   193
   194def expect_problem(problem_type, func):
   195    """Run a function. If it raises an acme_errors.ValidationError or messages.Error that
   196       contains the given problem_type, return. If it raises no error or the wrong
   197       error, raise an exception."""
   198    ok = False
   199    try:
   200        func()
   201    except messages.Error as e:
   202        if e.typ == problem_type:
   203            ok = True
   204        else:
   205            raise Exception("Expected %s, got %s" % (problem_type, e.__str__()))
   206    except acme_errors.ValidationError as e:
   207        for authzr in e.failed_authzrs:
   208            for chall in authzr.body.challenges:
   209                error = chall.error
   210                if error and error.typ == problem_type:
   211                    ok = True
   212                elif error:
   213                    raise Exception("Expected %s, got %s" % (problem_type, error.__str__()))
   214    if not ok:
   215        raise Exception('Expected %s, got no error' % problem_type)
   216
   217if __name__ == "__main__":
   218    # Die on SIGINT
   219    signal.signal(signal.SIGINT, signal.SIG_DFL)
   220    domains = sys.argv[1:]
   221    if len(domains) == 0:
   222        print(__doc__)
   223        sys.exit(0)
   224    try:
   225        auth_and_issue(domains)
   226    except messages.Error as e:
   227        print(e)
   228        sys.exit(1)

View as plain text