Compare commits

...

60 Commits

Author SHA1 Message Date
Jacob Hoffman-Andrews
5ee065ef20 Add acme-v2 branches to Travis. 2018-01-03 08:41:14 -08:00
Jacob Hoffman-Andrews
3e0b3beebd authz -> authzr 2018-01-02 15:45:59 -08:00
Jacob Hoffman-Andrews
8ec6b7c6e2 authz -> authzr 2018-01-02 10:55:06 -08:00
Jacob Hoffman-Andrews
c0841ca485 Make pip loud. 2017-12-14 14:53:14 -08:00
Jacob Hoffman-Andrews
b42c23d896 Make chisel2 use josepy. 2017-12-14 14:42:57 -08:00
Jacob Hoffman-Andrews
6cf656a68f Merge branch 'master' of github.com:certbot/certbot into acme-v2 2017-12-14 14:28:10 -08:00
Jacob Hoffman-Andrews
ac956eb08d Use camelCase for directory and other identifiers. 2017-12-14 13:26:04 -08:00
Jacob Hoffman-Andrews
15a8a1e7d8 Remove verify_ssl=False. 2017-12-14 12:54:33 -08:00
Jacob Hoffman-Andrews
37dcbebb1c Implement order finalization. 2017-12-13 18:22:01 -08:00
Jacob Hoffman-Andrews
584bcec337 Add deadline. 2017-12-13 17:41:27 -08:00
Jacob Hoffman-Andrews
40e039176e Move csr initialization. 2017-12-13 17:04:47 -08:00
Jacob Hoffman-Andrews
540d55c4f8 Add csr. 2017-12-08 19:23:19 -08:00
Jacob Hoffman-Andrews
2916ec896c Use identifiers flow, and poll differently. 2017-12-05 22:47:04 -08:00
Jacob Hoffman-Andrews
d9616c44f4 Remove noisy print. 2017-12-05 15:59:56 -08:00
Jacob Hoffman-Andrews
6a8906509f Merge branch 'master' into acme-v2 2017-12-05 14:58:56 -08:00
Jacob Hoffman-Andrews
40392918db Tidy 2017-12-04 14:39:48 -08:00
Jacob Hoffman-Andrews
28228aae09 Merge branch 'master' of github.com:certbot/certbot into acme-v2 2017-12-04 12:45:47 -08:00
Jacob Hoffman-Andrews
2517269a2c Merge branch 'master' of github.com:certbot/certbot into acme-v2 2017-12-01 17:27:25 -08:00
Jacob Hoffman-Andrews
acaf858fdb Add explicit versioning. 2017-11-27 15:08:50 -08:00
Jacob Hoffman-Andrews
f009296669 Remove separate NewAccount. 2017-11-27 15:04:00 -08:00
Jacob Hoffman-Andrews
2070bfa433 Fix resource_type. 2017-11-27 13:54:05 -08:00
Jacob Hoffman-Andrews
b9d7f5ad59 Hide chall body uri, improve challr uri. 2017-11-22 13:11:25 -08:00
Jacob Hoffman-Andrews
92409279d0 Add certificate fetching. 2017-11-21 17:58:00 -08:00
Jacob Hoffman-Andrews
b21b3208df Handle failed authorizations. 2017-11-21 15:34:28 -08:00
Jacob Hoffman-Andrews
cdaba6df33 Fix uri/url distinction in challenges. 2017-11-21 15:34:07 -08:00
Jacob Hoffman-Andrews
e09efb0029 Fix minor dysfunctions. 2017-11-20 17:22:16 -08:00
Jacob Hoffman-Andrews
d5607a7b34 Merge branch 'acme-v2' of github.com:certbot/certbot into acme-v2 2017-11-08 16:36:14 -08:00
Roland Bracewell Shoemaker
6a8f9c387f Small fixes 2017-11-03 14:16:33 -04:00
Roland Bracewell Shoemaker
38982eeedf Merge branch 'master' into acme-v2 2017-11-01 14:26:50 -07:00
Roland Shoemaker
590044ac77 Register new-account in the directory class 2017-10-16 14:14:02 -07:00
Roland Shoemaker
307f4e5c42 Remove tool/chisel2.py (should live in boulder repo) 2017-10-16 14:08:23 -07:00
Roland Shoemaker
1f979dd37b Fix registration endpoint selection + challenge decoding 2017-10-16 14:07:44 -07:00
Roland Shoemaker
5ae60088df revert breaking changes 2017-10-10 12:17:29 -07:00
Jacob Hoffman-Andrews
324caf2def Tweak field names. 2017-08-09 21:14:35 -07:00
Jacob Hoffman-Andrews
9cec48c711 Merge branch 'master' into acme-v2 2017-08-09 21:11:45 -07:00
Jacob Hoffman-Andrews
6bdb87d718 Use kid after register; provide key for signing 2017-05-16 11:55:40 -07:00
Jacob Hoffman-Andrews
3c7b59f730 Merge branch 'master' of github.com:certbot/certbot into acme-v2 2017-04-25 17:26:33 -07:00
Jacob Hoffman-Andrews
db249c9e19 Add polling for order status. 2017-04-25 10:55:01 -07:00
Jacob Hoffman-Andrews
d550542a94 Merge branch 'url-kid' into acme-v2 2017-04-20 17:27:27 -07:00
Jacob Hoffman-Andrews
89ffd76f4b Enforce mutual exclusivity of jwk and kid. 2017-04-20 17:22:20 -07:00
Jacob Hoffman-Andrews
52cb9d9421 Merge branch 'master' of github.com:certbot/certbot into acme-v2 2017-04-05 15:19:59 -07:00
Jacob Hoffman-Andrews
f1bbf87d30 Updates for acme-v2 2017-03-17 12:50:45 -07:00
Jacob Hoffman-Andrews
d35a23d378 Add chisel2 from Boulder master's chisel.py. 2017-03-17 12:49:50 -07:00
Jacob Hoffman-Andrews
34840b7933 Merge branch 'master' into acme-v2 2017-03-15 11:47:04 -07:00
Jacob Hoffman-Andrews
2ddba14d2e Add comments and fix lint. 2017-03-14 15:54:59 -07:00
Jacob Hoffman-Andrews
44c761cb68 Add url and kid to jws.
This will be required in order to implement the latest ACME spec, which uses
these protected header fields.
2017-03-13 18:55:48 -07:00
Jacob Hoffman-Andrews
23ce0c3129 Add authorizations parameter to order resource. 2017-03-13 18:41:42 -07:00
Jacob Hoffman-Andrews
dd52573bcf Fix new_order and make it fetch authzs. 2017-03-13 18:32:20 -07:00
Jacob Hoffman-Andrews
8b1c58bd8e Implement new-order / order. 2017-03-13 17:32:20 -07:00
Jacob Hoffman-Andrews
eff001547a Add url and kid to client. 2017-03-13 17:28:25 -07:00
Jacob Hoffman-Andrews
1e112fa936 Add url and kid to jws. 2017-03-13 17:28:25 -07:00
Jacob Hoffman-Andrews
d806714769 Fix terms-of-service-agreed. 2017-03-13 17:28:25 -07:00
Jacob Hoffman-Andrews
3cfaafb4c2 Merge branch 'master' of github.com:certbot/certbot into unrevert-new-authzr-uri 2017-03-13 17:28:16 -07:00
Jacob Hoffman-Andrews
9ce078bfd1 Add test for deprecated URI argument to request_challenges. 2017-03-10 16:25:20 -08:00
Jacob Hoffman-Andrews
a9e111d5f4 Fix account_test. 2017-03-10 15:04:21 -08:00
Jacob Hoffman-Andrews
bd09d60246 Merge branch 'master' of github.com:certbot/certbot into unrevert-new-authzr-uri 2017-03-09 18:39:32 -08:00
Jacob Hoffman-Andrews
d172a142aa Restore backwards compatibility for new_authzr_uri. 2017-03-09 16:33:42 -08:00
Jacob Hoffman-Andrews
24fd264c9c Add test that new_authzr_uri exists in regr. 2017-03-08 17:27:18 -08:00
Jacob Hoffman-Andrews
fd598d8d19 Save new_authzr_uri with account for older clients. 2017-03-06 14:30:59 -08:00
Jacob Hoffman-Andrews
31e68137cd Revert "Revert "Remove Link rel=next for authzs and new-certs." (#4277)"
This reverts commit 11ec1eb911.
2017-03-06 14:30:34 -08:00
7 changed files with 392 additions and 29 deletions

View File

@@ -67,6 +67,8 @@ branches:
- master
- /^\d+\.\d+\.x$/
- /^test-.*$/
- acme-v2
- acme-v2-integration
# container-based infrastructure
sudo: false

View File

@@ -10,6 +10,7 @@ import time
import six
from six.moves import http_client # pylint: disable=import-error
import crypto_util
import josepy as jose
import OpenSSL
import re
@@ -48,6 +49,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:ivar messages.Directory directory:
:ivar key: `.JWK` (private)
:ivar account: `.Account` (private)
:ivar acme_version: `int` (private) 1 for ACMEv1, or 2 for ACMEv2.
:ivar alg: `.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates?
:ivar .ClientNetwork net: Client network. Useful for testing. If not
@@ -56,8 +59,8 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
net=None):
def __init__(self, directory, key, account=None, acme_version=1, alg=jose.RS256,
verify_ssl=True, net=None):
"""Initialize.
:param directory: Directory Resource (`.messages.Directory`) or
@@ -65,7 +68,10 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
self.key = key
self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net
self.account = account
self.acme_version = acme_version
self.net = ClientNetwork(key, account=account, acme_version=acme_version,
alg=alg, verify_ssl=verify_ssl) if net is None else net
if isinstance(directory, six.string_types):
self.directory = messages.Directory.from_json(
@@ -93,9 +99,12 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
new_reg = messages.NewRegistration() if new_reg is None else new_reg
assert isinstance(new_reg, messages.NewRegistration)
if hasattr(self.directory, 'new_account'):
url = self.directory.new_account
else:
url = self.directory.new_reg
response = self.net.post(self.directory[new_reg], new_reg)
response = self.net.post(url, new_reg)
# TODO: handle errors
assert response.status_code == http_client.CREATED
@@ -168,14 +177,48 @@ class Client(object): # pylint: disable=too-many-instance-attributes
return self.update_registration(
regr.update(body=regr.body.update(agreement=regr.terms_of_service)))
def _authzr_from_response(self, response, identifier, uri=None):
def _authzr_from_response(self, response, identifier=None, uri=None):
authzr = messages.AuthorizationResource(
body=messages.Authorization.from_json(response.json()),
uri=response.headers.get('Location', uri))
if authzr.body.identifier != identifier:
if identifier is not None and authzr.body.identifier != identifier:
raise errors.UnexpectedUpdate(authzr)
return authzr
def _order_resource_from_response(self, response, uri=None, csr=None):
body = messages.Order.from_json(response.json())
authorizations = []
for url in body.authorizations:
authorizations.append(self._authzr_from_response(self.net.get(url)))
fullchain_pem = None
if body.certificate is not None:
certificate_response = self.net.get(body.certificate, content_type=None)
if certificate_response.ok:
fullchain_pem = certificate_response.text
return messages.OrderResource(
body=body,
uri=response.headers.get('Location', uri),
fullchain_pem=fullchain_pem,
authorizations=authorizations,
csr=csr)
def new_order(self, csr_pem):
"""Request challenges.
:returns: List of Authorization Resources.
:rtype: `list` of `.AuthorizationResource`
"""
csr = OpenSSL.crypto.load_certificate_request(OpenSSL.crypto.FILETYPE_PEM, csr_pem)
wrapped_csr = jose.ComparableX509(csr)
identifiers = []
for name in crypto_util._pyopenssl_cert_or_req_san(csr):
identifiers.append(messages.Identifier(typ=messages.IDENTIFIER_FQDN,
value=name))
order = messages.NewOrder(identifiers=identifiers)
response = self.net.post(self.directory.new_order, order)
order_response = self._order_resource_from_response(response, csr=wrapped_csr)
return order_response
def request_challenges(self, identifier, new_authzr_uri=None):
"""Request challenges.
@@ -227,18 +270,15 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .UnexpectedUpdate:
"""
response = self.net.post(challb.uri, response)
challr = messages.ChallengeResource(body=challb)
response = self.net.post(challr.uri, response)
try:
authzr_uri = response.links['up']['url']
except KeyError:
raise errors.ClientError('"up" Link header missing')
challr = messages.ChallengeResource(
return messages.ChallengeResource(
authzr_uri=authzr_uri,
body=messages.ChallengeBody.from_json(response.json()))
# TODO: check that challr.uri == response.headers['Location']?
if challr.uri != challb.uri:
raise errors.UnexpectedUpdate(challr.uri)
return challr
@classmethod
def retry_after(cls, response, default):
@@ -288,6 +328,43 @@ class Client(object): # pylint: disable=too-many-instance-attributes
response, authzr.body.identifier, authzr.uri)
return updated_authzr, response
def poll_order_and_request_issuance(self, orderr, max_time=datetime.timedelta(seconds=90)):
"""Poll Order Resource for status.
:param orderr: Order Resource
:type orderr: `.OrderResource`
:returns: Updated Order Resource
:rtype: (`.OrderResource`)
"""
responses = []
deadline = datetime.datetime.now() + max_time
for url in orderr.body.authorizations:
while datetime.datetime.now() < deadline:
time.sleep(1)
authzr = self._authzr_from_response(self.net.get(url), uri=url)
if authzr.body.status != messages.STATUS_PENDING:
responses.append(authzr)
break
for authzr in responses:
if authzr.body.status != messages.STATUS_VALID:
for chall in authzr.body.challenges:
if chall.error != None:
raise Exception("failed challenge for %s: %s" %
(authzr.body.identifier.value, chall.error))
raise Exception("failed authorization: %s" % authzr.body)
latest = self._order_resource_from_response(self.net.get(orderr.uri), uri=orderr.uri)
self.net.post(latest.body.finalize, messages.CertificateRequest(csr=orderr.csr))
while datetime.datetime.now() < deadline:
time.sleep(1)
latest = self._order_resource_from_response(self.net.get(orderr.uri), uri=orderr.uri)
if latest.fullchain_pem is not None:
return latest
return None
def request_issuance(self, csr, authzrs):
"""Request issuance.
@@ -509,15 +586,18 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
REPLAY_NONCE_HEADER = 'Replay-Nonce'
def __init__(self, key, alg=jose.RS256, verify_ssl=True,
user_agent='acme-python', timeout=DEFAULT_NETWORK_TIMEOUT):
def __init__(self, key, account=None, acme_version=1, alg=jose.RS256,
verify_ssl=True, user_agent='acme-python',
timeout=DEFAULT_NETWORK_TIMEOUT):
self.key = key
self.account = account
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces = set()
self.user_agent = user_agent
self.session = requests.Session()
self._default_timeout = timeout
self.acme_version = acme_version
def __del__(self):
# Try to close the session, but don't show exceptions to the
@@ -527,7 +607,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
except Exception: # pylint: disable=broad-except
pass
def _wrap_in_jws(self, obj, nonce):
def _wrap_in_jws(self, obj, nonce, url):
"""Wrap `JSONDeSerializable` object in JWS.
.. todo:: Implement ``acmePath``.
@@ -539,9 +619,17 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
"""
jobj = obj.json_dumps(indent=2).encode()
logger.debug('JWS payload:\n%s', jobj)
return jws.JWS.sign(
payload=jobj, key=self.key, alg=self.alg,
nonce=nonce).json_dumps(indent=2)
kwargs = {
"alg": self.alg,
"nonce": nonce
}
if self.acme_version is 2:
# new ACME spec
kwargs["url"] = url
if self.account is not None:
kwargs["kid"] = self.account["uri"]
kwargs["key"] = self.key
return jws.JWS.sign(jobj, **kwargs).json_dumps(indent=2)
@classmethod
def _check_response(cls, response, content_type=None):
@@ -715,7 +803,7 @@ class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
raise
def _post_once(self, url, obj, content_type=JOSE_CONTENT_TYPE, **kwargs):
data = self._wrap_in_jws(obj, self._get_nonce(url))
data = self._wrap_in_jws(obj, self._get_nonce(url), url)
kwargs.setdefault('headers', {'Content-Type': content_type})
response = self._send_request('POST', url, data=data, **kwargs)
self._add_nonce(response)

View File

@@ -467,11 +467,21 @@ class ClientNetworkTest(unittest.TestCase):
# pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce=b'Tg')
MockJSONDeSerializable('foo'), nonce=b'Tg', url="url")
jws = acme_jws.JWS.json_loads(jws_dump)
self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'})
self.assertEqual(jws.signature.combined.nonce, b'Tg')
self.net.account = {'uri': 'acct-uri'}
jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce=b'Tg', url="url")
jws = acme_jws.JWS.json_loads(jws_dump)
self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'})
self.assertEqual(jws.signature.combined.nonce, b'Tg')
self.assertEqual(jws.signature.combined.kid, u'acct-uri')
self.assertEqual(jws.signature.combined.url, u'url')
def test_check_response_not_ok_jobj_no_error(self):
self.response.ok = False
self.response.json.return_value = {}
@@ -701,13 +711,13 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.assertEqual(self.checked_response, self.net.post(
'uri', self.obj, content_type=self.content_type))
self.net._wrap_in_jws.assert_called_once_with(
self.obj, jose.b64decode(self.all_nonces.pop()))
self.obj, jose.b64decode(self.all_nonces.pop()), "uri")
self.available_nonces = []
self.assertRaises(errors.MissingNonce, self.net.post,
'uri', self.obj, content_type=self.content_type)
self.net._wrap_in_jws.assert_called_with(
self.obj, jose.b64decode(self.all_nonces.pop()))
self.obj, jose.b64decode(self.all_nonces.pop()), "uri")
def test_post_wrong_initial_nonce(self): # HEAD
self.available_nonces = [b'f', jose.b64encode(b'good')]

View File

@@ -1,5 +1,6 @@
"""ACME protocol messages."""
import collections
import re
import six
import josepy as jose
@@ -171,9 +172,9 @@ class Directory(jose.JSONDeSerializable):
class Meta(jose.JSONObjectWithFields):
"""Directory Meta."""
terms_of_service = jose.Field('terms-of-service', omitempty=True)
terms_of_service = jose.Field('termsOfService', omitempty=True)
website = jose.Field('website', omitempty=True)
caa_identities = jose.Field('caa-identities', omitempty=True)
caa_identities = jose.Field('caaIdentities', omitempty=True)
@classmethod
def _canon_key(cls, key):
@@ -193,11 +194,19 @@ class Directory(jose.JSONDeSerializable):
# not clear on that
self._jobj = canon_jobj
def _camelCase(self, name):
"""Convert a snake_case name to camelCase."""
return re.sub('_([a-z])', lambda x: x.group(1).upper(), name)
def __getattr__(self, name):
try:
return self[name.replace('_', '-')]
except KeyError as error:
raise AttributeError(str(error) + ': ' + name)
try:
print self._camelCase(name)
return self[self._camelCase(name)]
except KeyError as error:
raise AttributeError(str(error) + ': ' + name)
def __getitem__(self, name):
try:
@@ -251,6 +260,7 @@ class Registration(ResourceBody):
contact = jose.Field('contact', omitempty=True, default=())
agreement = jose.Field('agreement', omitempty=True)
status = jose.Field('status', omitempty=True)
terms_of_service_agreed = jose.Field('termsOfServiceAgreed', omitempty=True)
phone_prefix = 'tel:'
email_prefix = 'mailto:'
@@ -299,12 +309,10 @@ class RegistrationResource(ResourceWithURI):
"""Registration Resource.
:ivar acme.messages.Registration body:
:ivar unicode new_authzr_uri: Deprecated. Do not use.
:ivar unicode terms_of_service: URL for the CA TOS.
"""
body = jose.Field('body', decoder=Registration.from_json)
new_authzr_uri = jose.Field('new_authzr_uri', omitempty=True)
terms_of_service = jose.Field('terms_of_service', omitempty=True)
@@ -482,3 +490,37 @@ class Revocation(jose.JSONObjectWithFields):
certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
reason = jose.Field('reason')
class Order(ResourceBody):
"""Order Resource Body.
:ivar buffer csr: CSR in pem format.
:ivar string status:
:ivar list of string authorizations:
:ivar datetime.datetime expires:
"""
identifiers = jose.Field('identifiers', omitempty=True)
status = jose.Field('status', omitempty=True, default=None)
authorizations = jose.Field('authorizations', omitempty=True)
certificate = jose.Field('certificate', omitempty=True)
finalize = jose.Field('finalize', omitempty=True)
expires = fields.RFC3339Field('expires', omitempty=True)
class OrderResource(ResourceWithURI):
"""Order Resource.
:ivar acme.messages.Order body:
"""
body = jose.Field('body', decoder=Order.from_json)
csr = jose.Field('csr', omitempty=True)
authorizations = jose.Field('authorizations')
fullchain_pem = jose.Field('fullchain_pem', omitempty=True)
@Directory.register
class NewOrder(Order):
"""New order."""
resource_type = 'new-order'
resource = fields.Resource(resource_type)

View File

@@ -292,6 +292,11 @@ class ChallengeBodyTest(unittest.TestCase):
def test_from_json(self):
from acme.messages import ChallengeBody
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
jobj_from_cpy = self.jobj_from
jobj_from_cpy["url"] = "http://challb"
del(jobj_from_cpy["uri"])
self.assertEqual(self.challb, ChallengeBody.from_json(jobj_from_cpy))
def test_from_json_hashable(self):
from acme.messages import ChallengeBody

216
chisel2.py Normal file
View File

@@ -0,0 +1,216 @@
"""
A simple client that uses the Python ACME library to run a test issuance against
a local Boulder server. Unlike chisel.py this version implements the most recent
version of the ACME specification. Usage:
$ virtualenv venv
$ . venv/bin/activate
$ pip install -r requirements.txt
$ python chisel.py foo.com bar.com
"""
import json
import logging
import os
import sys
import signal
import threading
import time
import urllib2
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography import x509
from cryptography.hazmat.primitives import hashes
import OpenSSL
import josepy
from acme import challenges
from acme import client as acme_client
from acme import crypto_util as acme_crypto_util
from acme import errors as acme_errors
from acme import messages
from acme import standalone
logging.basicConfig()
logger = logging.getLogger()
logger.setLevel(int(os.getenv('LOGLEVEL', 0)))
DIRECTORY = os.getenv('DIRECTORY', 'http://localhost:4001/directory')
def make_client(email=None):
"""Build an acme.Client and register a new account with a random key."""
key = josepy.JWKRSA(key=rsa.generate_private_key(65537, 2048, default_backend()))
net = acme_client.ClientNetwork(key, acme_version=2,
user_agent="Boulder integration tester")
client = acme_client.Client(DIRECTORY, key=key, net=net, acme_version=2)
tos = client.directory.meta.terms_of_service
if tos is not None and "Do%20what%20thou%20wilt" in tos:
net.account = client.register(messages.NewRegistration.from_data(email=email,
terms_of_service_agreed=True))
else:
raise Exception("Unrecognized terms of service URL %s" % tos)
return client
def get_chall(authz, typ):
for chall_body in authz.body.challenges:
if isinstance(chall_body.chall, typ):
return chall_body
raise "No %s challenge found" % typ
class ValidationError(Exception):
"""An error that occurs during challenge validation."""
def __init__(self, domain, problem_type, detail, *args, **kwargs):
self.domain = domain
self.problem_type = problem_type
self.detail = detail
def __str__(self):
return "%s: %s: %s" % (self.domain, self.problem_type, self.detail)
def make_csr(domains):
key = OpenSSL.crypto.PKey()
key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
pem = OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
return acme_crypto_util.make_csr(pem, domains, False)
def issue(client, authzs, cert_output=None):
"""Given a list of authzs that are being processed by the server,
wait for them to be ready, then request issuance of a cert with a random
key for the given domains.
If cert_output is provided, write the cert as a PEM file to that path."""
csr = make_csr([authz.body.identifier.value for authz in authzs])
cert_resource = None
try:
cert_resource, _ = client.poll_and_request_issuance(jose.ComparableX509(csr), authzs)
except acme_errors.PollError as error:
# If we get a PollError, pick the first failed authz and turn it into a more
# useful ValidationError that contains details we can look for in tests.
for authz in error.updated:
updated_authz = json.loads(urllib2.urlopen(authz.uri).read())
domain = authz.body.identifier.value,
for c in updated_authz['challenges']:
if 'error' in c:
err = c['error']
raise ValidationError(domain, err['type'], err['detail'])
# If none of the authz's had an error, just re-raise.
raise
if cert_output is not None:
pem = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM,
cert_resource.body)
with open(cert_output, 'w') as f:
f.write(pem)
return cert_resource
def http_01_answer(client, chall_body):
"""Return an HTTP01Resource to server in response to the given challenge."""
response, validation = chall_body.response_and_validation(client.key)
return standalone.HTTP01RequestHandler.HTTP01Resource(
chall=chall_body.chall, response=response,
validation=validation)
def auth_and_issue(domains, chall_type="http-01", email=None, cert_output=None, client=None):
"""Make authzs for each of the given domains, set up a server to answer the
challenges in those authzs, tell the ACME server to validate the challenges,
then poll for the authzs to be ready and issue a cert."""
if client is None:
client = make_client(email)
csr_pem = make_csr(domains)
order = client.new_order(csr_pem)
authzs = order.authorizations
if chall_type == "http-01":
cleanup = do_http_challenges(client, authzs)
else:
raise Exception("invalid challenge type %s" % chall_type)
try:
order = client.poll_order_and_request_issuance(order)
print(order.fullchain_pem)
finally:
cleanup()
def do_dns_challenges(client, authzs):
for a in authzs:
c = get_chall(a, challenges.DNS01)
name, value = (c.validation_domain_name(a.body.identifier.value),
c.validation(client.key))
urllib2.urlopen("http://localhost:8055/set-txt",
data=json.dumps({
"host": name + ".",
"value": value,
})).read()
client.answer_challenge(c, c.response(client.key))
def cleanup():
pass
return cleanup
def do_http_challenges(client, authzs):
port = 5002
challs = [get_chall(a, challenges.HTTP01) for a in authzs]
answers = set([http_01_answer(client, c) for c in challs])
server = standalone.HTTP01Server(("", port), answers)
thread = threading.Thread(target=server.serve_forever)
thread.start()
# cleanup has to be called on any exception, or when validation is done.
# Otherwise the process won't terminate.
def cleanup():
server.shutdown()
server.server_close()
thread.join()
try:
# Loop until the HTTP01Server is ready.
while True:
try:
urllib2.urlopen("http://localhost:%d" % port)
break
except urllib2.URLError:
time.sleep(0.1)
for chall_body in challs:
client.answer_challenge(chall_body, chall_body.response(client.key))
except Exception:
cleanup()
raise
return cleanup
def expect_problem(problem_type, func):
"""Run a function. If it raises a ValidationError or messages.Error that
contains the given problem_type, return. If it raises no error or the wrong
error, raise an exception."""
ok = False
try:
func()
except ValidationError as e:
if e.problem_type == problem_type:
ok = True
else:
raise
except messages.Error as e:
if problem_type in e.__str__():
ok = True
else:
raise
if not ok:
raise Exception('Expected %s, got no error' % problem_type)
if __name__ == "__main__":
# Die on SIGINT
signal.signal(signal.SIGINT, signal.SIG_DFL)
domains = sys.argv[1:]
if len(domains) == 0:
print __doc__
sys.exit(0)
try:
auth_and_issue(domains)
except messages.Error, e:
print e
sys.exit(1)

View File

@@ -14,4 +14,4 @@ dev_constraints="$(dirname $my_path)/pip_constraints.txt"
set -x
# install the requested packages using the pinned requirements as constraints
pip install -q --constraint $certbot_auto_constraints --constraint $dev_constraints "$@"
pip install --constraint $certbot_auto_constraints --constraint $dev_constraints "$@"