Merge remote-tracking branch 'github/letsencrypt/master' into cli-config-fixes

Conflicts:
	letsencrypt/constants.py
This commit is contained in:
Jakub Warmuz
2015-06-23 07:47:48 +00:00
56 changed files with 1047 additions and 2368 deletions
+7
View File
@@ -18,6 +18,13 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) 'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
class ChallengeResponseTest(unittest.TestCase):
def test_from_json_none(self):
from acme.challenges import ChallengeResponse
self.assertTrue(ChallengeResponse.from_json(None) is None)
class SimpleHTTPTest(unittest.TestCase): class SimpleHTTPTest(unittest.TestCase):
def setUp(self): def setUp(self):
+55 -81
View File
@@ -1,4 +1,4 @@
"""Networking for ACME protocol v02.""" """ACME client API."""
import datetime import datetime
import heapq import heapq
import httplib import httplib
@@ -9,19 +9,18 @@ import M2Crypto
import requests import requests
import werkzeug import werkzeug
from acme import errors
from acme import jose from acme import jose
from acme import jws as acme_jws from acme import jws
from acme import messages2 from acme import messages
from letsencrypt import errors
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
class Network(object): class Client(object): # pylint: disable=too-many-instance-attributes
"""ACME networking. """ACME client.
.. todo:: .. todo::
Clean up raised error types hierarchy, document, and handle (wrap) Clean up raised error types hierarchy, document, and handle (wrap)
@@ -33,8 +32,6 @@ class Network(object):
:ivar bool verify_ssl: Verify SSL certificates? :ivar bool verify_ssl: Verify SSL certificates?
""" """
# TODO: Move below to acme module?
DER_CONTENT_TYPE = 'application/pkix-cert' DER_CONTENT_TYPE = 'application/pkix-cert'
JSON_CONTENT_TYPE = 'application/json' JSON_CONTENT_TYPE = 'application/json'
JSON_ERROR_CONTENT_TYPE = 'application/problem+json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
@@ -58,7 +55,7 @@ class Network(object):
""" """
dumps = obj.json_dumps() dumps = obj.json_dumps()
logging.debug('Serialized JSON: %s', dumps) logging.debug('Serialized JSON: %s', dumps)
return acme_jws.JWS.sign( return jws.JWS.sign(
payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps() payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
@classmethod @classmethod
@@ -75,12 +72,14 @@ class Network(object):
function will raise an error. Otherwise, wrong Content-Type function will raise an error. Otherwise, wrong Content-Type
is ignored, but logged. is ignored, but logged.
:raises letsencrypt.messages2.Error: If server response body :raises .messages.Error: If server response body
carries HTTP Problem (draft-ietf-appsawg-http-problem-00). carries HTTP Problem (draft-ietf-appsawg-http-problem-00).
:raises letsencrypt.errors.NetworkError: In case of other :raises .ClientError: In case of other networking errors.
networking errors.
""" """
logging.debug('Received response %s (headers: %s): %r',
response, response.headers, response.content)
response_ct = response.headers.get('Content-Type') response_ct = response.headers.get('Content-Type')
try: try:
# TODO: response.json() is called twice, once here, and # TODO: response.json() is called twice, once here, and
@@ -96,15 +95,13 @@ class Network(object):
'Ignoring wrong Content-Type (%r) for JSON Error', 'Ignoring wrong Content-Type (%r) for JSON Error',
response_ct) response_ct)
try: try:
logging.error("Error: %s", jobj) raise messages.Error.from_json(jobj)
logging.error("Response from server: %s", response.content)
raise messages2.Error.from_json(jobj)
except jose.DeserializationError as error: except jose.DeserializationError as error:
# Couldn't deserialize JSON object # Couldn't deserialize JSON object
raise errors.NetworkError((response, error)) raise errors.ClientError((response, error))
else: else:
# response is not JSON object # response is not JSON object
raise errors.NetworkError(response) raise errors.ClientError(response)
else: else:
if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE:
logging.debug( logging.debug(
@@ -112,13 +109,13 @@ class Network(object):
'response', response_ct) 'response', response_ct)
if content_type == cls.JSON_CONTENT_TYPE and jobj is None: if content_type == cls.JSON_CONTENT_TYPE and jobj is None:
raise errors.NetworkError( raise errors.ClientError(
'Unexpected response Content-Type: {0}'.format(response_ct)) 'Unexpected response Content-Type: {0}'.format(response_ct))
def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs): def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs):
"""Send GET request. """Send GET request.
:raises letsencrypt.errors.NetworkError: :raises .ClientError:
:returns: HTTP Response :returns: HTTP Response
:rtype: `requests.Response` :rtype: `requests.Response`
@@ -129,22 +126,22 @@ class Network(object):
try: try:
response = requests.get(uri, **kwargs) response = requests.get(uri, **kwargs)
except requests.exceptions.RequestException as error: except requests.exceptions.RequestException as error:
raise errors.NetworkError(error) raise errors.ClientError(error)
self._check_response(response, content_type=content_type) self._check_response(response, content_type=content_type)
return response return response
def _add_nonce(self, response): def _add_nonce(self, response):
if self.REPLAY_NONCE_HEADER in response.headers: if self.REPLAY_NONCE_HEADER in response.headers:
nonce = response.headers[self.REPLAY_NONCE_HEADER] nonce = response.headers[self.REPLAY_NONCE_HEADER]
error = acme_jws.Header.validate_nonce(nonce) error = jws.Header.validate_nonce(nonce)
if error is None: if error is None:
logging.debug('Storing nonce: %r', nonce) logging.debug('Storing nonce: %r', nonce)
self._nonces.add(nonce) self._nonces.add(nonce)
else: else:
raise errors.NetworkError('Invalid nonce ({0}): {1}'.format( raise errors.ClientError('Invalid nonce ({0}): {1}'.format(
nonce, error)) nonce, error))
else: else:
raise errors.NetworkError( raise errors.ClientError(
'Server {0} response did not include a replay nonce'.format( 'Server {0} response did not include a replay nonce'.format(
response.request.method)) response.request.method))
@@ -160,7 +157,7 @@ class Network(object):
:param JSONDeSerializable obj: Will be wrapped in JWS. :param JSONDeSerializable obj: Will be wrapped in JWS.
:param str content_type: Expected ``Content-Type``, fails if not set. :param str content_type: Expected ``Content-Type``, fails if not set.
:raises acme.messages2.NetworkError: :raises acme.messages.ClientError:
:returns: HTTP Response :returns: HTTP Response
:rtype: `requests.Response` :rtype: `requests.Response`
@@ -172,8 +169,7 @@ class Network(object):
try: try:
response = requests.post(uri, data=data, **kwargs) response = requests.post(uri, data=data, **kwargs)
except requests.exceptions.RequestException as error: except requests.exceptions.RequestException as error:
raise errors.NetworkError(error) raise errors.ClientError(error)
logging.debug('Received response %s: %r', response, response.text)
self._add_nonce(response) self._add_nonce(response)
self._check_response(response, content_type=content_type) self._check_response(response, content_type=content_type)
@@ -190,15 +186,15 @@ class Network(object):
try: try:
new_authzr_uri = response.links['next']['url'] new_authzr_uri = response.links['next']['url']
except KeyError: except KeyError:
raise errors.NetworkError('"next" link missing') raise errors.ClientError('"next" link missing')
return messages2.RegistrationResource( return messages.RegistrationResource(
body=messages2.Registration.from_json(response.json()), body=messages.Registration.from_json(response.json()),
uri=response.headers.get('Location', uri), uri=response.headers.get('Location', uri),
new_authzr_uri=new_authzr_uri, new_authzr_uri=new_authzr_uri,
terms_of_service=terms_of_service) terms_of_service=terms_of_service)
def register(self, contact=messages2.Registration._fields[ def register(self, contact=messages.Registration._fields[
'contact'].default): 'contact'].default):
"""Register. """Register.
@@ -208,10 +204,10 @@ class Network(object):
:returns: Registration Resource. :returns: Registration Resource.
:rtype: `.RegistrationResource` :rtype: `.RegistrationResource`
:raises letsencrypt.errors.UnexpectedUpdate: :raises .UnexpectedUpdate:
""" """
new_reg = messages2.Registration(contact=contact) new_reg = messages.Registration(contact=contact)
response = self._post(self.new_reg_uri, new_reg) response = self._post(self.new_reg_uri, new_reg)
assert response.status_code == httplib.CREATED # TODO: handle errors assert response.status_code == httplib.CREATED # TODO: handle errors
@@ -222,24 +218,6 @@ class Network(object):
return regr return regr
def register_from_account(self, account):
"""Register with server.
:param account: Account
:type account: :class:`letsencrypt.account.Account`
:returns: Updated account
:rtype: :class:`letsencrypt.account.Account`
"""
details = (
"mailto:" + account.email if account.email is not None else None,
"tel:" + account.phone if account.phone is not None else None,
)
account.regr = self.register(contact=tuple(
det for det in details if det is not None))
return account
def update_registration(self, regr): def update_registration(self, regr):
"""Update registration. """Update registration.
@@ -257,12 +235,12 @@ class Network(object):
# TODO: Boulder does not set Location or Link on update # TODO: Boulder does not set Location or Link on update
# (c.f. acme-spec #94) # (c.f. acme-spec #94)
updated_regr = self._regr_from_response( updated_regr = self._regr_from_response(
response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
terms_of_service=regr.terms_of_service) terms_of_service=regr.terms_of_service)
if updated_regr != regr: if updated_regr != regr:
raise errors.UnexpectedUpdate(regr) raise errors.UnexpectedUpdate(regr)
return updated_regr return updated_regr
def agree_to_tos(self, regr): def agree_to_tos(self, regr):
@@ -287,10 +265,10 @@ class Network(object):
try: try:
new_cert_uri = response.links['next']['url'] new_cert_uri = response.links['next']['url']
except KeyError: except KeyError:
raise errors.NetworkError('"next" link missing') raise errors.ClientError('"next" link missing')
authzr = messages2.AuthorizationResource( authzr = messages.AuthorizationResource(
body=messages2.Authorization.from_json(response.json()), body=messages.Authorization.from_json(response.json()),
uri=response.headers.get('Location', uri), uri=response.headers.get('Location', uri),
new_cert_uri=new_cert_uri) new_cert_uri=new_cert_uri)
if authzr.body.identifier != identifier: if authzr.body.identifier != identifier:
@@ -301,7 +279,7 @@ class Network(object):
"""Request challenges. """Request challenges.
:param identifier: Identifier to be challenged. :param identifier: Identifier to be challenged.
:type identifier: `.messages2.Identifier` :type identifier: `.messages.Identifier`
:param str new_authzr_uri: new-authorization URI :param str new_authzr_uri: new-authorization URI
@@ -309,7 +287,7 @@ class Network(object):
:rtype: `.AuthorizationResource` :rtype: `.AuthorizationResource`
""" """
new_authz = messages2.Authorization(identifier=identifier) new_authz = messages.Authorization(identifier=identifier)
response = self._post(new_authzr_uri, new_authz) response = self._post(new_authzr_uri, new_authz)
assert response.status_code == httplib.CREATED # TODO: handle errors assert response.status_code == httplib.CREATED # TODO: handle errors
return self._authzr_from_response(response, identifier) return self._authzr_from_response(response, identifier)
@@ -328,8 +306,8 @@ class Network(object):
:rtype: `.AuthorizationResource` :rtype: `.AuthorizationResource`
""" """
return self.request_challenges(messages2.Identifier( return self.request_challenges(messages.Identifier(
typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri) typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri)
def answer_challenge(self, challb, response): def answer_challenge(self, challb, response):
"""Answer challenge. """Answer challenge.
@@ -350,10 +328,10 @@ class Network(object):
try: try:
authzr_uri = response.links['up']['url'] authzr_uri = response.links['up']['url']
except KeyError: except KeyError:
raise errors.NetworkError('"up" Link header missing') raise errors.ClientError('"up" Link header missing')
challr = messages2.ChallengeResource( challr = messages.ChallengeResource(
authzr_uri=authzr_uri, authzr_uri=authzr_uri,
body=messages2.ChallengeBody.from_json(response.json())) body=messages.ChallengeBody.from_json(response.json()))
# TODO: check that challr.uri == response.headers['Location']? # TODO: check that challr.uri == response.headers['Location']?
if challr.uri != challb.uri: if challr.uri != challb.uri:
raise errors.UnexpectedUpdate(challr.uri) raise errors.UnexpectedUpdate(challr.uri)
@@ -412,14 +390,14 @@ class Network(object):
:param authzrs: `list` of `.AuthorizationResource` :param authzrs: `list` of `.AuthorizationResource`
:returns: Issued certificate :returns: Issued certificate
:rtype: `.messages2.CertificateResource` :rtype: `.messages.CertificateResource`
""" """
assert authzrs, "Authorizations list is empty" assert authzrs, "Authorizations list is empty"
logging.debug("Requesting issuance...") logging.debug("Requesting issuance...")
# TODO: assert len(authzrs) == number of SANs # TODO: assert len(authzrs) == number of SANs
req = messages2.CertificateRequest( req = messages.CertificateRequest(
csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs)) csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
@@ -434,9 +412,9 @@ class Network(object):
try: try:
uri = response.headers['Location'] uri = response.headers['Location']
except KeyError: except KeyError:
raise errors.NetworkError('"Location" Header missing') raise errors.ClientError('"Location" Header missing')
return messages2.CertificateResource( return messages.CertificateResource(
uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri,
body=jose.ComparableX509( body=jose.ComparableX509(
M2Crypto.X509.load_cert_der_string(response.content))) M2Crypto.X509.load_cert_der_string(response.content)))
@@ -459,7 +437,7 @@ class Network(object):
``Retry-After`` is not present in the response. ``Retry-After`` is not present in the response.
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
the issued certificate (`.messages2.CertificateResource.), the issued certificate (`.messages.CertificateResource.),
and ``updated_authzrs`` is a `tuple` consisting of updated and ``updated_authzrs`` is a `tuple` consisting of updated
Authorization Resources (`.AuthorizationResource`) as Authorization Resources (`.AuthorizationResource`) as
present in the responses from server, and in the same order present in the responses from server, and in the same order
@@ -488,7 +466,8 @@ class Network(object):
updated_authzr, response = self.poll(updated[authzr]) updated_authzr, response = self.poll(updated[authzr])
updated[authzr] = updated_authzr updated[authzr] = updated_authzr
if updated_authzr.body.status != messages2.STATUS_VALID: # pylint: disable=no-member
if updated_authzr.body.status != messages.STATUS_VALID:
# push back to the priority queue, with updated retry_after # push back to the priority queue, with updated retry_after
heapq.heappush(waiting, (self.retry_after( heapq.heappush(waiting, (self.retry_after(
response, default=mintime), authzr)) response, default=mintime), authzr))
@@ -526,7 +505,7 @@ class Network(object):
# "refresh cert", and this method integrated with self.refresh # "refresh cert", and this method integrated with self.refresh
response, cert = self._get_cert(certr.uri) response, cert = self._get_cert(certr.uri)
if 'Location' not in response.headers: if 'Location' not in response.headers:
raise errors.NetworkError('Location header missing') raise errors.ClientError('Location header missing')
if response.headers['Location'] != certr.uri: if response.headers['Location'] != certr.uri:
raise errors.UnexpectedUpdate(response.text) raise errors.UnexpectedUpdate(response.text)
return certr.update(body=cert) return certr.update(body=cert)
@@ -561,22 +540,17 @@ class Network(object):
else: else:
return None return None
def revoke(self, certr, when=messages2.Revocation.NOW): def revoke(self, cert):
"""Revoke certificate. """Revoke certificate.
:param certr: Certificate Resource :param .ComparableX509 cert: `M2Crypto.X509.X509` wrapped in
:type certr: `.CertificateResource` `.ComparableX509`
:param when: When should the revocation take place? Takes :raises .ClientError: If revocation is unsuccessful.
the same values as `.messages2.Revocation.revoke`.
:raises letsencrypt.errors.NetworkError: If revocation is
unsuccessful.
""" """
rev = messages2.Revocation(revoke=when, authorizations=tuple( response = self._post(messages.Revocation.url(self.new_reg_uri),
authzr.uri for authzr in certr.authzrs)) messages.Revocation(certificate=cert))
response = self._post(certr.uri, rev)
if response.status_code != httplib.OK: if response.status_code != httplib.OK:
raise errors.NetworkError( raise errors.ClientError(
'Successful revocation must return HTTP OK status') 'Successful revocation must return HTTP OK status')
@@ -1,42 +1,29 @@
"""Tests for letsencrypt.network2.""" """Tests for acme.client."""
import datetime import datetime
import httplib import httplib
import os import os
import pkg_resources import pkg_resources
import shutil
import tempfile
import unittest import unittest
import M2Crypto
import mock import mock
import requests import requests
from acme import challenges from acme import challenges
from acme import errors
from acme import jose from acme import jose
from acme import jws as acme_jws from acme import jws as acme_jws
from acme import messages2 from acme import messages
from acme import messages_test
from letsencrypt import account
from letsencrypt import errors
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'cert.pem'))))
CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'cert-san.pem'))))
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr.pem'))))
KEY = jose.JWKRSA.load(pkg_resources.resource_string( KEY = jose.JWKRSA.load(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))) 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
KEY2 = jose.JWKRSA.load(pkg_resources.resource_string( KEY2 = jose.JWKRSA.load(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))) 'acme.jose', os.path.join('testdata', 'rsa256_key.pem')))
class NetworkTest(unittest.TestCase): class ClientTest(unittest.TestCase):
"""Tests for letsencrypt.network2.Network.""" """Tests for acme.client.Client."""
# pylint: disable=too-many-instance-attributes,too-many-public-methods # pylint: disable=too-many-instance-attributes,too-many-public-methods
@@ -44,8 +31,8 @@ class NetworkTest(unittest.TestCase):
self.verify_ssl = mock.MagicMock() self.verify_ssl = mock.MagicMock()
self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped)
from letsencrypt.network2 import Network from acme.client import Client
self.net = Network( self.net = Client(
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl) key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl)
self.nonce = jose.b64encode('Nonce') self.nonce = jose.b64encode('Nonce')
@@ -58,44 +45,39 @@ class NetworkTest(unittest.TestCase):
self.post = mock.MagicMock(return_value=self.response) self.post = mock.MagicMock(return_value=self.response)
self.get = mock.MagicMock(return_value=self.response) self.get = mock.MagicMock(return_value=self.response)
self.identifier = messages2.Identifier( self.identifier = messages.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example.com') typ=messages.IDENTIFIER_FQDN, value='example.com')
self.config = mock.Mock(accounts_dir=tempfile.mkdtemp())
# Registration # Registration
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
reg = messages2.Registration( reg = messages.Registration(
contact=self.contact, key=KEY.public(), recovery_token='t') contact=self.contact, key=KEY.public(), recovery_token='t')
self.regr = messages2.RegistrationResource( self.regr = messages.RegistrationResource(
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
terms_of_service='https://www.letsencrypt-demo.org/tos') terms_of_service='https://www.letsencrypt-demo.org/tos')
# Authorization # Authorization
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
challb = messages2.ChallengeBody( challb = messages.ChallengeBody(
uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID, uri=(authzr_uri + '/1'), status=messages.STATUS_VALID,
chall=challenges.DNS(token='foo')) chall=challenges.DNS(token='foo'))
self.challr = messages2.ChallengeResource( self.challr = messages.ChallengeResource(
body=challb, authzr_uri=authzr_uri) body=challb, authzr_uri=authzr_uri)
self.authz = messages2.Authorization( self.authz = messages.Authorization(
identifier=messages2.Identifier( identifier=messages.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example.com'), typ=messages.IDENTIFIER_FQDN, value='example.com'),
challenges=(challb,), combinations=None) challenges=(challb,), combinations=None)
self.authzr = messages2.AuthorizationResource( self.authzr = messages.AuthorizationResource(
body=self.authz, uri=authzr_uri, body=self.authz, uri=authzr_uri,
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
# Request issuance # Request issuance
self.certr = messages2.CertificateResource( self.certr = messages.CertificateResource(
body=CERT, authzrs=(self.authzr,), body=messages_test.CERT, authzrs=(self.authzr,),
uri='https://www.letsencrypt-demo.org/acme/cert/1', uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca') cert_chain_uri='https://www.letsencrypt-demo.org/ca')
def tearDown(self):
shutil.rmtree(self.config.accounts_dir)
def _mock_post_get(self): def _mock_post_get(self):
# pylint: disable=protected-access # pylint: disable=protected-access
self.net._post = self.post self.net._post = self.post
@@ -127,22 +109,22 @@ class NetworkTest(unittest.TestCase):
self.response.json.return_value = {} self.response.json.return_value = {}
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net._check_response, self.response) errors.ClientError, self.net._check_response, self.response)
def test_check_response_not_ok_jobj_error(self): def test_check_response_not_ok_jobj_error(self):
self.response.ok = False self.response.ok = False
self.response.json.return_value = messages2.Error( self.response.json.return_value = messages.Error(
detail='foo', typ='serverInternal', title='some title').to_json() detail='foo', typ='serverInternal', title='some title').to_json()
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertRaises( self.assertRaises(
messages2.Error, self.net._check_response, self.response) messages.Error, self.net._check_response, self.response)
def test_check_response_not_ok_no_jobj(self): def test_check_response_not_ok_no_jobj(self):
self.response.ok = False self.response.ok = False
self.response.json.side_effect = ValueError self.response.json.side_effect = ValueError
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net._check_response, self.response) errors.ClientError, self.net._check_response, self.response)
def test_check_response_ok_no_jobj_ct_required(self): def test_check_response_ok_no_jobj_ct_required(self):
self.response.json.side_effect = ValueError self.response.json.side_effect = ValueError
@@ -150,7 +132,7 @@ class NetworkTest(unittest.TestCase):
self.response.headers['Content-Type'] = response_ct self.response.headers['Content-Type'] = response_ct
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net._check_response, self.response, errors.ClientError, self.net._check_response, self.response,
content_type=self.net.JSON_CONTENT_TYPE) content_type=self.net.JSON_CONTENT_TYPE)
def test_check_response_ok_no_jobj_no_ct(self): def test_check_response_ok_no_jobj_no_ct(self):
@@ -167,14 +149,14 @@ class NetworkTest(unittest.TestCase):
# pylint: disable=protected-access # pylint: disable=protected-access
self.net._check_response(self.response) self.net._check_response(self.response)
@mock.patch('letsencrypt.network2.requests') @mock.patch('acme.client.requests')
def test_get_requests_error_passthrough(self, requests_mock): def test_get_requests_error_passthrough(self, requests_mock):
requests_mock.exceptions = requests.exceptions requests_mock.exceptions = requests.exceptions
requests_mock.get.side_effect = requests.exceptions.RequestException requests_mock.get.side_effect = requests.exceptions.RequestException
# pylint: disable=protected-access # pylint: disable=protected-access
self.assertRaises(errors.NetworkError, self.net._get, 'uri') self.assertRaises(errors.ClientError, self.net._get, 'uri')
@mock.patch('letsencrypt.network2.requests') @mock.patch('acme.client.requests')
def test_get(self, requests_mock): def test_get(self, requests_mock):
# pylint: disable=protected-access # pylint: disable=protected-access
self.net._check_response = mock.MagicMock() self.net._check_response = mock.MagicMock()
@@ -186,16 +168,16 @@ class NetworkTest(unittest.TestCase):
# pylint: disable=protected-access # pylint: disable=protected-access
self.net._wrap_in_jws = self.wrap_in_jws self.net._wrap_in_jws = self.wrap_in_jws
@mock.patch('letsencrypt.network2.requests') @mock.patch('acme.client.requests')
def test_post_requests_error_passthrough(self, requests_mock): def test_post_requests_error_passthrough(self, requests_mock):
requests_mock.exceptions = requests.exceptions requests_mock.exceptions = requests.exceptions
requests_mock.post.side_effect = requests.exceptions.RequestException requests_mock.post.side_effect = requests.exceptions.RequestException
# pylint: disable=protected-access # pylint: disable=protected-access
self._mock_wrap_in_jws() self._mock_wrap_in_jws()
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
@mock.patch('letsencrypt.network2.requests') @mock.patch('acme.client.requests')
def test_post(self, requests_mock): def test_post(self, requests_mock):
# pylint: disable=protected-access # pylint: disable=protected-access
self.net._check_response = mock.MagicMock() self.net._check_response = mock.MagicMock()
@@ -206,7 +188,7 @@ class NetworkTest(unittest.TestCase):
self.net._check_response.assert_called_once_with( self.net._check_response.assert_called_once_with(
requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct') requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct')
@mock.patch('letsencrypt.network2.requests') @mock.patch('acme.client.requests')
def test_post_replay_nonce_handling(self, requests_mock): def test_post_replay_nonce_handling(self, requests_mock):
# pylint: disable=protected-access # pylint: disable=protected-access
self.net._check_response = mock.MagicMock() self.net._check_response = mock.MagicMock()
@@ -214,7 +196,7 @@ class NetworkTest(unittest.TestCase):
self.net._nonces.clear() self.net._nonces.clear()
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
nonce2 = jose.b64encode('Nonce2') nonce2 = jose.b64encode('Nonce2')
requests_mock.head('uri').headers = { requests_mock.head('uri').headers = {
@@ -231,9 +213,9 @@ class NetworkTest(unittest.TestCase):
# wrong nonce # wrong nonce
requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'} requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'}
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj) errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
@mock.patch('letsencrypt.client.network2.requests') @mock.patch('acme.client.requests')
def test_get_post_verify_ssl(self, requests_mock): def test_get_post_verify_ssl(self, requests_mock):
# pylint: disable=protected-access # pylint: disable=protected-access
self._mock_wrap_in_jws() self._mock_wrap_in_jws()
@@ -274,30 +256,7 @@ class NetworkTest(unittest.TestCase):
self.response.status_code = httplib.CREATED self.response.status_code = httplib.CREATED
self._mock_post_get() self._mock_post_get()
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net.register, self.regr.body) errors.ClientError, self.net.register, self.regr.body)
def test_register_from_account(self):
self.net.register = mock.Mock()
acc = account.Account(
self.config, 'key', email='cert-admin@example.com',
phone='+12025551212')
self.net.register_from_account(acc)
self.net.register.assert_called_with(contact=self.contact)
def test_register_from_account_partial_info(self):
self.net.register = mock.Mock()
acc = account.Account(
self.config, 'key', email='cert-admin@example.com')
acc2 = account.Account(self.config, 'key')
self.net.register_from_account(acc)
self.net.register.assert_called_with(
contact=('mailto:cert-admin@example.com',))
self.net.register_from_account(acc2)
self.net.register.assert_called_with(contact=())
def test_update_registration(self): def test_update_registration(self):
self.response.headers['Location'] = self.regr.uri self.response.headers['Location'] = self.regr.uri
@@ -339,7 +298,7 @@ class NetworkTest(unittest.TestCase):
self.response.status_code = httplib.CREATED self.response.status_code = httplib.CREATED
self._mock_post_get() self._mock_post_get()
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net.request_challenges, errors.ClientError, self.net.request_challenges,
self.identifier, self.regr) self.identifier, self.regr)
def test_request_domain_challenges(self): def test_request_domain_challenges(self):
@@ -363,7 +322,7 @@ class NetworkTest(unittest.TestCase):
def test_answer_challenge_missing_next(self): def test_answer_challenge_missing_next(self):
self._mock_post_get() self._mock_post_get()
self.assertRaises(errors.NetworkError, self.net.answer_challenge, self.assertRaises(errors.ClientError, self.net.answer_challenge,
self.challr.body, challenges.DNSResponse()) self.challr.body, challenges.DNSResponse())
def test_retry_after_date(self): def test_retry_after_date(self):
@@ -372,7 +331,7 @@ class NetworkTest(unittest.TestCase):
datetime.datetime(1999, 12, 31, 23, 59, 59), datetime.datetime(1999, 12, 31, 23, 59, 59),
self.net.retry_after(response=self.response, default=10)) self.net.retry_after(response=self.response, default=10))
@mock.patch('letsencrypt.network2.datetime') @mock.patch('acme.client.datetime')
def test_retry_after_invalid(self, dt_mock): def test_retry_after_invalid(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta dt_mock.timedelta = datetime.timedelta
@@ -382,7 +341,7 @@ class NetworkTest(unittest.TestCase):
datetime.datetime(2015, 3, 27, 0, 0, 10), datetime.datetime(2015, 3, 27, 0, 0, 10),
self.net.retry_after(response=self.response, default=10)) self.net.retry_after(response=self.response, default=10))
@mock.patch('letsencrypt.network2.datetime') @mock.patch('acme.client.datetime')
def test_retry_after_seconds(self, dt_mock): def test_retry_after_seconds(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta dt_mock.timedelta = datetime.timedelta
@@ -392,7 +351,7 @@ class NetworkTest(unittest.TestCase):
datetime.datetime(2015, 3, 27, 0, 0, 50), datetime.datetime(2015, 3, 27, 0, 0, 50),
self.net.retry_after(response=self.response, default=10)) self.net.retry_after(response=self.response, default=10))
@mock.patch('letsencrypt.network2.datetime') @mock.patch('acme.client.datetime')
def test_retry_after_missing(self, dt_mock): def test_retry_after_missing(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta dt_mock.timedelta = datetime.timedelta
@@ -413,30 +372,30 @@ class NetworkTest(unittest.TestCase):
self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr) self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr)
def test_request_issuance(self): def test_request_issuance(self):
self.response.content = CERT.as_der() self.response.content = messages_test.CERT.as_der()
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self.response.links['up'] = {'url': self.certr.cert_chain_uri} self.response.links['up'] = {'url': self.certr.cert_chain_uri}
self._mock_post_get() self._mock_post_get()
self.assertEqual( self.assertEqual(self.certr, self.net.request_issuance(
self.certr, self.net.request_issuance(CSR, (self.authzr,))) messages_test.CSR, (self.authzr,)))
# TODO: check POST args # TODO: check POST args
def test_request_issuance_missing_up(self): def test_request_issuance_missing_up(self):
self.response.content = CERT.as_der() self.response.content = messages_test.CERT.as_der()
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self._mock_post_get() self._mock_post_get()
self.assertEqual( self.assertEqual(
self.certr.update(cert_chain_uri=None), self.certr.update(cert_chain_uri=None),
self.net.request_issuance(CSR, (self.authzr,))) self.net.request_issuance(messages_test.CSR, (self.authzr,)))
def test_request_issuance_missing_location(self): def test_request_issuance_missing_location(self):
self._mock_post_get() self._mock_post_get()
self.assertRaises( self.assertRaises(
errors.NetworkError, self.net.request_issuance, errors.ClientError, self.net.request_issuance,
CSR, (self.authzr,)) messages_test.CSR, (self.authzr,))
@mock.patch('letsencrypt.network2.datetime') @mock.patch('acme.client.datetime')
@mock.patch('letsencrypt.network2.time') @mock.patch('acme.client.time')
def test_poll_and_request_issuance(self, time_mock, dt_mock): def test_poll_and_request_issuance(self, time_mock, dt_mock):
# clock.dt | pylint: disable=no-member # clock.dt | pylint: disable=no-member
clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27))
@@ -462,7 +421,7 @@ class NetworkTest(unittest.TestCase):
if not authzr.retries: # no more retries if not authzr.retries: # no more retries
done = mock.MagicMock(uri=authzr.uri, times=authzr.times) done = mock.MagicMock(uri=authzr.uri, times=authzr.times)
done.body.status = messages2.STATUS_VALID done.body.status = messages.STATUS_VALID
return done, [] return done, []
# response (2nd result tuple element) is reduced to only # response (2nd result tuple element) is reduced to only
@@ -517,10 +476,10 @@ class NetworkTest(unittest.TestCase):
def test_check_cert(self): def test_check_cert(self):
self.response.headers['Location'] = self.certr.uri self.response.headers['Location'] = self.certr.uri
self.response.content = CERT2.as_der() self.response.content = messages_test.CERT.as_der()
self._mock_post_get() self._mock_post_get()
self.assertEqual( self.assertEqual(self.certr.update(body=messages_test.CERT),
self.certr.update(body=CERT2), self.net.check_cert(self.certr)) self.net.check_cert(self.certr))
# TODO: split here and separate test # TODO: split here and separate test
self.response.headers['Location'] = 'foo' self.response.headers['Location'] = 'foo'
@@ -528,9 +487,9 @@ class NetworkTest(unittest.TestCase):
errors.UnexpectedUpdate, self.net.check_cert, self.certr) errors.UnexpectedUpdate, self.net.check_cert, self.certr)
def test_check_cert_missing_location(self): def test_check_cert_missing_location(self):
self.response.content = CERT2.as_der() self.response.content = messages_test.CERT.as_der()
self._mock_post_get() self._mock_post_get()
self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr) self.assertRaises(errors.ClientError, self.net.check_cert, self.certr)
def test_refresh(self): def test_refresh(self):
self.net.check_cert = mock.MagicMock() self.net.check_cert = mock.MagicMock()
@@ -550,13 +509,14 @@ class NetworkTest(unittest.TestCase):
def test_revoke(self): def test_revoke(self):
self._mock_post_get() self._mock_post_get()
self.net.revoke(self.certr, when=messages2.Revocation.NOW) self.net.revoke(self.certr.body)
self.post.assert_called_once_with(self.certr.uri, mock.ANY) self.post.assert_called_once_with(messages.Revocation.url(
self.net.new_reg_uri), mock.ANY)
def test_revoke_bad_status_raises_error(self): def test_revoke_bad_status_raises_error(self):
self.response.status_code = httplib.METHOD_NOT_ALLOWED self.response.status_code = httplib.METHOD_NOT_ALLOWED
self._mock_post_get() self._mock_post_get()
self.assertRaises(errors.NetworkError, self.net.revoke, self.certr) self.assertRaises(errors.ClientError, self.net.revoke, self.certr)
if __name__ == '__main__': if __name__ == '__main__':
+7
View File
@@ -1,8 +1,15 @@
"""ACME errors.""" """ACME errors."""
from acme.jose import errors as jose_errors from acme.jose import errors as jose_errors
class Error(Exception): class Error(Exception):
"""Generic ACME error.""" """Generic ACME error."""
class SchemaValidationError(jose_errors.DeserializationError): class SchemaValidationError(jose_errors.DeserializationError):
"""JSON schema ACME object validation error.""" """JSON schema ACME object validation error."""
class ClientError(Error):
"""Network error."""
class UnexpectedUpdate(ClientError):
"""Unexpected update."""
+6 -1
View File
@@ -218,11 +218,12 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
def fields_to_partial_json(self): def fields_to_partial_json(self):
"""Serialize fields to JSON.""" """Serialize fields to JSON."""
jobj = {} jobj = {}
omitted = set()
for slot, field in self._fields.iteritems(): for slot, field in self._fields.iteritems():
value = getattr(self, slot) value = getattr(self, slot)
if field.omit(value): if field.omit(value):
logging.debug('Omitting empty field "%s" (%s)', slot, value) omitted.add((slot, value))
else: else:
try: try:
jobj[field.json_name] = field.encode(value) jobj[field.json_name] = field.encode(value)
@@ -230,6 +231,10 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
raise errors.SerializationError( raise errors.SerializationError(
'Could not encode {0} ({1}): {2}'.format( 'Could not encode {0} ({1}): {2}'.format(
slot, value, error)) slot, value, error))
if omitted:
# pylint: disable=star-args
logging.debug('Omitted empty fields: %s', ', '.join(
'{0!s}={1!r}'.format(*field) for field in omitted))
return jobj return jobj
def to_partial_json(self): def to_partial_json(self):
+5 -4
View File
@@ -4,7 +4,8 @@ The following command has been used to generate test keys:
and for the CSR: and for the CSR:
python -c from 'letsencrypt.crypto_util import make_csr; openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
import pkg_resources; open("csr2.pem",
"w").write(make_csr(pkg_resources.resource_string("letsencrypt.tests", and for the certificate:
"testdata/rsa512_key.pem"), ["example2.com"])[0])'
openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
-10
View File
@@ -1,10 +0,0 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBXzCCAQkCAQAwejELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEVMBMGA1UEAwwMZXhhbXBsZTIuY29tMFwwDQYJKoZI
hvcNAQEBBQADSwAwSAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNH
tPXJmLlM8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAaAqMCgGCSqGSIb3
DQEJDjEbMBkwFwYDVR0RBBAwDoIMZXhhbXBsZTIuY29tMA0GCSqGSIb3DQEBCwUA
A0EAwsdL4FLIgISKV4vXFmc6QTV7CjBiP4XmPFbeN+gMFdR7QcnRyyxSpXxB0v8Z
oqYboP5LGFt9zC6/9GyjcI9/IQ==
-----END CERTIFICATE REQUEST-----
+304 -321
View File
@@ -1,106 +1,287 @@
"""ACME protocol v00 messages. """ACME protocol messages."""
import urlparse
.. warning:: This module is an implementation of the draft `ACME
protocol version 00`_, and not the "RESTified" `ACME protocol version
01`_ or later. It should work with `older Node.js implementation`_,
but will definitely not work with Boulder_. It is kept for reference
purposes only.
.. _`ACME protocol version 00`:
https://github.com/letsencrypt/acme-spec/blob/v00/draft-barnes-acme.md
.. _`ACME protocol version 01`:
https://github.com/letsencrypt/acme-spec/blob/v01/draft-barnes-acme.md
.. _Boulder: https://github.com/letsencrypt/boulder
.. _`older Node.js implementation`:
https://github.com/letsencrypt/node-acme/commit/f42aa5b7fad4cd2fc289653c4ab14f18052367b3
"""
import jsonschema
from acme import challenges from acme import challenges
from acme import errors from acme import fields
from acme import jose from acme import jose
from acme import other
from acme import util
class Message(jose.TypedJSONObjectWithFields): class Error(jose.JSONObjectWithFields, Exception):
# _fields_to_partial_json | pylint: disable=abstract-method """ACME error.
# pylint: disable=too-few-public-methods
"""ACME message."""
TYPES = {}
type_field_name = "type"
schema = NotImplemented https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
"""JSON schema the object is tested against in :meth:`from_json`.
Subclasses must overrride it with a value that is acceptable by
:func:`jsonschema.validate`, most probably using
:func:`acme.util.load_schema`.
""" """
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
ERROR_TYPE_DESCRIPTIONS = {
'malformed': 'The request message was malformed',
'unauthorized': 'The client lacks sufficient authorization',
'serverInternal': 'The server experienced an internal error',
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
'badNonce': 'The client sent an unacceptable anti-replay nonce',
}
typ = jose.Field('type')
title = jose.Field('title', omitempty=True)
detail = jose.Field('detail')
@typ.encoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
return Error.ERROR_TYPE_NAMESPACE + value
@typ.decoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
# pylint thinks isinstance(value, Error), so startswith is not found
# pylint: disable=no-member
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
raise jose.DeserializationError('Missing error type prefix')
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
raise jose.DeserializationError('Error type not recognized')
return without_prefix
@property
def description(self):
"""Hardcoded error description based on its type."""
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
def __str__(self):
if self.typ is not None:
return ' :: '.join([self.typ, self.description, self.detail])
else:
return str(self.detail)
class _Constant(jose.JSONDeSerializable):
"""ACME constant."""
__slots__ = ('name',)
POSSIBLE_NAMES = NotImplemented
def __init__(self, name):
self.POSSIBLE_NAMES[name] = self
self.name = name
def to_partial_json(self):
return self.name
@classmethod @classmethod
def from_json(cls, jobj): def from_json(cls, value):
"""Deserialize from (possibly invalid) JSON object. if value not in cls.POSSIBLE_NAMES:
raise jose.DeserializationError(
'{0} not recognized'.format(cls.__name__))
return cls.POSSIBLE_NAMES[value]
Note that the input ``jobj`` has not been sanitized in any way. def __repr__(self):
return '{0}({1})'.format(self.__class__.__name__, self.name)
:param jobj: JSON object. def __eq__(self, other):
return isinstance(other, type(self)) and other.name == self.name
:raises acme.errors.SchemaValidationError: if the input def __ne__(self, other):
JSON object could not be validated against JSON schema specified return not self.__eq__(other)
in :attr:`schema`.
:raises acme.jose.errors.DeserializationError: for any
other generic error in decoding.
:returns: instance of the class
"""
msg_cls = cls.get_type_cls(jobj)
# TODO: is that schema testing still relevant?
try:
jsonschema.validate(jobj, msg_cls.schema)
except jsonschema.ValidationError as error:
raise errors.SchemaValidationError(error)
return super(Message, cls).from_json(jobj)
@Message.register # pylint: disable=too-few-public-methods class Status(_Constant):
class Challenge(Message): """ACME "status" field."""
"""ACME "challenge" message. POSSIBLE_NAMES = {}
STATUS_UNKNOWN = Status('unknown')
STATUS_PENDING = Status('pending')
STATUS_PROCESSING = Status('processing')
STATUS_VALID = Status('valid')
STATUS_INVALID = Status('invalid')
STATUS_REVOKED = Status('revoked')
:ivar str nonce: Random data, **not** base64-encoded.
:ivar list challenges: List of
:class:`~acme.challenges.Challenge` objects.
.. todo:: class IdentifierType(_Constant):
1. can challenges contain two challenges of the same type? """ACME identifier type."""
2. can challenges contain duplicates? POSSIBLE_NAMES = {}
3. check "combinations" indices are in valid range IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
4. turn "combinations" elements into sets?
5. turn "combinations" into set?
class Identifier(jose.JSONObjectWithFields):
"""ACME identifier.
:ivar acme.messages.IdentifierType typ:
""" """
typ = "challenge" typ = jose.Field('type', decoder=IdentifierType.from_json)
schema = util.load_schema(typ) value = jose.Field('value')
session_id = jose.Field("sessionID")
nonce = jose.Field("nonce", encoder=jose.b64encode, class Resource(jose.JSONObjectWithFields):
decoder=jose.decode_b64jose) """ACME Resource.
challenges = jose.Field("challenges")
combinations = jose.Field("combinations", omitempty=True, default=()) :ivar str uri: Location of the resource.
:ivar acme.messages.ResourceBody body: Resource body.
"""
body = jose.Field('body')
class ResourceWithURI(Resource):
"""ACME Resource with URI.
:ivar str uri: Location of the resource.
"""
uri = jose.Field('uri') # no ChallengeResource.uri
class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body."""
class Registration(ResourceBody):
"""Registration Resource Body.
:ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact: Contact information following ACME spec
"""
# on new-reg key server ignores 'key' and populates it based on
# JWS.signature.combined.jwk
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
contact = jose.Field('contact', omitempty=True, default=())
recovery_token = jose.Field('recoveryToken', omitempty=True)
agreement = jose.Field('agreement', omitempty=True)
phone_prefix = 'tel:'
email_prefix = 'mailto:'
@classmethod
def from_data(cls, phone=None, email=None, **kwargs):
"""Create registration resource from contact details."""
details = list(kwargs.pop('contact', ()))
if phone is not None:
details.append(cls.phone_prefix + phone)
if email is not None:
details.append(cls.email_prefix + email)
kwargs['contact'] = tuple(details)
return cls(**kwargs)
def _filter_contact(self, prefix):
return tuple(
detail[len(prefix):] for detail in self.contact
if detail.startswith(prefix))
@property
def phones(self):
"""All phones found in the ``contact`` field."""
return self._filter_contact(self.phone_prefix)
@property
def emails(self):
"""All emails found in the ``contact`` field."""
return self._filter_contact(self.email_prefix)
@property
def phone(self):
"""Phone."""
assert len(self.phones) == 1
return self.phones[0]
@property
def email(self):
"""Email."""
assert len(self.emails) == 1
return self.emails[0]
class RegistrationResource(ResourceWithURI):
"""Registration Resource.
:ivar acme.messages.Registration body:
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
:ivar str terms_of_service: URL for the CA TOS.
"""
body = jose.Field('body', decoder=Registration.from_json)
new_authzr_uri = jose.Field('new_authzr_uri')
terms_of_service = jose.Field('terms_of_service', omitempty=True)
class ChallengeBody(ResourceBody):
"""Challenge Resource Body.
.. todo::
Confusingly, this has a similar name to `.challenges.Challenge`,
as well as `.achallenges.AnnotatedChallenge`. Please use names
such as ``challb`` to distinguish instances of this class from
``achall``.
:ivar acme.challenges.Challenge: Wrapped challenge.
Conveniently, all challenge fields are proxied, i.e. you can
call ``challb.x`` to get ``challb.chall.x`` contents.
:ivar acme.messages.Status status:
:ivar datetime.datetime validated:
"""
__slots__ = ('chall',)
uri = jose.Field('uri')
status = jose.Field('status', decoder=Status.from_json)
validated = fields.RFC3339Field('validated', omitempty=True)
def to_partial_json(self):
jobj = super(ChallengeBody, self).to_partial_json()
jobj.update(self.chall.to_partial_json())
return jobj
@classmethod
def fields_from_json(cls, jobj):
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
return jobj_fields
def __getattr__(self, name):
return getattr(self.chall, name)
class ChallengeResource(Resource):
"""Challenge Resource.
:ivar acme.messages.ChallengeBody body:
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
"""
body = jose.Field('body', decoder=ChallengeBody.from_json)
authzr_uri = jose.Field('authzr_uri')
@property
def uri(self): # pylint: disable=missing-docstring,no-self-argument
# bug? 'method already defined line None'
# pylint: disable=function-redefined
return self.body.uri # pylint: disable=no-member
class Authorization(ResourceBody):
"""Authorization Resource Body.
:ivar acme.messages.Identifier identifier:
:ivar list challenges: `list` of `.ChallengeBody`
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
of `int`, as opposed to `list` of `list` from the spec).
:ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact:
:ivar acme.messages.Status status:
:ivar datetime.datetime expires:
"""
identifier = jose.Field('identifier', decoder=Identifier.from_json)
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
# TODO: 'expires' is allowed for Authorization Resources in
# general, but for Key Authorization '[t]he "expires" field MUST
# be absent'... then acme-spec gives example with 'expires'
# present... That's confusing!
expires = fields.RFC3339Field('expires', omitempty=True)
@challenges.decoder @challenges.decoder
def challenges(value): # pylint: disable=missing-docstring,no-self-argument def challenges(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(challenges.Challenge.from_json(chall) for chall in value) return tuple(ChallengeBody.from_json(chall) for chall in value)
@property @property
def resolved_combinations(self): def resolved_combinations(self):
@@ -109,259 +290,61 @@ class Challenge(Message):
for combo in self.combinations) for combo in self.combinations)
@Message.register # pylint: disable=too-few-public-methods class AuthorizationResource(ResourceWithURI):
class ChallengeRequest(Message): """Authorization Resource.
"""ACME "challengeRequest" message."""
typ = "challengeRequest"
schema = util.load_schema(typ)
identifier = jose.Field("identifier")
:ivar acme.messages.Authorization body:
@Message.register # pylint: disable=too-few-public-methods :ivar str new_cert_uri: URI found in the 'next' ``Link`` header
class Authorization(Message):
"""ACME "authorization" message.
:ivar jwk: :class:`acme.jose.JWK`
""" """
typ = "authorization" body = jose.Field('body', decoder=Authorization.from_json)
schema = util.load_schema(typ) new_cert_uri = jose.Field('new_cert_uri')
recovery_token = jose.Field("recoveryToken", omitempty=True)
identifier = jose.Field("identifier", omitempty=True)
jwk = jose.Field("jwk", decoder=jose.JWK.from_json, omitempty=True)
@Message.register class CertificateRequest(jose.JSONObjectWithFields):
class AuthorizationRequest(Message): """ACME new-cert request.
"""ACME "authorizationRequest" message.
:ivar str nonce: Random data from the corresponding :ivar acme.jose.util.ComparableX509 csr:
:attr:`Challenge.nonce`, **not** base64-encoded. `M2Crypto.X509.Request` wrapped in `.ComparableX509`
:ivar list responses: List of completed challenges ( :ivar tuple authorizations: `tuple` of URIs (`str`)
:class:`acme.challenges.ChallengeResponse`).
:ivar signature: Signature (:class:`acme.other.Signature`).
""" """
typ = "authorizationRequest" csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
schema = util.load_schema(typ) authorizations = jose.Field('authorizations', decoder=tuple)
session_id = jose.Field("sessionID")
nonce = jose.Field("nonce", encoder=jose.b64encode,
decoder=jose.decode_b64jose)
responses = jose.Field("responses")
signature = jose.Field("signature", decoder=other.Signature.from_json)
contact = jose.Field("contact", omitempty=True, default=())
@responses.decoder class CertificateResource(ResourceWithURI):
def responses(value): # pylint: disable=missing-docstring,no-self-argument """Certificate Resource.
return tuple(challenges.ChallengeResponse.from_json(chall)
for chall in value) :ivar acme.jose.util.ComparableX509 body:
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
"""
cert_chain_uri = jose.Field('cert_chain_uri')
authzrs = jose.Field('authzrs')
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
:ivar .ComparableX509 certificate: `M2Crypto.X509.X509` wrapped in
`.ComparableX509`
"""
certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
# TODO: acme-spec#138, this allows only one ACME server instance per domain
PATH = '/acme/revoke-cert'
"""Path to revocation URL, see `url`"""
@classmethod @classmethod
def create(cls, name, key, sig_nonce=None, **kwargs): def url(cls, base):
"""Create signed "authorizationRequest". """Get revocation URL.
:param str name: Hostname :param str base: New Registration Resource or server (root) URL.
:param key: Key used for signing.
:type key: :class:`Crypto.PublicKey.RSA`
:param str sig_nonce: Nonce used for signature. Useful for testing.
:kwargs: Any other arguments accepted by the class constructor.
:returns: Signed "authorizationRequest" ACME message.
:rtype: :class:`AuthorizationRequest`
""" """
# pylint: disable=too-many-arguments return urlparse.urljoin(base, cls.PATH)
signature = other.Signature.from_msg(
name + kwargs["nonce"], key, sig_nonce)
return cls(
signature=signature, contact=kwargs.pop("contact", ()), **kwargs)
def verify(self, name):
"""Verify signature.
.. warning:: Caller must check that the public key encoded in the
:attr:`signature`'s :class:`acme.jose.JWK` object
is the correct key for a given context.
:param str name: Hostname
:returns: True iff ``signature`` can be verified, False otherwise.
:rtype: bool
"""
# self.signature is not Field | pylint: disable=no-member
return self.signature.verify(name + self.nonce)
@Message.register # pylint: disable=too-few-public-methods
class Certificate(Message):
"""ACME "certificate" message.
:ivar certificate: The certificate (:class:`M2Crypto.X509.X509`
wrapped in :class:`acme.util.ComparableX509`).
:ivar list chain: Chain of certificates (:class:`M2Crypto.X509.X509`
wrapped in :class:`acme.util.ComparableX509` ).
"""
typ = "certificate"
schema = util.load_schema(typ)
certificate = jose.Field("certificate", encoder=jose.encode_cert,
decoder=jose.decode_cert)
chain = jose.Field("chain", omitempty=True, default=())
refresh = jose.Field("refresh", omitempty=True)
@chain.decoder
def chain(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(jose.decode_cert(cert) for cert in value)
@chain.encoder
def chain(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(jose.encode_cert(cert) for cert in value)
@Message.register
class CertificateRequest(Message):
"""ACME "certificateRequest" message.
:ivar csr: Certificate Signing Request (:class:`M2Crypto.X509.Request`
wrapped in :class:`acme.util.ComparableX509`.
:ivar signature: Signature (:class:`acme.other.Signature`).
"""
typ = "certificateRequest"
schema = util.load_schema(typ)
csr = jose.Field("csr", encoder=jose.encode_csr,
decoder=jose.decode_csr)
signature = jose.Field("signature", decoder=other.Signature.from_json)
@classmethod
def create(cls, key, sig_nonce=None, **kwargs):
"""Create signed "certificateRequest".
:param key: Key used for signing.
:type key: :class:`Crypto.PublicKey.RSA`
:param str sig_nonce: Nonce used for signature. Useful for testing.
:kwargs: Any other arguments accepted by the class constructor.
:returns: Signed "certificateRequest" ACME message.
:rtype: :class:`CertificateRequest`
"""
return cls(signature=other.Signature.from_msg(
kwargs["csr"].as_der(), key, sig_nonce), **kwargs)
def verify(self):
"""Verify signature.
.. warning:: Caller must check that the public key encoded in the
:attr:`signature`'s :class:`acme.jose.JWK` object
is the correct key for a given context.
:returns: True iff ``signature`` can be verified, False otherwise.
:rtype: bool
"""
# self.signature is not Field | pylint: disable=no-member
return self.signature.verify(self.csr.as_der())
@Message.register # pylint: disable=too-few-public-methods
class Defer(Message):
"""ACME "defer" message."""
typ = "defer"
schema = util.load_schema(typ)
token = jose.Field("token")
interval = jose.Field("interval", omitempty=True)
message = jose.Field("message", omitempty=True)
@Message.register # pylint: disable=too-few-public-methods
class Error(Message):
"""ACME "error" message."""
typ = "error"
schema = util.load_schema(typ)
error = jose.Field("error")
message = jose.Field("message", omitempty=True)
more_info = jose.Field("moreInfo", omitempty=True)
MESSAGE_CODES = {
"malformed": "The request message was malformed",
"unauthorized": "The client lacks sufficient authorization",
"serverInternal": "The server experienced an internal error",
"notSupported": "The request type is not supported",
"unknown": "The server does not recognize an ID/token in the request",
"badCSR": "The CSR is unacceptable (e.g., due to a short key)",
}
@Message.register # pylint: disable=too-few-public-methods
class Revocation(Message):
"""ACME "revocation" message."""
typ = "revocation"
schema = util.load_schema(typ)
@Message.register
class RevocationRequest(Message):
"""ACME "revocationRequest" message.
:ivar certificate: Certificate (:class:`M2Crypto.X509.X509`
wrapped in :class:`acme.util.ComparableX509`).
:ivar signature: Signature (:class:`acme.other.Signature`).
"""
typ = "revocationRequest"
schema = util.load_schema(typ)
certificate = jose.Field("certificate", decoder=jose.decode_cert,
encoder=jose.encode_cert)
signature = jose.Field("signature", decoder=other.Signature.from_json)
@classmethod
def create(cls, key, sig_nonce=None, **kwargs):
"""Create signed "revocationRequest".
:param key: Key used for signing.
:type key: :class:`Crypto.PublicKey.RSA`
:param str sig_nonce: Nonce used for signature. Useful for testing.
:kwargs: Any other arguments accepted by the class constructor.
:returns: Signed "revocationRequest" ACME message.
:rtype: :class:`RevocationRequest`
"""
return cls(signature=other.Signature.from_msg(
kwargs["certificate"].as_der(), key, sig_nonce), **kwargs)
def verify(self):
"""Verify signature.
.. warning:: Caller must check that the public key encoded in the
:attr:`signature`'s :class:`acme.jose.JWK` object
is the correct key for a given context.
:returns: True iff ``signature`` can be verified, False otherwise.
:rtype: bool
"""
# self.signature is not Field | pylint: disable=no-member
return self.signature.verify(self.certificate.as_der())
@Message.register # pylint: disable=too-few-public-methods
class StatusRequest(Message):
"""ACME "statusRequest" message."""
typ = "statusRequest"
schema = util.load_schema(typ)
token = jose.Field("token")
-298
View File
@@ -1,298 +0,0 @@
"""ACME protocol messages."""
from acme import challenges
from acme import fields
from acme import jose
class Error(jose.JSONObjectWithFields, Exception):
"""ACME error.
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
"""
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
ERROR_TYPE_DESCRIPTIONS = {
'malformed': 'The request message was malformed',
'unauthorized': 'The client lacks sufficient authorization',
'serverInternal': 'The server experienced an internal error',
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
'badNonce': 'The client sent an unacceptable anti-replay nonce',
}
typ = jose.Field('type')
title = jose.Field('title', omitempty=True)
detail = jose.Field('detail')
@typ.encoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
return Error.ERROR_TYPE_NAMESPACE + value
@typ.decoder
def typ(value): # pylint: disable=missing-docstring,no-self-argument
# pylint thinks isinstance(value, Error), so startswith is not found
# pylint: disable=no-member
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
raise jose.DeserializationError('Missing error type prefix')
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
raise jose.DeserializationError('Error type not recognized')
return without_prefix
@property
def description(self):
"""Hardcoded error description based on its type."""
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
def __str__(self):
if self.typ is not None:
return ' :: '.join([self.typ, self.description, self.detail])
else:
return str(self.detail)
class _Constant(jose.JSONDeSerializable):
"""ACME constant."""
__slots__ = ('name',)
POSSIBLE_NAMES = NotImplemented
def __init__(self, name):
self.POSSIBLE_NAMES[name] = self
self.name = name
def to_partial_json(self):
return self.name
@classmethod
def from_json(cls, value):
if value not in cls.POSSIBLE_NAMES:
raise jose.DeserializationError(
'{0} not recognized'.format(cls.__name__))
return cls.POSSIBLE_NAMES[value]
def __repr__(self):
return '{0}({1})'.format(self.__class__.__name__, self.name)
def __eq__(self, other):
return isinstance(other, type(self)) and other.name == self.name
def __ne__(self, other):
return not self.__eq__(other)
class Status(_Constant):
"""ACME "status" field."""
POSSIBLE_NAMES = {}
STATUS_UNKNOWN = Status('unknown')
STATUS_PENDING = Status('pending')
STATUS_PROCESSING = Status('processing')
STATUS_VALID = Status('valid')
STATUS_INVALID = Status('invalid')
STATUS_REVOKED = Status('revoked')
class IdentifierType(_Constant):
"""ACME identifier type."""
POSSIBLE_NAMES = {}
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
class Identifier(jose.JSONObjectWithFields):
"""ACME identifier.
:ivar acme.messages2.IdentifierType typ:
"""
typ = jose.Field('type', decoder=IdentifierType.from_json)
value = jose.Field('value')
class Resource(jose.ImmutableMap):
"""ACME Resource.
:ivar acme.messages2.ResourceBody body: Resource body.
:ivar str uri: Location of the resource.
"""
__slots__ = ('body', 'uri')
class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body."""
class RegistrationResource(Resource):
"""Registration Resource.
:ivar acme.messages2.Registration body:
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
:ivar str terms_of_service: URL for the CA TOS.
"""
__slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service')
class Registration(ResourceBody):
"""Registration Resource Body.
:ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact: Contact information following ACME spec
"""
# on new-reg key server ignores 'key' and populates it based on
# JWS.signature.combined.jwk
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
contact = jose.Field('contact', omitempty=True, default=())
recovery_token = jose.Field('recoveryToken', omitempty=True)
agreement = jose.Field('agreement', omitempty=True)
class ChallengeResource(Resource, jose.JSONObjectWithFields):
"""Challenge Resource.
:ivar acme.messages2.ChallengeBody body:
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
"""
__slots__ = ('body', 'authzr_uri')
@property
def uri(self): # pylint: disable=missing-docstring,no-self-argument
# bug? 'method already defined line None'
# pylint: disable=function-redefined
return self.body.uri
class ChallengeBody(ResourceBody):
"""Challenge Resource Body.
.. todo::
Confusingly, this has a similar name to `.challenges.Challenge`,
as well as `.achallenges.AnnotatedChallenge`. Please use names
such as ``challb`` to distinguish instances of this class from
``achall``.
:ivar acme.challenges.Challenge: Wrapped challenge.
Conveniently, all challenge fields are proxied, i.e. you can
call ``challb.x`` to get ``challb.chall.x`` contents.
:ivar acme.messages2.Status status:
:ivar datetime.datetime validated:
"""
__slots__ = ('chall',)
uri = jose.Field('uri')
status = jose.Field('status', decoder=Status.from_json)
validated = fields.RFC3339Field('validated', omitempty=True)
def to_partial_json(self):
jobj = super(ChallengeBody, self).to_partial_json()
jobj.update(self.chall.to_partial_json())
return jobj
@classmethod
def fields_from_json(cls, jobj):
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
return jobj_fields
def __getattr__(self, name):
return getattr(self.chall, name)
class AuthorizationResource(Resource):
"""Authorization Resource.
:ivar acme.messages2.Authorization body:
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
"""
__slots__ = ('body', 'uri', 'new_cert_uri')
class Authorization(ResourceBody):
"""Authorization Resource Body.
:ivar acme.messages2.Identifier identifier:
:ivar list challenges: `list` of `.ChallengeBody`
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
of `int`, as opposed to `list` of `list` from the spec).
:ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact:
:ivar acme.messages2.Status status:
:ivar datetime.datetime expires:
"""
identifier = jose.Field('identifier', decoder=Identifier.from_json)
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
# TODO: 'expires' is allowed for Authorization Resources in
# general, but for Key Authorization '[t]he "expires" field MUST
# be absent'... then acme-spec gives example with 'expires'
# present... That's confusing!
expires = fields.RFC3339Field('expires', omitempty=True)
@challenges.decoder
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(ChallengeBody.from_json(chall) for chall in value)
@property
def resolved_combinations(self):
"""Combinations with challenges instead of indices."""
return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations)
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request.
:ivar acme.jose.util.ComparableX509 csr:
`M2Crypto.X509.Request` wrapped in `.ComparableX509`
:ivar tuple authorizations: `tuple` of URIs (`str`)
"""
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
authorizations = jose.Field('authorizations', decoder=tuple)
class CertificateResource(Resource):
"""Certificate Resource.
:ivar acme.jose.util.ComparableX509 body:
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
"""
__slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs')
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
:ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`.
:ivar tuple authorizations: Same as `CertificateRequest.authorizations`
"""
NOW = 'now'
"""A possible value for `revoke`, denoting that certificate should
be revoked now."""
revoke = jose.Field('revoke')
authorizations = CertificateRequest._fields['authorizations']
@revoke.decoder
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
if value == Revocation.NOW:
return value
else:
return fields.RFC3339Field.default_decoder(value)
@revoke.encoder
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
if value == Revocation.NOW:
return value
else:
return fields.RFC3339Field.default_encoder(value)
-250
View File
@@ -1,250 +0,0 @@
"""Tests for acme.messages2."""
import datetime
import os
import pkg_resources
import unittest
import mock
import pytz
from Crypto.PublicKey import RSA
from acme import challenges
from acme import jose
KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
class ErrorTest(unittest.TestCase):
"""Tests for acme.messages2.Error."""
def setUp(self):
from acme.messages2 import Error
self.error = Error(detail='foo', typ='malformed', title='title')
self.jobj = {'detail': 'foo', 'title': 'some title'}
def test_typ_prefix(self):
self.assertEqual('malformed', self.error.typ)
self.assertEqual(
'urn:acme:error:malformed', self.error.to_partial_json()['type'])
self.assertEqual(
'malformed', self.error.from_json(self.error.to_partial_json()).typ)
def test_typ_decoder_missing_prefix(self):
from acme.messages2 import Error
self.jobj['type'] = 'malformed'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
self.jobj['type'] = 'not valid bare type'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
def test_typ_decoder_not_recognized(self):
from acme.messages2 import Error
self.jobj['type'] = 'urn:acme:error:baz'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
def test_description(self):
self.assertEqual(
'The request message was malformed', self.error.description)
def test_from_json_hashable(self):
from acme.messages2 import Error
hash(Error.from_json(self.error.to_json()))
def test_str(self):
self.assertEqual(
'malformed :: The request message was malformed :: foo',
str(self.error))
self.assertEqual('foo', str(self.error.update(typ=None)))
class ConstantTest(unittest.TestCase):
"""Tests for acme.messages2._Constant."""
def setUp(self):
from acme.messages2 import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {}
self.MockConstant = MockConstant # pylint: disable=invalid-name
self.const_a = MockConstant('a')
self.const_b = MockConstant('b')
def test_to_partial_json(self):
self.assertEqual('a', self.const_a.to_partial_json())
self.assertEqual('b', self.const_b.to_partial_json())
def test_from_json(self):
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
self.assertRaises(
jose.DeserializationError, self.MockConstant.from_json, 'c')
def test_from_json_hashable(self):
hash(self.MockConstant.from_json('a'))
def test_repr(self):
self.assertEqual('MockConstant(a)', repr(self.const_a))
self.assertEqual('MockConstant(b)', repr(self.const_b))
def test_equality(self):
const_a_prime = self.MockConstant('a')
self.assertFalse(self.const_a == self.const_b)
self.assertTrue(self.const_a == const_a_prime)
self.assertTrue(self.const_a != self.const_b)
self.assertFalse(self.const_a != const_a_prime)
class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages2.Registration."""
def setUp(self):
key = jose.jwk.JWKRSA(key=KEY.publickey())
contact = ('mailto:letsencrypt-client@letsencrypt.org',)
recovery_token = 'XYZ'
agreement = 'https://letsencrypt.org/terms'
from acme.messages2 import Registration
self.reg = Registration(
key=key, contact=contact, recovery_token=recovery_token,
agreement=agreement)
self.jobj_to = {
'contact': contact,
'recoveryToken': recovery_token,
'agreement': agreement,
'key': key,
}
self.jobj_from = self.jobj_to.copy()
self.jobj_from['key'] = key.to_json()
def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.reg.to_partial_json())
def test_from_json(self):
from acme.messages2 import Registration
self.assertEqual(self.reg, Registration.from_json(self.jobj_from))
def test_from_json_hashable(self):
from acme.messages2 import Registration
hash(Registration.from_json(self.jobj_from))
class ChallengeResourceTest(unittest.TestCase):
"""Tests for acme.messages2.ChallengeResource."""
def test_uri(self):
from acme.messages2 import ChallengeResource
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
uri='http://challb'), authzr_uri='http://authz').uri)
class ChallengeBodyTest(unittest.TestCase):
"""Tests for acme.messages2.ChallengeBody."""
def setUp(self):
self.chall = challenges.DNS(token='foo')
from acme.messages2 import ChallengeBody
from acme.messages2 import STATUS_VALID
self.status = STATUS_VALID
self.challb = ChallengeBody(
uri='http://challb', chall=self.chall, status=self.status)
self.jobj_to = {
'uri': 'http://challb',
'status': self.status,
'type': 'dns',
'token': 'foo',
}
self.jobj_from = self.jobj_to.copy()
self.jobj_from['status'] = 'valid'
def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.challb.to_partial_json())
def test_from_json(self):
from acme.messages2 import ChallengeBody
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
def test_from_json_hashable(self):
from acme.messages2 import ChallengeBody
hash(ChallengeBody.from_json(self.jobj_from))
def test_proxy(self):
self.assertEqual('foo', self.challb.token)
class AuthorizationTest(unittest.TestCase):
"""Tests for acme.messages2.Authorization."""
def setUp(self):
from acme.messages2 import ChallengeBody
from acme.messages2 import STATUS_VALID
self.challbs = (
ChallengeBody(
uri='http://challb1', status=STATUS_VALID,
chall=challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A')),
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
chall=challenges.RecoveryToken()),
)
combinations = ((0, 2), (1, 2))
from acme.messages2 import Authorization
from acme.messages2 import Identifier
from acme.messages2 import IDENTIFIER_FQDN
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
self.authz = Authorization(
identifier=identifier, combinations=combinations,
challenges=self.challbs)
self.jobj_from = {
'identifier': identifier.to_json(),
'challenges': [challb.to_json() for challb in self.challbs],
'combinations': combinations,
}
def test_from_json(self):
from acme.messages2 import Authorization
Authorization.from_json(self.jobj_from)
def test_from_json_hashable(self):
from acme.messages2 import Authorization
hash(Authorization.from_json(self.jobj_from))
def test_resolved_combinations(self):
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0], self.challbs[2]),
(self.challbs[1], self.challbs[2]),
))
class RevocationTest(unittest.TestCase):
"""Tests for acme.messages2.RevocationTest."""
def setUp(self):
from acme.messages2 import Revocation
self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW)
self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime(
2015, 3, 27, tzinfo=pytz.utc))
self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW}
self.jobj_date = {'authorizations': (),
'revoke': '2015-03-27T00:00:00Z'}
def test_revoke_decoder(self):
from acme.messages2 import Revocation
self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now))
self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date))
def test_revoke_encoder(self):
self.assertEqual(self.jobj_now, self.rev_now.to_partial_json())
self.assertEqual(self.jobj_date, self.rev_date.to_partial_json())
def test_from_json_hashable(self):
from acme.messages2 import Revocation
hash(Revocation.from_json(self.rev_now.to_json()))
if __name__ == '__main__':
unittest.main() # pragma: no cover
+300 -441
View File
@@ -3,477 +3,336 @@ import os
import pkg_resources import pkg_resources
import unittest import unittest
import Crypto.PublicKey.RSA from Crypto.PublicKey import RSA
import M2Crypto import M2Crypto
import mock
from acme import challenges from acme import challenges
from acme import errors
from acme import jose from acme import jose
from acme import other
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
pkg_resources.resource_string( pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) 'acme.jose', os.path.join('testdata', 'cert.der')),
M2Crypto.X509.FORMAT_DER))
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'csr.der')),
M2Crypto.X509.FORMAT_DER))
KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
CERT = jose.ComparableX509(M2Crypto.X509.load_cert( CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
pkg_resources.resource_filename( format=M2Crypto.X509.FORMAT_DER, file=pkg_resources.resource_filename(
'letsencrypt.tests', os.path.join('testdata', 'cert.pem')))) 'acme.jose', os.path.join('testdata', 'cert.der'))))
CSR = jose.ComparableX509(M2Crypto.X509.load_request(
pkg_resources.resource_filename(
'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))))
CSR2 = jose.ComparableX509(M2Crypto.X509.load_request(
pkg_resources.resource_filename(
'acme.jose', os.path.join('testdata', 'csr2.pem'))))
class MessageTest(unittest.TestCase):
"""Tests for acme.messages.Message."""
def setUp(self):
# pylint: disable=missing-docstring,too-few-public-methods
from acme.messages import Message
class MockParentMessage(Message):
# pylint: disable=abstract-method
TYPES = {}
@MockParentMessage.register
class MockMessage(MockParentMessage):
typ = 'test'
schema = {
'type': 'object',
'properties': {
'price': {'type': 'number'},
'name': {'type': 'string'},
},
}
price = jose.Field('price')
name = jose.Field('name')
self.parent_cls = MockParentMessage
self.msg = MockMessage(price=123, name='foo')
def test_from_json_validates(self):
self.assertRaises(errors.SchemaValidationError,
self.parent_cls.from_json,
{'type': 'test', 'price': 'asd'})
class ChallengeTest(unittest.TestCase):
def setUp(self):
challs = (
challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'),
challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'),
challenges.RecoveryToken(),
)
combinations = ((0, 2), (1, 2))
from acme.messages import Challenge
self.msg = Challenge(
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
challenges=challs, combinations=combinations)
self.jmsg_to = {
'type': 'challenge',
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
'challenges': challs,
'combinations': combinations,
}
self.jmsg_from = {
'type': 'challenge',
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
'challenges': [chall.to_json() for chall in challs],
'combinations': [[0, 2], [1, 2]], # TODO array tuples
}
def test_resolved_combinations(self):
self.assertEqual(self.msg.resolved_combinations, (
(
challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'),
challenges.RecoveryToken()
),
(
challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'),
challenges.RecoveryToken(),
)
))
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
def test_from_json(self):
from acme.messages import Challenge
self.assertEqual(Challenge.from_json(self.jmsg_from), self.msg)
def test_json_without_optionals(self):
del self.jmsg_from['combinations']
del self.jmsg_to['combinations']
from acme.messages import Challenge
msg = Challenge.from_json(self.jmsg_from)
self.assertEqual(msg.combinations, ())
self.assertEqual(msg.to_partial_json(), self.jmsg_to)
class ChallengeRequestTest(unittest.TestCase):
def setUp(self):
from acme.messages import ChallengeRequest
self.msg = ChallengeRequest(identifier='example.com')
self.jmsg = {
'type': 'challengeRequest',
'identifier': 'example.com',
}
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
def test_from_json(self):
from acme.messages import ChallengeRequest
self.assertEqual(ChallengeRequest.from_json(self.jmsg), self.msg)
class AuthorizationTest(unittest.TestCase):
def setUp(self):
jwk = jose.JWKRSA(key=KEY.publickey())
from acme.messages import Authorization
self.msg = Authorization(recovery_token='tok', jwk=jwk,
identifier='example.com')
self.jmsg = {
'type': 'authorization',
'recoveryToken': 'tok',
'identifier': 'example.com',
'jwk': jwk,
}
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
def test_from_json(self):
self.jmsg['jwk'] = self.jmsg['jwk'].to_partial_json()
from acme.messages import Authorization
self.assertEqual(Authorization.from_json(self.jmsg), self.msg)
def test_json_without_optionals(self):
del self.jmsg['recoveryToken']
del self.jmsg['identifier']
del self.jmsg['jwk']
from acme.messages import Authorization
msg = Authorization.from_json(self.jmsg)
self.assertTrue(msg.recovery_token is None)
self.assertTrue(msg.identifier is None)
self.assertTrue(msg.jwk is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class AuthorizationRequestTest(unittest.TestCase):
def setUp(self):
self.responses = (
challenges.SimpleHTTPResponse(path='Hf5GrX4Q7EBax9hc2jJnfw'),
None, # null
challenges.RecoveryTokenResponse(token='23029d88d9e123e'),
)
self.contact = ("mailto:cert-admin@example.com", "tel:+12025551212")
signature = other.Signature(
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()),
sig='-v\xd8\xc2\xa3\xba0\xd6\x92\x16\xb5.\xbe\xa1[\x04\xbe'
'\x1b\xa1X\xd2)\x18\x94\x8f\xd7\xd0\xc0\xbbcI`W\xdf v'
'\xe4\xed\xe8\x03J\xe8\xc8<?\xc8W\x94\x94cj(\xe7\xaa$'
'\x92\xe9\x96\x11\xc2\xefx\x0bR',
nonce='\xab?\x08o\xe6\x81$\x9f\xa1\xc9\x025\x1c\x1b\xa5+')
from acme.messages import AuthorizationRequest
self.msg = AuthorizationRequest(
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
responses=self.responses,
signature=signature,
contact=self.contact,
)
self.jmsg_to = {
'type': 'authorizationRequest',
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
'responses': self.responses,
'signature': signature,
'contact': self.contact,
}
self.jmsg_from = {
'type': 'authorizationRequest',
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
'responses': [None if response is None else response.to_json()
for response in self.responses],
'signature': signature.to_json(),
# TODO: schema validation doesn't recognize tuples as
# arrays :(
'contact': list(self.contact),
}
def test_create(self):
from acme.messages import AuthorizationRequest
self.assertEqual(self.msg, AuthorizationRequest.create(
name='example.com', key=KEY, responses=self.responses,
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
sig_nonce='\xab?\x08o\xe6\x81$\x9f\xa1\xc9\x025\x1c\x1b\xa5+',
contact=self.contact))
def test_verify(self):
self.assertTrue(self.msg.verify('example.com'))
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
def test_from_json(self):
from acme.messages import AuthorizationRequest
self.assertEqual(
self.msg, AuthorizationRequest.from_json(self.jmsg_from))
def test_json_without_optionals(self):
del self.jmsg_from['contact']
del self.jmsg_to['contact']
from acme.messages import AuthorizationRequest
msg = AuthorizationRequest.from_json(self.jmsg_from)
self.assertEqual(msg.contact, ())
self.assertEqual(self.jmsg_to, msg.to_partial_json())
class CertificateTest(unittest.TestCase):
def setUp(self):
refresh = 'https://example.com/refresh/Dr8eAwTVQfSS/'
from acme.messages import Certificate
self.msg = Certificate(
certificate=CERT, chain=(CERT,), refresh=refresh)
self.jmsg_to = {
'type': 'certificate',
'certificate': jose.b64encode(CERT.as_der()),
'chain': (jose.b64encode(CERT.as_der()),),
'refresh': refresh,
}
self.jmsg_from = self.jmsg_to.copy()
# TODO: schema validation array tuples
self.jmsg_from['chain'] = list(self.jmsg_from['chain'])
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
def test_from_json(self):
from acme.messages import Certificate
self.assertEqual(Certificate.from_json(self.jmsg_from), self.msg)
def test_json_without_optionals(self):
del self.jmsg_from['chain']
del self.jmsg_from['refresh']
del self.jmsg_to['chain']
del self.jmsg_to['refresh']
from acme.messages import Certificate
msg = Certificate.from_json(self.jmsg_from)
self.assertEqual(msg.chain, ())
self.assertTrue(msg.refresh is None)
self.assertEqual(self.jmsg_to, msg.to_partial_json())
class CertificateRequestTest(unittest.TestCase):
def setUp(self):
signature = other.Signature(
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()),
sig='\x15\xed\x84\xaa:\xf2DO\x0e9 \xbcg\xf8\xc0\xcf\x87\x9a'
'\x95\xeb\xffT[\x84[\xec\x85\x7f\x8eK\xe9\xc2\x12\xc8Q'
'\xafo\xc6h\x07\xba\xa6\xdf\xd1\xa7"$\xba=Z\x13n\x14\x0b'
'k\xfe\xee\xb4\xe4\xc8\x05\x9a\x08\xa7',
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9')
from acme.messages import CertificateRequest
self.msg = CertificateRequest(csr=CSR, signature=signature)
self.jmsg_to = {
'type': 'certificateRequest',
'csr': jose.b64encode(CSR.as_der()),
'signature': signature,
}
self.jmsg_from = self.jmsg_to.copy()
self.jmsg_from['signature'] = self.jmsg_from['signature'].to_json()
def test_create(self):
from acme.messages import CertificateRequest
self.assertEqual(self.msg, CertificateRequest.create(
csr=CSR, key=KEY,
sig_nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'))
def test_verify(self):
self.assertTrue(self.msg.verify())
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
def test_from_json(self):
from acme.messages import CertificateRequest
self.assertEqual(self.msg, CertificateRequest.from_json(self.jmsg_from))
class DeferTest(unittest.TestCase):
def setUp(self):
from acme.messages import Defer
self.msg = Defer(
token='O7-s9MNq1siZHlgrMzi9_A', interval=60,
message='Warming up the HSM')
self.jmsg = {
'type': 'defer',
'token': 'O7-s9MNq1siZHlgrMzi9_A',
'interval': 60,
'message': 'Warming up the HSM',
}
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
def test_from_json(self):
from acme.messages import Defer
self.assertEqual(Defer.from_json(self.jmsg), self.msg)
def test_json_without_optionals(self):
del self.jmsg['interval']
del self.jmsg['message']
from acme.messages import Defer
msg = Defer.from_json(self.jmsg)
self.assertTrue(msg.interval is None)
self.assertTrue(msg.message is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class ErrorTest(unittest.TestCase): class ErrorTest(unittest.TestCase):
"""Tests for acme.messages.Error."""
def setUp(self): def setUp(self):
from acme.messages import Error from acme.messages import Error
self.msg = Error( self.error = Error(detail='foo', typ='malformed', title='title')
error='badCSR', message='RSA keys must be at least 2048 bits long', self.jobj = {'detail': 'foo', 'title': 'some title'}
more_info='https://ca.example.com/documentation/csr-requirements')
self.jmsg = { def test_typ_prefix(self):
'type': 'error', self.assertEqual('malformed', self.error.typ)
'error': 'badCSR', self.assertEqual(
'message':'RSA keys must be at least 2048 bits long', 'urn:acme:error:malformed', self.error.to_partial_json()['type'])
'moreInfo': 'https://ca.example.com/documentation/csr-requirements', self.assertEqual(
} 'malformed', self.error.from_json(self.error.to_partial_json()).typ)
def test_typ_decoder_missing_prefix(self):
from acme.messages import Error
self.jobj['type'] = 'malformed'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
self.jobj['type'] = 'not valid bare type'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
def test_typ_decoder_not_recognized(self):
from acme.messages import Error
self.jobj['type'] = 'urn:acme:error:baz'
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
def test_description(self):
self.assertEqual(
'The request message was malformed', self.error.description)
def test_from_json_hashable(self):
from acme.messages import Error
hash(Error.from_json(self.error.to_json()))
def test_str(self):
self.assertEqual(
'malformed :: The request message was malformed :: foo',
str(self.error))
self.assertEqual('foo', str(self.error.update(typ=None)))
class ConstantTest(unittest.TestCase):
"""Tests for acme.messages._Constant."""
def setUp(self):
from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {}
self.MockConstant = MockConstant # pylint: disable=invalid-name
self.const_a = MockConstant('a')
self.const_b = MockConstant('b')
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg) self.assertEqual('a', self.const_a.to_partial_json())
self.assertEqual('b', self.const_b.to_partial_json())
def test_from_json(self): def test_from_json(self):
from acme.messages import Error self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
self.assertEqual(Error.from_json(self.jmsg), self.msg) self.assertRaises(
jose.DeserializationError, self.MockConstant.from_json, 'c')
def test_json_without_optionals(self): def test_from_json_hashable(self):
del self.jmsg['message'] hash(self.MockConstant.from_json('a'))
del self.jmsg['moreInfo']
from acme.messages import Error def test_repr(self):
msg = Error.from_json(self.jmsg) self.assertEqual('MockConstant(a)', repr(self.const_a))
self.assertEqual('MockConstant(b)', repr(self.const_b))
self.assertTrue(msg.message is None) def test_equality(self):
self.assertTrue(msg.more_info is None) const_a_prime = self.MockConstant('a')
self.assertEqual(self.jmsg, msg.to_partial_json()) self.assertFalse(self.const_a == self.const_b)
self.assertTrue(self.const_a == const_a_prime)
self.assertTrue(self.const_a != self.const_b)
self.assertFalse(self.const_a != const_a_prime)
class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages.Registration."""
def setUp(self):
key = jose.jwk.JWKRSA(key=KEY.publickey())
contact = (
'mailto:admin@foo.com',
'tel:1234',
)
recovery_token = 'XYZ'
agreement = 'https://letsencrypt.org/terms'
from acme.messages import Registration
self.reg = Registration(
key=key, contact=contact, recovery_token=recovery_token,
agreement=agreement)
self.jobj_to = {
'contact': contact,
'recoveryToken': recovery_token,
'agreement': agreement,
'key': key,
}
self.jobj_from = self.jobj_to.copy()
self.jobj_from['key'] = key.to_json()
def test_from_data(self):
from acme.messages import Registration
reg = Registration.from_data(phone='1234', email='admin@foo.com')
self.assertEqual(reg.contact, (
'tel:1234',
'mailto:admin@foo.com',
))
def test_phones(self):
self.assertEqual(('1234',), self.reg.phones)
def test_emails(self):
self.assertEqual(('admin@foo.com',), self.reg.emails)
def test_phone(self):
self.assertEqual('1234', self.reg.phone)
def test_email(self):
self.assertEqual('admin@foo.com', self.reg.email)
def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.reg.to_partial_json())
def test_from_json(self):
from acme.messages import Registration
self.assertEqual(self.reg, Registration.from_json(self.jobj_from))
def test_from_json_hashable(self):
from acme.messages import Registration
hash(Registration.from_json(self.jobj_from))
class RegistrationResourceTest(unittest.TestCase):
"""Tests for acme.messages.RegistrationResource."""
def setUp(self):
from acme.messages import RegistrationResource
self.regr = RegistrationResource(
body=mock.sentinel.body, uri=mock.sentinel.uri,
new_authzr_uri=mock.sentinel.new_authzr_uri,
terms_of_service=mock.sentinel.terms_of_service)
def test_to_partial_json(self):
self.assertEqual(self.regr.to_json(), {
'body': mock.sentinel.body,
'uri': mock.sentinel.uri,
'new_authzr_uri': mock.sentinel.new_authzr_uri,
'terms_of_service': mock.sentinel.terms_of_service,
})
class ChallengeResourceTest(unittest.TestCase):
"""Tests for acme.messages.ChallengeResource."""
def test_uri(self):
from acme.messages import ChallengeResource
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
uri='http://challb'), authzr_uri='http://authz').uri)
class ChallengeBodyTest(unittest.TestCase):
"""Tests for acme.messages.ChallengeBody."""
def setUp(self):
self.chall = challenges.DNS(token='foo')
from acme.messages import ChallengeBody
from acme.messages import STATUS_VALID
self.status = STATUS_VALID
self.challb = ChallengeBody(
uri='http://challb', chall=self.chall, status=self.status)
self.jobj_to = {
'uri': 'http://challb',
'status': self.status,
'type': 'dns',
'token': 'foo',
}
self.jobj_from = self.jobj_to.copy()
self.jobj_from['status'] = 'valid'
def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.challb.to_partial_json())
def test_from_json(self):
from acme.messages import ChallengeBody
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
def test_from_json_hashable(self):
from acme.messages import ChallengeBody
hash(ChallengeBody.from_json(self.jobj_from))
def test_proxy(self):
self.assertEqual('foo', self.challb.token)
class AuthorizationTest(unittest.TestCase):
"""Tests for acme.messages.Authorization."""
def setUp(self):
from acme.messages import ChallengeBody
from acme.messages import STATUS_VALID
self.challbs = (
ChallengeBody(
uri='http://challb1', status=STATUS_VALID,
chall=challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A')),
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
chall=challenges.RecoveryToken()),
)
combinations = ((0, 2), (1, 2))
from acme.messages import Authorization
from acme.messages import Identifier
from acme.messages import IDENTIFIER_FQDN
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
self.authz = Authorization(
identifier=identifier, combinations=combinations,
challenges=self.challbs)
self.jobj_from = {
'identifier': identifier.to_json(),
'challenges': [challb.to_json() for challb in self.challbs],
'combinations': combinations,
}
def test_from_json(self):
from acme.messages import Authorization
Authorization.from_json(self.jobj_from)
def test_from_json_hashable(self):
from acme.messages import Authorization
hash(Authorization.from_json(self.jobj_from))
def test_resolved_combinations(self):
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0], self.challbs[2]),
(self.challbs[1], self.challbs[2]),
))
class AuthorizationResourceTest(unittest.TestCase):
"""Tests for acme.messages.AuthorizationResource."""
def test_json_de_serializable(self):
from acme.messages import AuthorizationResource
authzr = AuthorizationResource(
uri=mock.sentinel.uri,
body=mock.sentinel.body,
new_cert_uri=mock.sentinel.new_cert_uri,
)
self.assertTrue(isinstance(authzr, jose.JSONDeSerializable))
class CertificateRequestTest(unittest.TestCase):
"""Tests for acme.messages.CertificateRequest."""
def setUp(self):
from acme.messages import CertificateRequest
self.req = CertificateRequest(csr=CSR, authorizations=('foo',))
def test_json_de_serializable(self):
self.assertTrue(isinstance(self.req, jose.JSONDeSerializable))
from acme.messages import CertificateRequest
self.assertEqual(
self.req, CertificateRequest.from_json(self.req.to_json()))
class CertificateResourceTest(unittest.TestCase):
"""Tests for acme.messages.CertificateResourceTest."""
def setUp(self):
from acme.messages import CertificateResource
self.certr = CertificateResource(
body=CERT, uri=mock.sentinel.uri, authzrs=(),
cert_chain_uri=mock.sentinel.cert_chain_uri)
def test_json_de_serializable(self):
self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable))
from acme.messages import CertificateResource
self.assertEqual(
self.certr, CertificateResource.from_json(self.certr.to_json()))
class RevocationTest(unittest.TestCase): class RevocationTest(unittest.TestCase):
"""Tests for acme.messages.RevocationTest."""
def test_url(self):
from acme.messages import Revocation
url = 'https://letsencrypt-demo.org/acme/revoke-cert'
self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org'))
self.assertEqual(
url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg'))
def setUp(self): def setUp(self):
from acme.messages import Revocation from acme.messages import Revocation
self.msg = Revocation() self.rev = Revocation(certificate=CERT)
self.jmsg = {'type': 'revocation'}
def test_to_partial_json(self): def test_from_json_hashable(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
def test_from_json(self):
from acme.messages import Revocation from acme.messages import Revocation
self.assertEqual(Revocation.from_json(self.jmsg), self.msg) hash(Revocation.from_json(self.rev.to_json()))
class RevocationRequestTest(unittest.TestCase):
def setUp(self):
self.sig_nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
signature = other.Signature(
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()),
sig='eJ\xfe\x12"U\x87\x8b\xbf/ ,\xdeP\xb2\xdc1\xb00\xe5\x1dB'
'\xfch<\xc6\x9eH@!\x1c\x16\xb2\x0b_\xc4\xddP\x89\xc8\xce?'
'\x16g\x069I\xb9\xb3\x91\xb9\x0e$3\x9f\x87\x8e\x82\xca\xc5'
's\xd9\xd0\xe7',
nonce=self.sig_nonce)
from acme.messages import RevocationRequest
self.msg = RevocationRequest(certificate=CERT, signature=signature)
self.jmsg_to = {
'type': 'revocationRequest',
'certificate': jose.b64encode(CERT.as_der()),
'signature': signature,
}
self.jmsg_from = self.jmsg_to.copy()
self.jmsg_from['signature'] = self.jmsg_from['signature'].to_json()
def test_create(self):
from acme.messages import RevocationRequest
self.assertEqual(self.msg, RevocationRequest.create(
certificate=CERT, key=KEY, sig_nonce=self.sig_nonce))
def test_verify(self):
self.assertTrue(self.msg.verify())
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
def test_from_json(self):
from acme.messages import RevocationRequest
self.assertEqual(self.msg, RevocationRequest.from_json(self.jmsg_from))
class StatusRequestTest(unittest.TestCase):
def setUp(self):
from acme.messages import StatusRequest
self.msg = StatusRequest(token=u'O7-s9MNq1siZHlgrMzi9_A')
self.jmsg = {
'type': 'statusRequest',
'token': u'O7-s9MNq1siZHlgrMzi9_A',
}
def test_to_partial_json(self):
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
def test_from_json(self):
from acme.messages import StatusRequest
self.assertEqual(StatusRequest.from_json(self.jmsg), self.msg)
if __name__ == '__main__': if __name__ == '__main__':
-21
View File
@@ -1,21 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/authorization#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for an authorization message",
"type": "object",
"required": ["type"],
"properties": {
"type" : {
"enum" : [ "authorization" ]
},
"recoveryToken" : {
"type": "string"
},
"identifier" : {
"type": "string"
},
"jwk": {
"$ref": "file:acme/schemata/jwk.json"
}
}
}
-38
View File
@@ -1,38 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/authorizationRequest#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for an authorizationRequest message",
"type": "object",
"required": ["type", "sessionID", "nonce", "signature", "responses"],
"properties": {
"type" : {
"enum" : [ "authorizationRequest" ]
},
"sessionID" : {
"type" : "string"
},
"nonce" : {
"type": "string"
},
"signature" : {
"$ref": "file:acme/schemata/signature.json"
},
"responses": {
"type": "array",
"minItems": 1,
"items": {
"anyOf": [
{ "$ref": "file:acme/schemata/responseobject.json" },
{ "type": "null" }
]
}
},
"contact": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
}
-25
View File
@@ -1,25 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/certificate#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a certificate message",
"type": "object",
"required": ["type", "certificate"],
"properties": {
"type" : {
"enum" : [ "certificate" ]
},
"certificate" : {
"type" : "string"
},
"chain" : {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"refresh" : {
"type": "string"
}
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/certificateRequest#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a certificateRequest message",
"type": "object",
"required": ["type", "csr", "signature"],
"properties": {
"type" : {
"enum" : [ "certificateRequest" ]
},
"csr" : {
"type" : "string" ,
"pattern": "^[-_=0-9A-Za-z]+$"
},
"signature" : {
"$ref": "file:acme/schemata/signature.json"
}
}
}
-36
View File
@@ -1,36 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/challenge#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a challenge message",
"type": "object",
"required": ["type", "sessionID", "nonce", "challenges"],
"properties": {
"type" : {
"enum" : [ "challenge" ]
},
"sessionID" : {
"type" : "string"
},
"nonce" : {
"type": "string"
},
"challenges": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "file:acme/schemata/challengeobject.json"
}
},
"combinations": {
"type": "array",
"minItems": 1,
"items": {
"type": "array",
"minItems": 1,
"items": {
"type": "integer"
}
}
}
}
}
-15
View File
@@ -1,15 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/challengeRequest#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a challengeRequest message",
"type": "object",
"required": ["type", "identifier"],
"properties": {
"type" : {
"enum" : [ "challengeRequest" ]
},
"identifier" : {
"type": "string"
}
}
}
-130
View File
@@ -1,130 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/challengeobject#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Subschema for an individual challenge (within challenge)",
"anyOf": [
{ "type": "object",
"required": ["type", "token"],
"properties": {
"type": {
"enum" : [ "simpleHttp" ]
},
"token": {
"type": "string"
}
}
},
{ "type": "object",
"required": ["type", "r", "nonce"],
"properties": {
"type": {
"enum" : [ "dvsni" ]
},
"r": {
"type" : [ "string" ],
"pattern": "^[-_=0-9A-Za-z]+$"
},
"nonce": {
"type": "string",
"pattern": "^[0-9a-f]+$"
}
}
},
{ "type": "object",
"required": ["type"],
"properties": {
"type": {
"enum" : [ "recoveryContact" ]
},
"activationURL": {
"type" : "string"
},
"successURL": {
"type": "string"
},
"contact": {
"type": "string"
}
}
},
{ "type": "object",
"required": ["type"],
"properties": {
"type": {
"enum" : [ "recoveryToken" ]
}
}
},
{ "type": "object",
"required": ["type", "alg", "nonce", "hints"],
"properties": {
"type": {
"enum" : [ "proofOfPossession" ]
},
"alg": {
"type": "string"
},
"nonce": {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
},
"hints": {
"type": "object",
"properties": {
"jwk": {
"type": "object"
},
"certFingerprints": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[0-9a-f]+$"
}
},
"subjectKeyIdentifiers": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[0-9a-f]+$"
}
},
"serialNumbers": {
"type": "array",
"minItems": 1,
"items": {
"type": "integer"
}
},
"issuers": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
},
"authorizedFor": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
}
}
},
{ "type": "object",
"required": ["type", "token"],
"properties": {
"type": {
"enum" : [ "dns" ]
},
"token": {
"type": "string"
}
}
}
]
}
-21
View File
@@ -1,21 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/defer#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a defer message",
"type": "object",
"required": ["type", "token"],
"properties": {
"type" : {
"enum" : [ "defer" ]
},
"token" : {
"type": "string"
},
"interval" : {
"type": "integer"
},
"message": {
"type": "string"
}
}
}
-21
View File
@@ -1,21 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/error#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for an error message",
"type": "object",
"required": ["type", "error"],
"properties": {
"type" : {
"enum" : [ "error" ]
},
"error" : {
"enum" : [ "malformed", "unauthorized", "serverInternal", "nonSupported", "unknown", "badCSR" ]
},
"message" : {
"type": "string"
},
"moreInfo": {
"type": "string"
}
}
}
-19
View File
@@ -1,19 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/jwk#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a jwk (**kty RSA/e=65537 ONLY**)",
"type": "object",
"required": ["kty", "e", "n"],
"properties": {
"kty": {
"enum" : [ "RSA" ]
},
"e": {
"enum" : [ "AQAB" ]
},
"n": {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
}
}
}
-75
View File
@@ -1,75 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/responseobject#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Subschema for an individual challenge response (within authorizationRequest)",
"anyOf": [
{ "type": "object",
"required": ["type", "path"],
"properties": {
"type": {
"enum" : [ "simpleHttp" ]
},
"path": {
"type": "string"
}
}
},
{ "type": "object",
"required": ["type", "s"],
"properties": {
"type": {
"enum" : [ "dvsni" ]
},
"s": {
"type" : [ "string" ],
"pattern": "^[-_=0-9A-Za-z]+$"
}
}
},
{ "type": "object",
"required": ["type"],
"properties": {
"type": {
"enum" : [ "recoveryContact" ]
},
"token": {
"type" : "string"
}
}
},
{ "type": "object",
"required": ["type"],
"properties": {
"type": {
"enum" : [ "recoveryToken" ]
},
"token": {
"type" : "string"
}
}
},
{ "type": "object",
"required": ["type", "nonce", "signature"],
"properties": {
"type": {
"enum" : [ "proofOfPossession" ]
},
"nonce": {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
},
"signature": {
"$ref": "file:acme/schemata/signature.json"
}
}
},
{ "type": "object",
"required": ["type"],
"properties": {
"type": {
"enum" : [ "dns" ]
}
}
}
]
}
-12
View File
@@ -1,12 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/revocation#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a revocation message",
"type": "object",
"required": ["type"],
"properties": {
"type" : {
"enum" : [ "revocation" ]
}
}
}
-18
View File
@@ -1,18 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/revocationRequest#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a revocationRequest message",
"type": "object",
"required": ["type", "certificate", "signature"],
"properties": {
"type" : {
"enum" : [ "revocationRequest" ]
},
"certificate" : {
"type" : "string"
},
"signature" : {
"$ref": "file:acme/schemata/signature.json"
}
}
}
-71
View File
@@ -1,71 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/signature#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a signature (alg RS256/e=65537 or P-256 ONLY)",
"type": "object",
"required": ["alg", "nonce", "sig", "jwk"],
"properties": {
"anyOf": [
{
"alg" : {
"enum" : [ "RS256" ]
},
"nonce" : {
"type" : "string"
},
"sig" : {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
},
"jwk": {
"type": "object",
"required": ["kty", "e", "n"],
"properties": {
"kty": {
"enum" : [ "RSA" ]
},
"e": {
"enum" : [ "AQAB" ]
},
"n": {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
}
}
}
},
{
"alg" : {
"enum" : [ "ES256" ]
},
"nonce" : {
"type" : "string"
},
"sig" : {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
},
"jwk": {
"type": "object",
"required": ["kty", "crv", "x", "y"],
"properties": {
"kty": {
"enum" : [ "EC" ]
},
"crv": {
"enum" : [ "P-256" ]
},
"x": {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
},
"y": {
"type": "string",
"pattern": "^[-_=0-9A-Za-z]+$"
}
}
}
}
]
}
}
-15
View File
@@ -1,15 +0,0 @@
{
"id": "https://letsencrypt.org/schema/01/statusRequest#",
"$schema": "http://json-schema.org/draft-04/schema#",
"description": "Schema for a statusRequest message",
"type": "object",
"required": ["type", "token"],
"properties": {
"type" : {
"enum" : [ "statusRequest" ]
},
"token" : {
"type": "string"
}
}
}
-5
View File
@@ -1,5 +0,0 @@
:mod:`letsencrypt.network2`
---------------------------
.. automodule:: letsencrypt.network2
:members:
+7 -9
View File
@@ -7,21 +7,19 @@
:members: :members:
Client
------
.. automodule:: acme.client
:members:
Messages Messages
-------- --------
v00
~~~
.. automodule:: acme.messages .. automodule:: acme.messages
:members: :members:
v02
~~~
.. automodule:: acme.messages2
:members:
Challenges Challenges
---------- ----------
+45
View File
@@ -0,0 +1,45 @@
"""Example script showing how to use acme client API."""
import logging
import os
import pkg_resources
import Crypto.PublicKey.RSA
import M2Crypto
from acme import client
from acme import messages
from acme import jose
logging.basicConfig(level=logging.DEBUG)
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
BITS = 2048 # minimum for Boulder
DOMAIN = 'example1.com' # example.com is ignored by Boulder
key = jose.JWKRSA.load(
Crypto.PublicKey.RSA.generate(BITS).exportKey(format="PEM"))
acme = client.Client(NEW_REG_URL, key)
regr = acme.register(contact=())
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
acme.update_registration(regr.update(
body=regr.body.update(agreement=regr.terms_of_service)))
logging.debug(regr)
authzr = acme.request_challenges(
identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN),
new_authzr_uri=regr.new_authzr_uri)
logging.debug(authzr)
authzr, authzr_response = acme.poll(authzr)
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'csr.der')),
M2Crypto.X509.FORMAT_DER)
try:
acme.request_issuance(csr, (authzr,))
except messages.Error as error:
print ("This script is doomed to fail as no authorization "
"challenges are ever solved. Error from server: {0}".format(error))
-42
View File
@@ -1,42 +0,0 @@
import logging
import os
import pkg_resources
import M2Crypto
from acme import messages2
from acme import jose
from letsencrypt import network2
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
key = jose.JWKRSA.load(pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
net = network2.Network(NEW_REG_URL, key)
regr = net.register(contact=(
'mailto:cert-admin@example.com', 'tel:+12025551212'))
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
net.update_registration(regr.update(
body=regr.body.update(agreement=regr.terms_of_service)))
logging.debug(regr)
authzr = net.request_challenges(
identifier=messages2.Identifier(
typ=messages2.IDENTIFIER_FQDN, value='example1.com'),
new_authzr_uri=regr.new_authzr_uri)
logging.debug(authzr)
authzr, authzr_response = net.poll(authzr)
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'csr.pem')))
try:
net.request_issuance(csr, (authzr,))
except messages2.Error as error:
print error.detail
+5 -5
View File
@@ -6,7 +6,7 @@ import re
import configobj import configobj
import zope.component import zope.component
from acme import messages2 from acme import messages
from letsencrypt import crypto_util from letsencrypt import crypto_util
from letsencrypt import errors from letsencrypt import errors
@@ -28,7 +28,7 @@ class Account(object):
:ivar str phone: Client's phone number :ivar str phone: Client's phone number
:ivar regr: Registration Resource :ivar regr: Registration Resource
:type regr: :class:`~acme.messages2.RegistrationResource` :type regr: :class:`~acme.messages.RegistrationResource`
""" """
@@ -141,11 +141,11 @@ class Account(object):
if "RegistrationResource" in acc_config: if "RegistrationResource" in acc_config:
acc_config_rr = acc_config["RegistrationResource"] acc_config_rr = acc_config["RegistrationResource"]
regr = messages2.RegistrationResource( regr = messages.RegistrationResource(
uri=acc_config_rr["uri"], uri=acc_config_rr["uri"],
new_authzr_uri=acc_config_rr["new_authzr_uri"], new_authzr_uri=acc_config_rr["new_authzr_uri"],
terms_of_service=acc_config_rr["terms_of_service"], terms_of_service=acc_config_rr["terms_of_service"],
body=messages2.Registration.from_json(acc_config_rr["body"])) body=messages.Registration.from_json(acc_config_rr["body"]))
else: else:
regr = None regr = None
@@ -227,5 +227,5 @@ class Account(object):
if cls.EMAIL_REGEX.match(email): if cls.EMAIL_REGEX.match(email):
return not email.startswith(".") and ".." not in email return not email.startswith(".") and ".." not in email
else: else:
logging.warn("Invalid email address.") logging.warn("Invalid email address: %s.", email)
return False return False
+2 -2
View File
@@ -5,11 +5,11 @@ Please use names such as ``achall`` to distiguish from variables "of type"
and :class:`.ChallengeBody` (denoted by ``challb``):: and :class:`.ChallengeBody` (denoted by ``challb``)::
from acme import challenges from acme import challenges
from acme import messages2 from acme import messages
from letsencrypt import achallenges from letsencrypt import achallenges
chall = challenges.DNS(token='foo') chall = challenges.DNS(token='foo')
challb = messages2.ChallengeBody(chall=chall) challb = messages.ChallengeBody(chall=chall)
achall = achallenges.DNS(chall=challb, domain='example.com') achall = achallenges.DNS(chall=challb, domain='example.com')
Note, that all annotated challenges act as a proxy objects:: Note, that all annotated challenges act as a proxy objects::
+8 -11
View File
@@ -52,10 +52,10 @@ class AugeasConfigurator(common.Plugin):
lens_path = self.aug.get(path + "/lens") lens_path = self.aug.get(path + "/lens")
# As aug.get may return null # As aug.get may return null
if lens_path and lens in lens_path: if lens_path and lens in lens_path:
# Strip off /augeas/files and /error logging.error(
logging.error("There has been an error in parsing the file: %s", "There has been an error in parsing the file (%s): %s",
path[13:len(path) - 6]) # Strip off /augeas/files and /error
logging.error(self.aug.get(path + "/message")) path[13:len(path) - 6], self.aug.get(path + "/message"))
def save(self, title=None, temporary=False): def save(self, title=None, temporary=False):
"""Saves all changes to the configuration files. """Saves all changes to the configuration files.
@@ -122,13 +122,10 @@ class AugeasConfigurator(common.Plugin):
# Check for the root of save problems # Check for the root of save problems
new_errs = self.aug.match("/augeas//error") new_errs = self.aug.match("/augeas//error")
# logging.error("During Save - %s", mod_conf) # logging.error("During Save - %s", mod_conf)
# Only print new errors caused by recent save logging.error("Unable to save files: %s. Attempted Save Notes: %s",
for err in new_errs: ", ".join(err[13:len(err) - 6] for err in new_errs
if err not in ex_errs: # Only new errors caused by recent save
logging.error( if err not in ex_errs), self.save_notes)
"Unable to save file - %s", err[13:len(err) - 6])
logging.error("Attempted Save Notes")
logging.error(self.save_notes)
# Wrapper functions for Reverter class # Wrapper functions for Reverter class
def recovery_routine(self): def recovery_routine(self):
+14 -20
View File
@@ -4,7 +4,7 @@ import logging
import time import time
from acme import challenges from acme import challenges
from acme import messages2 from acme import messages
from letsencrypt import achallenges from letsencrypt import achallenges
from letsencrypt import constants from letsencrypt import constants
@@ -24,13 +24,13 @@ class AuthHandler(object):
:ivar network: Network object for sending and receiving authorization :ivar network: Network object for sending and receiving authorization
messages messages
:type network: :class:`letsencrypt.network2.Network` :type network: :class:`letsencrypt.network.Network`
:ivar account: Client's Account :ivar account: Client's Account
:type account: :class:`letsencrypt.account.Account` :type account: :class:`letsencrypt.account.Account`
:ivar dict authzr: ACME Authorization Resource dict where keys are domains :ivar dict authzr: ACME Authorization Resource dict where keys are domains
and values are :class:`acme.messages2.AuthorizationResource` and values are :class:`acme.messages.AuthorizationResource`
:ivar list dv_c: DV challenges in the form of :ivar list dv_c: DV challenges in the form of
:class:`letsencrypt.achallenges.AnnotatedChallenge` :class:`letsencrypt.achallenges.AnnotatedChallenge`
:ivar list cont_c: Continuity challenges in the :ivar list cont_c: Continuity challenges in the
@@ -82,7 +82,7 @@ class AuthHandler(object):
self.verify_authzr_complete() self.verify_authzr_complete()
# Only return valid authorizations # Only return valid authorizations
return [authzr for authzr in self.authzr.values() return [authzr for authzr in self.authzr.values()
if authzr.body.status == messages2.STATUS_VALID] if authzr.body.status == messages.STATUS_VALID]
def _choose_challenges(self, domains): def _choose_challenges(self, domains):
"""Retrieve necessary challenges to satisfy server.""" """Retrieve necessary challenges to satisfy server."""
@@ -198,7 +198,7 @@ class AuthHandler(object):
failed = [] failed = []
self.authzr[domain], _ = self.network.poll(self.authzr[domain]) self.authzr[domain], _ = self.network.poll(self.authzr[domain])
if self.authzr[domain].body.status == messages2.STATUS_VALID: if self.authzr[domain].body.status == messages.STATUS_VALID:
return achalls, [] return achalls, []
# Note: if the whole authorization is invalid, the individual failed # Note: if the whole authorization is invalid, the individual failed
@@ -207,9 +207,9 @@ class AuthHandler(object):
status = self._get_chall_status(self.authzr[domain], achall) status = self._get_chall_status(self.authzr[domain], achall)
# This does nothing for challenges that have yet to be decided yet. # This does nothing for challenges that have yet to be decided yet.
if status == messages2.STATUS_VALID: if status == messages.STATUS_VALID:
completed.append(achall) completed.append(achall)
elif status == messages2.STATUS_INVALID: elif status == messages.STATUS_INVALID:
failed.append(achall) failed.append(achall)
return completed, failed return completed, failed
@@ -221,7 +221,7 @@ class AuthHandler(object):
each challenge resource. each challenge resource.
:param authzr: Authorization Resource :param authzr: Authorization Resource
:type authzr: :class:`acme.messages2.AuthorizationResource` :type authzr: :class:`acme.messages.AuthorizationResource`
:param achall: Annotated challenge for which to get status :param achall: Annotated challenge for which to get status
:type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge` :type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge`
@@ -279,8 +279,8 @@ class AuthHandler(object):
""" """
for authzr in self.authzr.values(): for authzr in self.authzr.values():
if (authzr.body.status != messages2.STATUS_VALID and if (authzr.body.status != messages.STATUS_VALID and
authzr.body.status != messages2.STATUS_INVALID): authzr.body.status != messages.STATUS_INVALID):
raise errors.AuthorizationError("Incomplete authorizations") raise errors.AuthorizationError("Incomplete authorizations")
def _challenge_factory(self, domain, path): def _challenge_factory(self, domain, path):
@@ -321,7 +321,7 @@ def challb_to_achall(challb, key, domain):
"""Converts a ChallengeBody object to an AnnotatedChallenge. """Converts a ChallengeBody object to an AnnotatedChallenge.
:param challb: ChallengeBody :param challb: ChallengeBody
:type challb: :class:`acme.messages2.ChallengeBody` :type challb: :class:`acme.messages.ChallengeBody`
:param key: Key :param key: Key
:type key: :class:`letsencrypt.le_util.Key` :type key: :class:`letsencrypt.le_util.Key`
@@ -333,28 +333,22 @@ def challb_to_achall(challb, key, domain):
""" """
chall = challb.chall chall = challb.chall
logging.info("%s challenge for %s", chall.typ, domain)
if isinstance(chall, challenges.DVSNI): if isinstance(chall, challenges.DVSNI):
logging.info(" DVSNI challenge for %s.", domain)
return achallenges.DVSNI( return achallenges.DVSNI(
challb=challb, domain=domain, key=key) challb=challb, domain=domain, key=key)
elif isinstance(chall, challenges.SimpleHTTP): elif isinstance(chall, challenges.SimpleHTTP):
logging.info(" SimpleHTTP challenge for %s.", domain)
return achallenges.SimpleHTTP( return achallenges.SimpleHTTP(
challb=challb, domain=domain, key=key) challb=challb, domain=domain, key=key)
elif isinstance(chall, challenges.DNS): elif isinstance(chall, challenges.DNS):
logging.info(" DNS challenge for %s.", domain)
return achallenges.DNS(challb=challb, domain=domain) return achallenges.DNS(challb=challb, domain=domain)
elif isinstance(chall, challenges.RecoveryToken): elif isinstance(chall, challenges.RecoveryToken):
logging.info(" Recovery Token Challenge for %s.", domain)
return achallenges.RecoveryToken(challb=challb, domain=domain) return achallenges.RecoveryToken(challb=challb, domain=domain)
elif isinstance(chall, challenges.RecoveryContact): elif isinstance(chall, challenges.RecoveryContact):
logging.info(" Recovery Contact Challenge for %s.", domain)
return achallenges.RecoveryContact( return achallenges.RecoveryContact(
challb=challb, domain=domain) challb=challb, domain=domain)
elif isinstance(chall, challenges.ProofOfPossession): elif isinstance(chall, challenges.ProofOfPossession):
logging.info(" Proof-of-Possession Challenge for %s", domain)
return achallenges.ProofOfPossession( return achallenges.ProofOfPossession(
challb=challb, domain=domain) challb=challb, domain=domain)
@@ -370,8 +364,8 @@ def gen_challenge_path(challbs, preferences, combinations):
.. todo:: This can be possibly be rewritten to use resolved_combinations. .. todo:: This can be possibly be rewritten to use resolved_combinations.
:param tuple challbs: A tuple of challenges :param tuple challbs: A tuple of challenges
(:class:`acme.messages2.Challenge`) from (:class:`acme.messages.Challenge`) from
:class:`acme.messages2.AuthorizationResource` to be :class:`acme.messages.AuthorizationResource` to be
fulfilled by the client in order to prove possession of the fulfilled by the client in order to prove possession of the
identifier. identifier.
+3 -3
View File
@@ -16,7 +16,7 @@ from letsencrypt import crypto_util
from letsencrypt import errors from letsencrypt import errors
from letsencrypt import interfaces from letsencrypt import interfaces
from letsencrypt import le_util from letsencrypt import le_util
from letsencrypt import network2 from letsencrypt import network
from letsencrypt import reverter from letsencrypt import reverter
from letsencrypt import revoker from letsencrypt import revoker
from letsencrypt import storage from letsencrypt import storage
@@ -29,7 +29,7 @@ class Client(object):
"""ACME protocol client. """ACME protocol client.
:ivar network: Network object for sending and receiving messages :ivar network: Network object for sending and receiving messages
:type network: :class:`letsencrypt.network2.Network` :type network: :class:`letsencrypt.network.Network`
:ivar account: Account object used for registration :ivar account: Account object used for registration
:type account: :class:`letsencrypt.account.Account` :type account: :class:`letsencrypt.account.Account`
@@ -62,7 +62,7 @@ class Client(object):
self.installer = installer self.installer = installer
# TODO: Allow for other alg types besides RS256 # TODO: Allow for other alg types besides RS256
self.network = network2.Network( self.network = network.Network(
config.server, jwk.JWKRSA.load(self.account.key.pem), config.server, jwk.JWKRSA.load(self.account.key.pem),
verify_ssl=(not config.no_verify_ssl)) verify_ssl=(not config.no_verify_ssl))
-5
View File
@@ -87,8 +87,3 @@ REC_TOKEN_DIR = "recovery_tokens"
RENEWER_CONFIG_FILENAME = "renewer.conf" RENEWER_CONFIG_FILENAME = "renewer.conf"
"""Renewer config file name (relative to `IConfig.config_dir`).""" """Renewer config file name (relative to `IConfig.config_dir`)."""
NETSTAT = "/bin/netstat"
"""Location of netstat binary for checking whether a listener is already
running on the specified port (Linux-specific)."""
+1 -1
View File
@@ -40,7 +40,7 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"):
try: try:
key_pem = make_key(key_size) key_pem = make_key(key_size)
except ValueError as err: except ValueError as err:
logging.fatal(str(err)) logging.exception(err)
raise err raise err
# Save file # Save file
-8
View File
@@ -5,14 +5,6 @@ class LetsEncryptClientError(Exception):
"""Generic Let's Encrypt client error.""" """Generic Let's Encrypt client error."""
class NetworkError(LetsEncryptClientError):
"""Network error."""
class UnexpectedUpdate(NetworkError):
"""Unexpected update."""
class LetsEncryptReverterError(LetsEncryptClientError): class LetsEncryptReverterError(LetsEncryptClientError):
"""Let's Encrypt Reverter error.""" """Let's Encrypt Reverter error."""
+1 -2
View File
@@ -148,8 +148,7 @@ class IConfig(zope.interface.Interface):
""" """
server = zope.interface.Attribute( server = zope.interface.Attribute(
"CA hostname (and optionally :port). The server certificate must " "ACME new registration URI (including /acme/new-reg).")
"be trusted in order to avoid further modifications to the client.")
email = zope.interface.Attribute( email = zope.interface.Attribute(
"Email used for registration and recovery contact.") "Email used for registration and recovery contact.")
rsa_key_size = zope.interface.Attribute("Size of the RSA key.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
+18 -113
View File
@@ -1,121 +1,26 @@
"""Network Module.""" """Networking for ACME protocol."""
import logging from acme import client
import sys
import time
import requests
from acme import jose
from acme import messages
from letsencrypt import errors
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning class Network(client.Client):
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() """ACME networking."""
logging.getLogger("requests").setLevel(logging.WARNING) def register_from_account(self, account):
"""Register with server.
.. todo:: this should probably not be a part of network...
class Network(object): :param account: Account
"""Class for communicating with ACME servers. :type account: :class:`letsencrypt.account.Account`
:ivar str server_url: Full URL of the ACME service :returns: Updated account
:rtype: :class:`letsencrypt.account.Account`
"""
def __init__(self, server):
"""Initialize Network instance.
:param str server: ACME (CA) server[:port]
""" """
self.server_url = "https://%s/acme/" % server details = (
"mailto:" + account.email if account.email is not None else None,
def send(self, msg): "tel:" + account.phone if account.phone is not None else None,
"""Send ACME message to server. )
account.regr = self.register(contact=tuple(
:param msg: ACME message. det for det in details if det is not None))
:type msg: :class:`acme.messages.Message` return account
:returns: Server response message.
:rtype: :class:`acme.messages.Message`
:raises acme.errors.ValidationError: if `msg` is not
valid serializable ACME JSON message.
:raises errors.LetsEncryptClientError: in case of connection error
or if response from server is not a valid ACME message.
"""
try:
response = requests.post(
self.server_url,
data=msg.json_dumps(),
headers={"Content-Type": "application/json"},
verify=True
)
except requests.exceptions.RequestException as error:
raise errors.LetsEncryptClientError(
'Sending ACME message to server has failed: %s' % error)
json_string = response.json()
try:
return messages.Message.from_json(json_string)
except jose.DeserializationError as error:
logging.error(json_string)
raise # TODO
def send_and_receive_expected(self, msg, expected):
"""Send ACME message to server and return expected message.
:param msg: ACME message.
:type msg: :class:`acme.Message`
:returns: ACME response message of expected type.
:rtype: :class:`acme.messages.Message`
:raises errors.LetsEncryptClientError: An exception is thrown
"""
response = self.send(msg)
return self.is_expected_msg(response, expected)
def is_expected_msg(self, response, expected, delay=3, rounds=20):
"""Is response expected ACME message?
:param response: ACME response message from server.
:type response: :class:`acme.messages.Message`
:param expected: Expected response type.
:type expected: subclass of :class:`acme.messages.Message`
:param int delay: Number of seconds to delay before next round
in case of ACME "defer" response message.
:param int rounds: Number of resend attempts in case of ACME "defer"
response message.
:returns: ACME response message from server.
:rtype: :class:`acme.messages.Message`
:raises LetsEncryptClientError: if server sent ACME "error" message
"""
for _ in xrange(rounds):
if isinstance(response, expected):
return response
elif isinstance(response, messages.Error):
logging.error("%s", response)
raise errors.LetsEncryptClientError(response.error)
elif isinstance(response, messages.Defer):
logging.info("Waiting for %d seconds...", delay)
time.sleep(delay)
response = self.send(
messages.StatusRequest(token=response.token))
else:
logging.fatal("Received unexpected message")
logging.fatal("Expected: %s", expected)
logging.fatal("Received: %s", response)
sys.exit(33)
logging.error(
"Server has deferred past the max of %d seconds", rounds * delay)
+6 -6
View File
@@ -16,7 +16,6 @@ import tempfile
import Crypto.PublicKey.RSA import Crypto.PublicKey.RSA
import M2Crypto import M2Crypto
from acme import messages
from acme.jose import util as jose_util from acme.jose import util as jose_util
from letsencrypt import errors from letsencrypt import errors
@@ -45,7 +44,9 @@ class Revoker(object):
""" """
def __init__(self, installer, config, no_confirm=False): def __init__(self, installer, config, no_confirm=False):
self.network = network.Network(config.server) # XXX
self.network = network.Network(new_reg_uri=None, key=None, alg=None)
self.installer = installer self.installer = installer
self.config = config self.config = config
self.no_confirm = no_confirm self.no_confirm = no_confirm
@@ -238,6 +239,8 @@ class Revoker(object):
:returns: TODO :returns: TODO
""" """
# XXX | pylint: disable=unused-variable
# These will both have to change in the future away from M2Crypto # These will both have to change in the future away from M2Crypto
# pylint: disable=protected-access # pylint: disable=protected-access
certificate = jose_util.ComparableX509(cert._cert) certificate = jose_util.ComparableX509(cert._cert)
@@ -250,10 +253,7 @@ class Revoker(object):
raise errors.LetsEncryptRevokerError( raise errors.LetsEncryptRevokerError(
"Corrupted backup key file: %s" % cert.backup_key_path) "Corrupted backup key file: %s" % cert.backup_key_path)
# TODO: Catch error associated with already revoked and proceed. return self.network.revoke(cert=None) # XXX
return self.network.send_and_receive_expected(
messages.RevocationRequest.create(certificate=certificate, key=key),
messages.Revocation)
def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use
"""Remove certificate and key. """Remove certificate and key.
+3 -3
View File
@@ -7,7 +7,7 @@ import shutil
import tempfile import tempfile
import unittest import unittest
from acme import messages2 from acme import messages
from letsencrypt import configuration from letsencrypt import configuration
from letsencrypt import errors from letsencrypt import errors
@@ -40,11 +40,11 @@ class AccountTest(unittest.TestCase):
self.key = le_util.Key(key_file, key_pem) self.key = le_util.Key(key_file, key_pem)
self.email = "client@letsencrypt.org" self.email = "client@letsencrypt.org"
self.regr = messages2.RegistrationResource( self.regr = messages.RegistrationResource(
uri="uri", uri="uri",
new_authzr_uri="new_authzr_uri", new_authzr_uri="new_authzr_uri",
terms_of_service="terms_of_service", terms_of_service="terms_of_service",
body=messages2.Registration( body=messages.Registration(
recovery_token="recovery_token", agreement="agreement") recovery_token="recovery_token", agreement="agreement")
) )
+15 -15
View File
@@ -8,7 +8,7 @@ import Crypto.PublicKey.RSA
from acme import challenges from acme import challenges
from acme import jose from acme import jose
from acme import messages2 from acme import messages
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey( KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
@@ -78,19 +78,19 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
"status": status, "status": status,
} }
if status == messages2.STATUS_VALID: if status == messages.STATUS_VALID:
kwargs.update({"validated": datetime.datetime.now()}) kwargs.update({"validated": datetime.datetime.now()})
return messages2.ChallengeBody(**kwargs) # pylint: disable=star-args return messages.ChallengeBody(**kwargs) # pylint: disable=star-args
# Pending ChallengeBody objects # Pending ChallengeBody objects
DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING) DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING)
SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages2.STATUS_PENDING) SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages.STATUS_PENDING)
DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING) DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING)
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING) RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING)
RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING) RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages.STATUS_PENDING)
POP_P = chall_to_challb(POP, messages2.STATUS_PENDING) POP_P = chall_to_challb(POP, messages.STATUS_PENDING)
CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P,
RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P] RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P]
@@ -106,7 +106,7 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
"""Generate an authorization resource. """Generate an authorization resource.
:param authz_status: Status object :param authz_status: Status object
:type authz_status: :class:`acme.messages2.Status` :type authz_status: :class:`acme.messages.Status`
:param list challs: Challenge objects :param list challs: Challenge objects
:param list statuses: status of each challenge object :param list statuses: status of each challenge object
:param bool combos: Whether or not to add combinations :param bool combos: Whether or not to add combinations
@@ -118,13 +118,13 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
for chall, status in itertools.izip(challs, statuses) for chall, status in itertools.izip(challs, statuses)
) )
authz_kwargs = { authz_kwargs = {
"identifier": messages2.Identifier( "identifier": messages.Identifier(
typ=messages2.IDENTIFIER_FQDN, value=domain), typ=messages.IDENTIFIER_FQDN, value=domain),
"challenges": challbs, "challenges": challbs,
} }
if combos: if combos:
authz_kwargs.update({"combinations": gen_combos(challbs)}) authz_kwargs.update({"combinations": gen_combos(challbs)})
if authz_status == messages2.STATUS_VALID: if authz_status == messages.STATUS_VALID:
authz_kwargs.update({ authz_kwargs.update({
"status": authz_status, "status": authz_status,
"expires": datetime.datetime.now() + datetime.timedelta(days=31), "expires": datetime.datetime.now() + datetime.timedelta(days=31),
@@ -135,8 +135,8 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
}) })
# pylint: disable=star-args # pylint: disable=star-args
return messages2.AuthorizationResource( return messages.AuthorizationResource(
uri="https://trusted.ca/new-authz-resource", uri="https://trusted.ca/new-authz-resource",
new_cert_uri="https://trusted.ca/new-cert", new_cert_uri="https://trusted.ca/new-cert",
body=messages2.Authorization(**authz_kwargs) body=messages.Authorization(**authz_kwargs)
) )
+23 -23
View File
@@ -6,11 +6,11 @@ import unittest
import mock import mock
from acme import challenges from acme import challenges
from acme import messages2 from acme import messages
from letsencrypt import errors from letsencrypt import errors
from letsencrypt import le_util from letsencrypt import le_util
from letsencrypt import network2 from letsencrypt import network
from letsencrypt.tests import acme_util from letsencrypt.tests import acme_util
@@ -37,8 +37,8 @@ class ChallengeFactoryTest(unittest.TestCase):
self.dom = "test" self.dom = "test"
self.handler.authzr[self.dom] = acme_util.gen_authzr( self.handler.authzr[self.dom] = acme_util.gen_authzr(
messages2.STATUS_PENDING, self.dom, acme_util.CHALLENGES, messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES,
[messages2.STATUS_PENDING]*6, False) [messages.STATUS_PENDING]*6, False)
def test_all(self): def test_all(self):
cont_c, dv_c = self.handler._challenge_factory(self.dom, range(0, 6)) cont_c, dv_c = self.handler._challenge_factory(self.dom, range(0, 6))
@@ -57,9 +57,9 @@ class ChallengeFactoryTest(unittest.TestCase):
def test_unrecognized(self): def test_unrecognized(self):
self.handler.authzr["failure.com"] = acme_util.gen_authzr( self.handler.authzr["failure.com"] = acme_util.gen_authzr(
messages2.STATUS_PENDING, "failure.com", messages.STATUS_PENDING, "failure.com",
[mock.Mock(chall="chall", typ="unrecognized")], [mock.Mock(chall="chall", typ="unrecognized")],
[messages2.STATUS_PENDING]) [messages.STATUS_PENDING])
self.assertRaises(errors.LetsEncryptClientError, self.assertRaises(errors.LetsEncryptClientError,
self.handler._challenge_factory, "failure.com", [0]) self.handler._challenge_factory, "failure.com", [0])
@@ -86,7 +86,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.mock_dv_auth.perform.side_effect = gen_auth_resp self.mock_dv_auth.perform.side_effect = gen_auth_resp
self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM")) self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM"))
self.mock_net = mock.MagicMock(spec=network2.Network) self.mock_net = mock.MagicMock(spec=network.Network)
self.handler = AuthHandler( self.handler = AuthHandler(
self.mock_dv_auth, self.mock_cont_auth, self.mock_dv_auth, self.mock_cont_auth,
@@ -160,10 +160,10 @@ class GetAuthorizationsTest(unittest.TestCase):
for dom in self.handler.authzr.keys(): for dom in self.handler.authzr.keys():
azr = self.handler.authzr[dom] azr = self.handler.authzr[dom]
self.handler.authzr[dom] = acme_util.gen_authzr( self.handler.authzr[dom] = acme_util.gen_authzr(
messages2.STATUS_VALID, messages.STATUS_VALID,
dom, dom,
[challb.chall for challb in azr.body.challenges], [challb.chall for challb in azr.body.challenges],
[messages2.STATUS_VALID]*len(azr.body.challenges), [messages.STATUS_VALID]*len(azr.body.challenges),
azr.body.combinations) azr.body.combinations)
@@ -182,16 +182,16 @@ class PollChallengesTest(unittest.TestCase):
self.doms = ["0", "1", "2"] self.doms = ["0", "1", "2"]
self.handler.authzr[self.doms[0]] = acme_util.gen_authzr( self.handler.authzr[self.doms[0]] = acme_util.gen_authzr(
messages2.STATUS_PENDING, self.doms[0], messages.STATUS_PENDING, self.doms[0],
acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False)
self.handler.authzr[self.doms[1]] = acme_util.gen_authzr( self.handler.authzr[self.doms[1]] = acme_util.gen_authzr(
messages2.STATUS_PENDING, self.doms[1], messages.STATUS_PENDING, self.doms[1],
acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False)
self.handler.authzr[self.doms[2]] = acme_util.gen_authzr( self.handler.authzr[self.doms[2]] = acme_util.gen_authzr(
messages2.STATUS_PENDING, self.doms[2], messages.STATUS_PENDING, self.doms[2],
acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False) acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False)
self.chall_update = {} self.chall_update = {}
for dom in self.doms: for dom in self.doms:
@@ -205,7 +205,7 @@ class PollChallengesTest(unittest.TestCase):
self.handler._poll_challenges(self.chall_update, False) self.handler._poll_challenges(self.chall_update, False)
for authzr in self.handler.authzr.values(): for authzr in self.handler.authzr.values():
self.assertEqual(authzr.body.status, messages2.STATUS_VALID) self.assertEqual(authzr.body.status, messages.STATUS_VALID)
@mock.patch("letsencrypt.auth_handler.time") @mock.patch("letsencrypt.auth_handler.time")
def test_poll_challenges_failure_best_effort(self, unused_mock_time): def test_poll_challenges_failure_best_effort(self, unused_mock_time):
@@ -213,7 +213,7 @@ class PollChallengesTest(unittest.TestCase):
self.handler._poll_challenges(self.chall_update, True) self.handler._poll_challenges(self.chall_update, True)
for authzr in self.handler.authzr.values(): for authzr in self.handler.authzr.values():
self.assertEqual(authzr.body.status, messages2.STATUS_PENDING) self.assertEqual(authzr.body.status, messages.STATUS_PENDING)
@mock.patch("letsencrypt.auth_handler.time") @mock.patch("letsencrypt.auth_handler.time")
def test_poll_challenges_failure(self, unused_mock_time): def test_poll_challenges_failure(self, unused_mock_time):
@@ -241,10 +241,10 @@ class PollChallengesTest(unittest.TestCase):
# Basically it didn't raise an error and it stopped earlier than # Basically it didn't raise an error and it stopped earlier than
# Making all challenges invalid which would make mock_poll_solve_one # Making all challenges invalid which would make mock_poll_solve_one
# change authzr to invalid # change authzr to invalid
return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_VALID) return self._mock_poll_solve_one_chall(authzr, messages.STATUS_VALID)
def _mock_poll_solve_one_invalid(self, authzr): def _mock_poll_solve_one_invalid(self, authzr):
return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_INVALID) return self._mock_poll_solve_one_chall(authzr, messages.STATUS_INVALID)
def _mock_poll_solve_one_chall(self, authzr, desired_status): def _mock_poll_solve_one_chall(self, authzr, desired_status):
# pylint: disable=no-self-use # pylint: disable=no-self-use
@@ -269,10 +269,10 @@ class PollChallengesTest(unittest.TestCase):
else: else:
status_ = authzr.body.status status_ = authzr.body.status
new_authzr = messages2.AuthorizationResource( new_authzr = messages.AuthorizationResource(
uri=authzr.uri, uri=authzr.uri,
new_cert_uri=authzr.new_cert_uri, new_cert_uri=authzr.new_cert_uri,
body=messages2.Authorization( body=messages.Authorization(
identifier=authzr.body.identifier, identifier=authzr.body.identifier,
challenges=new_challbs, challenges=new_challbs,
combinations=authzr.body.combinations, combinations=authzr.body.combinations,
@@ -429,8 +429,8 @@ def gen_auth_resp(chall_list):
def gen_dom_authzr(domain, unused_new_authzr_uri, challs): def gen_dom_authzr(domain, unused_new_authzr_uri, challs):
"""Generates new authzr for domains.""" """Generates new authzr for domains."""
return acme_util.gen_authzr( return acme_util.gen_authzr(
messages2.STATUS_PENDING, domain, challs, messages.STATUS_PENDING, domain, challs,
[messages2.STATUS_PENDING]*len(challs)) [messages.STATUS_PENDING]*len(challs))
if __name__ == "__main__": if __name__ == "__main__":
+3 -3
View File
@@ -26,14 +26,14 @@ class ClientTest(unittest.TestCase):
self.account = mock.MagicMock(**{"key.pem": KEY}) self.account = mock.MagicMock(**{"key.pem": KEY})
from letsencrypt.client import Client from letsencrypt.client import Client
with mock.patch("letsencrypt.client.network2") as network2: with mock.patch("letsencrypt.client.network") as network:
self.client = Client( self.client = Client(
config=self.config, account_=self.account, dv_auth=None, config=self.config, account_=self.account, dv_auth=None,
installer=None) installer=None)
self.network2 = network2 self.network = network
def test_init_network_verify_ssl(self): def test_init_network_verify_ssl(self):
self.network2.Network.assert_called_once_with( self.network.Network.assert_called_once_with(
mock.ANY, mock.ANY, verify_ssl=True) mock.ANY, mock.ANY, verify_ssl=True)
@mock.patch("letsencrypt.client.zope.component.getUtility") @mock.patch("letsencrypt.client.zope.component.getUtility")
+50
View File
@@ -0,0 +1,50 @@
"""Tests for letsencrypt.network."""
import shutil
import tempfile
import unittest
import mock
from letsencrypt import account
class NetworkTest(unittest.TestCase):
"""Tests for letsencrypt.network.Network."""
def setUp(self):
from letsencrypt.network import Network
self.net = Network(
new_reg_uri=None, key=None, alg=None, verify_ssl=None)
self.config = mock.Mock(accounts_dir=tempfile.mkdtemp())
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
def tearDown(self):
shutil.rmtree(self.config.accounts_dir)
def test_register_from_account(self):
self.net.register = mock.Mock()
acc = account.Account(
self.config, 'key', email='cert-admin@example.com',
phone='+12025551212')
self.net.register_from_account(acc)
self.net.register.assert_called_with(contact=self.contact)
def test_register_from_account_partial_info(self):
self.net.register = mock.Mock()
acc = account.Account(
self.config, 'key', email='cert-admin@example.com')
acc2 = account.Account(self.config, 'key')
self.net.register_from_account(acc)
self.net.register.assert_called_with(
contact=('mailto:cert-admin@example.com',))
self.net.register_from_account(acc2)
self.net.register.assert_called_with(contact=())
if __name__ == '__main__':
unittest.main() # pragma: no cover
@@ -8,7 +8,7 @@ import mock
from acme import challenges from acme import challenges
from acme import jose from acme import jose
from acme import messages2 from acme import messages
from letsencrypt import achallenges from letsencrypt import achallenges
from letsencrypt import proof_of_possession from letsencrypt import proof_of_possession
@@ -48,8 +48,8 @@ class ProofOfPossessionTest(unittest.TestCase):
issuers=(), authorized_for=()) issuers=(), authorized_for=())
chall = challenges.ProofOfPossession( chall = challenges.ProofOfPossession(
alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints)
challb = messages2.ChallengeBody( challb = messages.ChallengeBody(
chall=chall, uri="http://example", status=messages2.STATUS_PENDING) chall=chall, uri="http://example", status=messages.STATUS_PENDING)
self.achall = achallenges.ProofOfPossession( self.achall = achallenges.ProofOfPossession(
challb=challb, domain="example.com") challb=challb, domain="example.com")
@@ -60,8 +60,8 @@ class ProofOfPossessionTest(unittest.TestCase):
issuers=(), authorized_for=()) issuers=(), authorized_for=())
chall = challenges.ProofOfPossession( chall = challenges.ProofOfPossession(
alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints) alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints)
challb = messages2.ChallengeBody( challb = messages.ChallengeBody(
chall=chall, uri="http://example", status=messages2.STATUS_PENDING) chall=chall, uri="http://example", status=messages.STATUS_PENDING)
self.achall = achallenges.ProofOfPossession( self.achall = achallenges.ProofOfPossession(
challb=challb, domain="example.com") challb=challb, domain="example.com")
self.assertEqual(self.proof_of_pos.perform(self.achall), None) self.assertEqual(self.proof_of_pos.perform(self.achall), None)
+6 -6
View File
@@ -63,7 +63,7 @@ class RevokerTest(RevokerBase):
def tearDown(self): def tearDown(self):
shutil.rmtree(self.backup_dir) shutil.rmtree(self.backup_dir)
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") @mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("letsencrypt.revoker.revocation") @mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_key_all(self, mock_display, mock_net): def test_revoke_by_key_all(self, mock_display, mock_net):
mock_display().confirm_revocation.return_value = True mock_display().confirm_revocation.return_value = True
@@ -89,7 +89,7 @@ class RevokerTest(RevokerBase):
self.revoker.revoke_from_key, self.revoker.revoke_from_key,
self.key) self.key)
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") @mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("letsencrypt.revoker.revocation") @mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_wrong_key(self, mock_display, mock_net): def test_revoke_by_wrong_key(self, mock_display, mock_net):
mock_display().confirm_revocation.return_value = True mock_display().confirm_revocation.return_value = True
@@ -105,7 +105,7 @@ class RevokerTest(RevokerBase):
# No revocation went through # No revocation went through
self.assertEqual(mock_net.call_count, 0) self.assertEqual(mock_net.call_count, 0)
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") @mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("letsencrypt.revoker.revocation") @mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_cert(self, mock_display, mock_net): def test_revoke_by_cert(self, mock_display, mock_net):
mock_display().confirm_revocation.return_value = True mock_display().confirm_revocation.return_value = True
@@ -122,7 +122,7 @@ class RevokerTest(RevokerBase):
self.assertEqual(mock_net.call_count, 1) self.assertEqual(mock_net.call_count, 1)
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") @mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("letsencrypt.revoker.revocation") @mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_cert_not_found(self, mock_display, mock_net): def test_revoke_by_cert_not_found(self, mock_display, mock_net):
mock_display().confirm_revocation.return_value = True mock_display().confirm_revocation.return_value = True
@@ -141,7 +141,7 @@ class RevokerTest(RevokerBase):
self.assertEqual(mock_net.call_count, 1) self.assertEqual(mock_net.call_count, 1)
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") @mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("letsencrypt.revoker.revocation") @mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_menu(self, mock_display, mock_net): def test_revoke_by_menu(self, mock_display, mock_net):
mock_display().confirm_revocation.return_value = True mock_display().confirm_revocation.return_value = True
@@ -165,7 +165,7 @@ class RevokerTest(RevokerBase):
self.assertEqual(mock_display.more_info_cert.call_count, 1) self.assertEqual(mock_display.more_info_cert.call_count, 1)
@mock.patch("letsencrypt.revoker.logging") @mock.patch("letsencrypt.revoker.logging")
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected") @mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("letsencrypt.revoker.revocation") @mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log): def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log):
mock_display().confirm_revocation.return_value = True mock_display().confirm_revocation.return_value = True
+8 -13
View File
@@ -187,8 +187,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if not path["cert_path"] or not path["cert_key"]: if not path["cert_path"] or not path["cert_key"]:
# Throw some can't find all of the directives error" # Throw some can't find all of the directives error"
logging.warn( logging.warn(
"Cannot find a cert or key directive in %s", vhost.path) "Cannot find a cert or key directive in %s. "
logging.warn("VirtualHost was not modified") "VirtualHost was not modified", vhost.path)
# Presumably break here so that the virtualhost is not modified # Presumably break here so that the virtualhost is not modified
return False return False
@@ -414,8 +414,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Note: This could be made to also look for ip:443 combo # Note: This could be made to also look for ip:443 combo
# TODO: Need to search only open directives and IfMod mod_ssl.c # TODO: Need to search only open directives and IfMod mod_ssl.c
if len(self.parser.find_dir(parser.case_i("Listen"), "443")) == 0: if len(self.parser.find_dir(parser.case_i("Listen"), "443")) == 0:
logging.debug("No Listen 443 directive found") logging.debug("No Listen 443 directive found. Setting the "
logging.debug("Setting the Apache Server to Listen on port 443") "Apache Server to Listen on port 443")
path = self.parser.add_dir_to_ifmodssl( path = self.parser.add_dir_to_ifmodssl(
parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443") parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443")
self.save_notes += "Added Listen 443 directive to %s\n" % path self.save_notes += "Added Listen 443 directive to %s\n" % path
@@ -929,9 +929,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if proc.returncode != 0: if proc.returncode != 0:
# Enter recovery routine... # Enter recovery routine...
logging.error("Configtest failed") logging.error("Configtest failed\n%s\n%s", stdout, stderr)
logging.error(stdout)
logging.error(stderr)
return False return False
return True return True
@@ -1061,9 +1059,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod):
stdout=open("/dev/null", "w"), stdout=open("/dev/null", "w"),
stderr=open("/dev/null", "w")) stderr=open("/dev/null", "w"))
apache_restart(apache_init_script) apache_restart(apache_init_script)
except (OSError, subprocess.CalledProcessError) as err: except (OSError, subprocess.CalledProcessError):
logging.error("Error enabling mod_%s", mod_name) logging.exception("Error enabling mod_%s", mod_name)
logging.error("Exception: %s", err)
sys.exit(1) sys.exit(1)
@@ -1126,9 +1123,7 @@ def apache_restart(apache_init_script):
if proc.returncode != 0: if proc.returncode != 0:
# Enter recovery routine... # Enter recovery routine...
logging.error("Apache Restart Failed!") logging.error("Apache Restart Failed!\n%s\n%s", stdout, stderr)
logging.error(stdout)
logging.error(stderr)
return False return False
except (OSError, ValueError): except (OSError, ValueError):
+4 -5
View File
@@ -59,10 +59,9 @@ class ApacheDvsni(common.Dvsni):
vhost = self.configurator.choose_vhost(achall.domain) vhost = self.configurator.choose_vhost(achall.domain)
if vhost is None: if vhost is None:
logging.error( logging.error(
"No vhost exists with servername or alias of: %s", "No vhost exists with servername or alias of: %s. "
achall.domain) "No _default_:443 vhost exists. Please specify servernames "
logging.error("No _default_:443 vhost exists") "in the Apache config", achall.domain)
logging.error("Please specify servernames in the Apache config")
return None return None
# TODO - @jdkasten review this code to make sure it makes sense # TODO - @jdkasten review this code to make sure it makes sense
@@ -141,7 +140,7 @@ class ApacheDvsni(common.Dvsni):
""" """
ips = " ".join(str(i) for i in ip_addrs) ips = " ".join(str(i) for i in ip_addrs)
document_root = os.path.join( document_root = os.path.join(
self.configurator.config.config_dir, "dvsni_page/") self.configurator.config.work_dir, "dvsni_page/")
# TODO: Python docs is not clear how mutliline string literal # TODO: Python docs is not clear how mutliline string literal
# newlines are parsed on different platforms. At least on # newlines are parsed on different platforms. At least on
# Linux (Debian sid), when source file uses CRLF, Python still # Linux (Debian sid), when source file uses CRLF, Python still
+5 -11
View File
@@ -132,9 +132,8 @@ class NginxConfigurator(common.Plugin):
vhost.filep, vhost.names) vhost.filep, vhost.names)
except errors.LetsEncryptMisconfigurationError: except errors.LetsEncryptMisconfigurationError:
logging.warn( logging.warn(
"Cannot find a cert or key directive in %s for %s", "Cannot find a cert or key directive in %s for %s. "
vhost.filep, vhost.names) "VirtualHost was not modified.", vhost.filep, vhost.names)
logging.warn("VirtualHost was not modified")
# Presumably break here so that the virtualhost is not modified # Presumably break here so that the virtualhost is not modified
return False return False
@@ -354,9 +353,7 @@ class NginxConfigurator(common.Plugin):
if proc.returncode != 0: if proc.returncode != 0:
# Enter recovery routine... # Enter recovery routine...
logging.error("Config test failed") logging.error("Config test failed\n%s\n%s", stdout, stderr)
logging.error(stdout)
logging.error(stderr)
return False return False
return True return True
@@ -572,14 +569,11 @@ def nginx_restart(nginx_ctl):
if nginx_proc.returncode != 0: if nginx_proc.returncode != 0:
# Enter recovery routine... # Enter recovery routine...
logging.error("Nginx Restart Failed!") logging.error("Nginx Restart Failed!\n%s\n%s", stdout, stderr)
logging.error(stdout)
logging.error(stderr)
return False return False
except (OSError, ValueError): except (OSError, ValueError):
logging.fatal( logging.fatal("Nginx Restart Failed - Please Check the Configuration")
"Nginx Restart Failed - Please Check the Configuration")
sys.exit(1) sys.exit(1)
return True return True
+3 -3
View File
@@ -50,9 +50,9 @@ class NginxDvsni(common.Dvsni):
vhost = self.configurator.choose_vhost(achall.domain) vhost = self.configurator.choose_vhost(achall.domain)
if vhost is None: if vhost is None:
logging.error( logging.error(
"No nginx vhost exists with server_name matching: %s", "No nginx vhost exists with server_name matching: %s. "
"Please specify server_names in the Nginx config.",
achall.domain) achall.domain)
logging.error("Please specify server_names in the Nginx config")
return None return None
for addr in vhost.addrs: for addr in vhost.addrs:
@@ -124,7 +124,7 @@ class NginxDvsni(common.Dvsni):
""" """
document_root = os.path.join( document_root = os.path.join(
self.configurator.config.config_dir, "dvsni_page") self.configurator.config.work_dir, "dvsni_page")
block = [['listen', str(addr)] for addr in addrs] block = [['listen', str(addr)] for addr in addrs]
+5 -5
View File
@@ -5,7 +5,7 @@ import unittest
import mock import mock
from acme import challenges from acme import challenges
from acme import messages2 from acme import messages
from letsencrypt import achallenges from letsencrypt import achallenges
from letsencrypt import errors from letsencrypt import errors
@@ -164,20 +164,20 @@ class NginxConfiguratorTest(util.NginxTest):
# Note: As more challenges are offered this will have to be expanded # Note: As more challenges are offered this will have to be expanded
auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem) auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem)
achall1 = achallenges.DVSNI( achall1 = achallenges.DVSNI(
challb=messages2.ChallengeBody( challb=messages.ChallengeBody(
chall=challenges.DVSNI( chall=challenges.DVSNI(
r="foo", r="foo",
nonce="bar"), nonce="bar"),
uri="https://ca.org/chall0_uri", uri="https://ca.org/chall0_uri",
status=messages2.Status("pending"), status=messages.Status("pending"),
), domain="localhost", key=auth_key) ), domain="localhost", key=auth_key)
achall2 = achallenges.DVSNI( achall2 = achallenges.DVSNI(
challb=messages2.ChallengeBody( challb=messages.ChallengeBody(
chall=challenges.DVSNI( chall=challenges.DVSNI(
r="abc", r="abc",
nonce="def"), nonce="def"),
uri="https://ca.org/chall1_uri", uri="https://ca.org/chall1_uri",
status=messages2.Status("pending"), status=messages.Status("pending"),
), domain="example.com", key=auth_key) ), domain="example.com", key=auth_key)
dvsni_ret_val = [ dvsni_ret_val = [
+62 -1
View File
@@ -28,11 +28,65 @@ meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn)))
readme = read_file(os.path.join(here, 'README.rst')) readme = read_file(os.path.join(here, 'README.rst'))
changes = read_file(os.path.join(here, 'CHANGES.rst')) changes = read_file(os.path.join(here, 'CHANGES.rst'))
# #358: acme, letsencrypt, letsencrypt_apache, letsencrypt_nginx, etc.
# shall be distributed separately. Please make sure to keep the
# dependecy lists up to date: this is being somewhat checked below
# using an assert statement! Separate lists are helpful for OS package
# maintainers. and will make the future migration a lot easier.
acme_install_requires = [
'argparse',
#'letsencrypt' # TODO: uses testdata vectors
'mock',
'pycrypto',
'pyrfc3339',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
'pytz',
'requests',
'werkzeug',
'M2Crypto',
]
letsencrypt_install_requires = [
#'acme',
'argparse',
'ConfigArgParse',
'configobj',
'M2Crypto',
'mock',
'parsedatetime',
'psutil>=2.1.0', # net_connections introduced in 2.1.0
'pycrypto',
# https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions
'PyOpenSSL>=0.15',
'pyrfc3339',
'python-augeas',
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
'pytz',
'requests',
'zope.component',
'zope.interface',
'M2Crypto',
]
letsencrypt_apache_install_requires = [
#'acme',
#'letsencrypt',
'mock',
'python-augeas',
'zope.component',
'zope.interface',
]
letsencrypt_nginx_install_requires = [
#'acme',
#'letsencrypt',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
'mock',
'zope.interface',
]
install_requires = [ install_requires = [
'argparse', 'argparse',
'ConfigArgParse', 'ConfigArgParse',
'configobj', 'configobj',
'jsonschema<2.5.1', # https://github.com/Julian/jsonschema/issues/233
'mock', 'mock',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'parsedatetime', 'parsedatetime',
@@ -55,6 +109,13 @@ install_requires = [
'M2Crypto', 'M2Crypto',
] ]
assert set(install_requires) == set.union(*(set(ireq) for ireq in (
acme_install_requires,
letsencrypt_install_requires,
letsencrypt_apache_install_requires,
letsencrypt_nginx_install_requires
))), "*install_requires don't match up!"
dev_extras = [ dev_extras = [
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289 # Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289
'astroid==1.3.5', 'astroid==1.3.5',