Merge branch 'master' of ssh://github.com/letsencrypt/lets-encrypt-preview into treat_duplicate_as_renewal

This commit is contained in:
Seth Schoen
2015-09-11 00:42:35 -07:00
66 changed files with 417 additions and 177 deletions
+4
View File
@@ -0,0 +1,4 @@
[pep8]
# E265 block comment should start with '# '
# E501 line too long (X > 79 characters)
ignore = E265,E501
+1 -1
View File
@@ -218,7 +218,7 @@ ignore-long-lines=^\s*(# )?<?https?://\S+>?$
single-line-if-stmt=no single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled # List of optional constructs for which whitespace checking is disabled
no-space-check=trailing-comma,dict-separator no-space-check=trailing-comma
# Maximum number of lines in a module # Maximum number of lines in a module
max-module-lines=1250 max-module-lines=1250
+5 -5
View File
@@ -136,7 +136,7 @@ class SimpleHTTPResponseTest(unittest.TestCase):
jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'), jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'),
alg=jose.RS256, key=account_key) alg=jose.RS256, key=account_key)
for bad_resource in (resource.update(tls=True), for bad_resource in (resource.update(tls=True),
resource.update(token=b'x'*20)) resource.update(token=(b'x' * 20)))
) )
for validation in validations: for validation in validations:
self.assertFalse(self.resp_http.check_validation( self.assertFalse(self.resp_http.check_validation(
@@ -320,7 +320,7 @@ class DVSNIResponseTest(unittest.TestCase):
def test_simple_verify_wrong_token(self): def test_simple_verify_wrong_token(self):
msg = self.msg.update(validation=jose.JWS.sign( msg = self.msg.update(validation=jose.JWS.sign(
payload=self.chall.update(token=b'b'*20).json_dumps().encode(), payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(),
key=self.key, alg=jose.RS256)) key=self.key, alg=jose.RS256))
self.assertFalse(msg.simple_verify( self.assertFalse(msg.simple_verify(
self.chall, self.domain, self.key.public_key())) self.chall, self.domain, self.key.public_key()))
@@ -350,9 +350,9 @@ class RecoveryContactTest(unittest.TestCase):
contact='c********n@example.com') contact='c********n@example.com')
self.jmsg = { self.jmsg = {
'type': 'recoveryContact', 'type': 'recoveryContact',
'activationURL' : 'https://example.ca/sendrecovery/a5bd99383fb0', 'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0',
'successURL' : 'https://example.ca/confirmrecovery/bb1b9928932', 'successURL': 'https://example.ca/confirmrecovery/bb1b9928932',
'contact' : 'c********n@example.com', 'contact': 'c********n@example.com',
} }
def test_to_partial_json(self): def test_to_partial_json(self):
+20 -7
View File
@@ -4,6 +4,7 @@ import heapq
import logging import logging
import time import time
import six
from six.moves import http_client # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error
import OpenSSL import OpenSSL
@@ -32,7 +33,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
Clean up raised error types hierarchy, document, and handle (wrap) Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()`. instances of `.DeserializationError` raised in `from_json()`.
:ivar str new_reg_uri: Location of new-reg :ivar messages.Directory directory:
:ivar key: `.JWK` (private) :ivar key: `.JWK` (private)
:ivar alg: `.JWASignature` :ivar alg: `.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates? :ivar bool verify_ssl: Verify SSL certificates?
@@ -43,12 +44,23 @@ class Client(object): # pylint: disable=too-many-instance-attributes
""" """
DER_CONTENT_TYPE = 'application/pkix-cert' DER_CONTENT_TYPE = 'application/pkix-cert'
def __init__(self, new_reg_uri, key, alg=jose.RS256, def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
verify_ssl=True, net=None): net=None):
self.new_reg_uri = new_reg_uri """Initialize.
:param directory: Directory Resource (`.messages.Directory`) or
URI from which the resource will be downloaded.
"""
self.key = key self.key = key
self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net
if isinstance(directory, six.string_types):
self.directory = messages.Directory.from_json(
self.net.get(directory).json())
else:
self.directory = directory
@classmethod @classmethod
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
terms_of_service=None): terms_of_service=None):
@@ -82,7 +94,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
new_reg = messages.NewRegistration() if new_reg is None else new_reg new_reg = messages.NewRegistration() if new_reg is None else new_reg
assert isinstance(new_reg, messages.NewRegistration) assert isinstance(new_reg, messages.NewRegistration)
response = self.net.post(self.new_reg_uri, new_reg) response = self.net.post(self.directory[new_reg], new_reg)
# TODO: handle errors # TODO: handle errors
assert response.status_code == http_client.CREATED assert response.status_code == http_client.CREATED
@@ -441,8 +453,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .ClientError: If revocation is unsuccessful. :raises .ClientError: If revocation is unsuccessful.
""" """
response = self.net.post(messages.Revocation.url(self.new_reg_uri), response = self.net.post(self.directory[messages.Revocation],
messages.Revocation(certificate=cert)) messages.Revocation(certificate=cert),
content_type=None)
if response.status_code != http_client.OK: if response.status_code != http_client.OK:
raise errors.ClientError( raise errors.ClientError(
'Successful revocation must return HTTP OK status') 'Successful revocation must return HTTP OK status')
+19 -4
View File
@@ -33,10 +33,14 @@ class ClientTest(unittest.TestCase):
self.net.post.return_value = self.response self.net.post.return_value = self.response
self.net.get.return_value = self.response self.net.get.return_value = self.response
self.directory = messages.Directory({
messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg',
messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert',
})
from acme.client import Client from acme.client import Client
self.client = Client( self.client = Client(
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg', directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)
key=KEY, alg=jose.RS256, net=self.net)
self.identifier = messages.Identifier( self.identifier = messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='example.com') typ=messages.IDENTIFIER_FQDN, value='example.com')
@@ -72,6 +76,13 @@ class ClientTest(unittest.TestCase):
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 test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import Client
self.client = Client(
directory=uri, key=KEY, alg=jose.RS256, net=self.net)
self.net.get.assert_called_once_with(uri)
def test_register(self): def test_register(self):
# "Instance of 'Field' has no to_json/update member" bug: # "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member # pylint: disable=no-member
@@ -348,8 +359,8 @@ class ClientTest(unittest.TestCase):
def test_revoke(self): def test_revoke(self):
self.client.revoke(self.certr.body) self.client.revoke(self.certr.body)
self.net.post.assert_called_once_with(messages.Revocation.url( self.net.post.assert_called_once_with(
self.client.new_reg_uri), mock.ANY) self.directory[messages.Revocation], mock.ANY, content_type=None)
def test_revoke_bad_status_raises_error(self): def test_revoke_bad_status_raises_error(self):
self.response.status_code = http_client.METHOD_NOT_ALLOWED self.response.status_code = http_client.METHOD_NOT_ALLOWED
@@ -379,11 +390,14 @@ class ClientNetworkTest(unittest.TestCase):
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
def __init__(self, value): def __init__(self, value):
self.value = value self.value = value
def to_partial_json(self): def to_partial_json(self):
return {'foo': self.value} return {'foo': self.value}
@classmethod @classmethod
def from_json(cls, value): def from_json(cls, value):
pass # pragma: no cover pass # pragma: no cover
# pylint: disable=protected-access # pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws( jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce=b'Tg') MockJSONDeSerializable('foo'), nonce=b'Tg')
@@ -487,6 +501,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')] self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')]
self.available_nonces = self.all_nonces[:] self.available_nonces = self.all_nonces[:]
def send_request(*args, **kwargs): def send_request(*args, **kwargs):
# pylint: disable=unused-argument,missing-docstring # pylint: disable=unused-argument,missing-docstring
if self.available_nonces: if self.available_nonces:
+11 -4
View File
@@ -307,6 +307,7 @@ def encode_b64jose(data):
# b64encode produces ASCII characters only # b64encode produces ASCII characters only
return b64.b64encode(data).decode('ascii') return b64.b64encode(data).decode('ascii')
def decode_b64jose(data, size=None, minimum=False): def decode_b64jose(data, size=None, minimum=False):
"""Decode JOSE Base-64 field. """Decode JOSE Base-64 field.
@@ -324,13 +325,14 @@ def decode_b64jose(data, size=None, minimum=False):
except error_cls as error: except error_cls as error:
raise errors.DeserializationError(error) raise errors.DeserializationError(error)
if size is not None and ((not minimum and len(decoded) != size) if size is not None and ((not minimum and len(decoded) != size) or
or (minimum and len(decoded) < size)): (minimum and len(decoded) < size)):
raise errors.DeserializationError( raise errors.DeserializationError(
"Expected at least or exactly {0} bytes".format(size)) "Expected at least or exactly {0} bytes".format(size))
return decoded return decoded
def encode_hex16(value): def encode_hex16(value):
"""Hexlify. """Hexlify.
@@ -340,6 +342,7 @@ def encode_hex16(value):
""" """
return binascii.hexlify(value).decode() return binascii.hexlify(value).decode()
def decode_hex16(value, size=None, minimum=False): def decode_hex16(value, size=None, minimum=False):
"""Decode hexlified field. """Decode hexlified field.
@@ -352,8 +355,8 @@ def decode_hex16(value, size=None, minimum=False):
""" """
value = value.encode() value = value.encode()
if size is not None and ((not minimum and len(value) != size * 2) if size is not None and ((not minimum and len(value) != size * 2) or
or (minimum and len(value) < size * 2)): (minimum and len(value) < size * 2)):
raise errors.DeserializationError() raise errors.DeserializationError()
error_cls = TypeError if six.PY2 else binascii.Error error_cls = TypeError if six.PY2 else binascii.Error
try: try:
@@ -361,6 +364,7 @@ def decode_hex16(value, size=None, minimum=False):
except error_cls as error: except error_cls as error:
raise errors.DeserializationError(error) raise errors.DeserializationError(error)
def encode_cert(cert): def encode_cert(cert):
"""Encode certificate as JOSE Base-64 DER. """Encode certificate as JOSE Base-64 DER.
@@ -371,6 +375,7 @@ def encode_cert(cert):
return encode_b64jose(OpenSSL.crypto.dump_certificate( return encode_b64jose(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert)) OpenSSL.crypto.FILETYPE_ASN1, cert))
def decode_cert(b64der): def decode_cert(b64der):
"""Decode JOSE Base-64 DER-encoded certificate. """Decode JOSE Base-64 DER-encoded certificate.
@@ -384,6 +389,7 @@ def decode_cert(b64der):
except OpenSSL.crypto.Error as error: except OpenSSL.crypto.Error as error:
raise errors.DeserializationError(error) raise errors.DeserializationError(error)
def encode_csr(csr): def encode_csr(csr):
"""Encode CSR as JOSE Base-64 DER. """Encode CSR as JOSE Base-64 DER.
@@ -394,6 +400,7 @@ def encode_csr(csr):
return encode_b64jose(OpenSSL.crypto.dump_certificate_request( return encode_b64jose(OpenSSL.crypto.dump_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, csr)) OpenSSL.crypto.FILETYPE_ASN1, csr))
def decode_csr(b64der): def decode_csr(b64der):
"""Decode JOSE Base-64 DER-encoded CSR. """Decode JOSE Base-64 DER-encoded CSR.
+5
View File
@@ -52,6 +52,7 @@ class FieldTest(unittest.TestCase):
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
def to_partial_json(self): def to_partial_json(self):
return 'foo' # pragma: no cover return 'foo' # pragma: no cover
@classmethod @classmethod
def from_json(cls, jobj): def from_json(cls, jobj):
pass # pragma: no cover pass # pragma: no cover
@@ -93,14 +94,18 @@ class JSONObjectWithFieldsMetaTest(unittest.TestCase):
self.field2 = Field('Baz2') self.field2 = Field('Baz2')
# pylint: disable=invalid-name,missing-docstring,too-few-public-methods # pylint: disable=invalid-name,missing-docstring,too-few-public-methods
# pylint: disable=blacklisted-name # pylint: disable=blacklisted-name
@six.add_metaclass(JSONObjectWithFieldsMeta) @six.add_metaclass(JSONObjectWithFieldsMeta)
class A(object): class A(object):
__slots__ = ('bar',) __slots__ = ('bar',)
baz = self.field baz = self.field
class B(A): class B(A):
pass pass
class C(A): class C(A):
baz = self.field2 baz = self.field2
self.a_cls = A self.a_cls = A
self.b_cls = B self.b_cls = B
self.c_cls = C self.c_cls = C
+5 -4
View File
@@ -294,10 +294,10 @@ class JWS(json_util.JSONObjectWithFields):
# ... it must be in protected # ... it must be in protected
return ( return (
b64.b64encode(self.signature.protected.encode('utf-8')) b64.b64encode(self.signature.protected.encode('utf-8')) +
+ b'.' + b'.' +
b64.b64encode(self.payload) b64.b64encode(self.payload) +
+ b'.' + b'.' +
b64.b64encode(self.signature.signature)) b64.b64encode(self.signature.signature))
@classmethod @classmethod
@@ -345,6 +345,7 @@ class JWS(json_util.JSONObjectWithFields):
signatures=tuple(cls.signature_cls.from_json(sig) signatures=tuple(cls.signature_cls.from_json(sig)
for sig in jobj['signatures'])) for sig in jobj['signatures']))
class CLI(object): class CLI(object):
"""JWS CLI.""" """JWS CLI."""
+60 -15
View File
@@ -1,11 +1,10 @@
"""ACME protocol messages.""" """ACME protocol messages."""
import collections import collections
from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error
from acme import challenges from acme import challenges
from acme import fields from acme import fields
from acme import jose from acme import jose
from acme import util
class Error(jose.JSONObjectWithFields, Exception): class Error(jose.JSONObjectWithFields, Exception):
@@ -128,6 +127,56 @@ class Identifier(jose.JSONObjectWithFields):
value = jose.Field('value') value = jose.Field('value')
class Directory(jose.JSONDeSerializable):
"""Directory."""
_REGISTERED_TYPES = {}
@classmethod
def _canon_key(cls, key):
return getattr(key, 'resource_type', key)
@classmethod
def register(cls, resource_body_cls):
"""Register resource."""
assert resource_body_cls.resource_type not in cls._REGISTERED_TYPES
cls._REGISTERED_TYPES[resource_body_cls.resource_type] = resource_body_cls
return resource_body_cls
def __init__(self, jobj):
canon_jobj = util.map_keys(jobj, self._canon_key)
if not set(canon_jobj).issubset(self._REGISTERED_TYPES):
# TODO: acme-spec is not clear about this: 'It is a JSON
# dictionary, whose keys are the "resource" values listed
# in {{https-requests}}'z
raise ValueError('Wrong directory fields')
# TODO: check that everything is an absolute URL; acme-spec is
# not clear on that
self._jobj = canon_jobj
def __getattr__(self, name):
try:
return self[name.replace('_', '-')]
except KeyError as error:
raise AttributeError(str(error))
def __getitem__(self, name):
try:
return self._jobj[self._canon_key(name)]
except KeyError:
raise KeyError('Directory field not found')
def to_partial_json(self):
return self._jobj
@classmethod
def from_json(cls, jobj):
try:
return cls(jobj)
except ValueError as error:
raise jose.DeserializationError(str(error))
class Resource(jose.JSONObjectWithFields): class Resource(jose.JSONObjectWithFields):
"""ACME Resource. """ACME Resource.
@@ -216,16 +265,20 @@ class Registration(ResourceBody):
"""All emails found in the ``contact`` field.""" """All emails found in the ``contact`` field."""
return self._filter_contact(self.email_prefix) return self._filter_contact(self.email_prefix)
@Directory.register
class NewRegistration(Registration): class NewRegistration(Registration):
"""New registration.""" """New registration."""
resource_type = 'new-reg' resource_type = 'new-reg'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class UpdateRegistration(Registration): class UpdateRegistration(Registration):
"""Update registration.""" """Update registration."""
resource_type = 'reg' resource_type = 'reg'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class RegistrationResource(ResourceWithURI): class RegistrationResource(ResourceWithURI):
"""Registration Resource. """Registration Resource.
@@ -328,11 +381,14 @@ class Authorization(ResourceBody):
return tuple(tuple(self.challenges[idx] for idx in combo) return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations) for combo in self.combinations)
@Directory.register
class NewAuthorization(Authorization): class NewAuthorization(Authorization):
"""New authorization.""" """New authorization."""
resource_type = 'new-authz' resource_type = 'new-authz'
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
class AuthorizationResource(ResourceWithURI): class AuthorizationResource(ResourceWithURI):
"""Authorization Resource. """Authorization Resource.
@@ -344,6 +400,7 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri = jose.Field('new_cert_uri') new_cert_uri = jose.Field('new_cert_uri')
@Directory.register
class CertificateRequest(jose.JSONObjectWithFields): class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request. """ACME new-cert request.
@@ -369,6 +426,7 @@ class CertificateResource(ResourceWithURI):
authzrs = jose.Field('authzrs') authzrs = jose.Field('authzrs')
@Directory.register
class Revocation(jose.JSONObjectWithFields): class Revocation(jose.JSONObjectWithFields):
"""Revocation message. """Revocation message.
@@ -380,16 +438,3 @@ class Revocation(jose.JSONObjectWithFields):
resource = fields.Resource(resource_type) resource = fields.Resource(resource_type)
certificate = jose.Field( certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) '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
def url(cls, base):
"""Get revocation URL.
:param str base: New Registration Resource or server (root) URL.
"""
return urllib_parse.urljoin(base, cls.PATH)
+40 -8
View File
@@ -60,6 +60,7 @@ class ConstantTest(unittest.TestCase):
def setUp(self): def setUp(self):
from acme.messages import _Constant from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {} POSSIBLE_NAMES = {}
@@ -92,6 +93,45 @@ class ConstantTest(unittest.TestCase):
self.assertFalse(self.const_a != const_a_prime) self.assertFalse(self.const_a != const_a_prime)
class DirectoryTest(unittest.TestCase):
"""Tests for acme.messages.Directory."""
def setUp(self):
from acme.messages import Directory
self.dir = Directory({
'new-reg': 'reg',
mock.MagicMock(resource_type='new-cert'): 'cert',
})
def test_init_wrong_key_value_error(self):
from acme.messages import Directory
self.assertRaises(ValueError, Directory, {'foo': 'bar'})
def test_getitem(self):
self.assertEqual('reg', self.dir['new-reg'])
from acme.messages import NewRegistration
self.assertEqual('reg', self.dir[NewRegistration])
self.assertEqual('reg', self.dir[NewRegistration()])
def test_getitem_fails_with_key_error(self):
self.assertRaises(KeyError, self.dir.__getitem__, 'foo')
def test_getattr(self):
self.assertEqual('reg', self.dir.new_reg)
def test_getattr_fails_with_attribute_error(self):
self.assertRaises(AttributeError, self.dir.__getattr__, 'foo')
def test_to_partial_json(self):
self.assertEqual(
self.dir.to_partial_json(), {'new-reg': 'reg', 'new-cert': 'cert'})
def test_from_json_deserialization_error_on_wrong_key(self):
from acme.messages import Directory
self.assertRaises(
jose.DeserializationError, Directory.from_json, {'foo': 'bar'})
class RegistrationTest(unittest.TestCase): class RegistrationTest(unittest.TestCase):
"""Tests for acme.messages.Registration.""" """Tests for acme.messages.Registration."""
@@ -211,7 +251,6 @@ class ChallengeBodyTest(unittest.TestCase):
'detail': 'Unable to communicate with DNS server', 'detail': 'Unable to communicate with DNS server',
} }
def test_to_partial_json(self): def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.challb.to_partial_json()) self.assertEqual(self.jobj_to, self.challb.to_partial_json())
@@ -320,13 +359,6 @@ class CertificateResourceTest(unittest.TestCase):
class RevocationTest(unittest.TestCase): class RevocationTest(unittest.TestCase):
"""Tests for acme.messages.RevocationTest.""" """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.rev = Revocation(certificate=CERT) self.rev = Revocation(certificate=CERT)
+6
View File
@@ -20,12 +20,14 @@ def vector_path(*names):
return pkg_resources.resource_filename( return pkg_resources.resource_filename(
__name__, os.path.join('testdata', *names)) __name__, os.path.join('testdata', *names))
def load_vector(*names): def load_vector(*names):
"""Load contents of a test vector.""" """Load contents of a test vector."""
# luckily, resource_string opens file in binary mode # luckily, resource_string opens file in binary mode
return pkg_resources.resource_string( return pkg_resources.resource_string(
__name__, os.path.join('testdata', *names)) __name__, os.path.join('testdata', *names))
def _guess_loader(filename, loader_pem, loader_der): def _guess_loader(filename, loader_pem, loader_der):
_, ext = os.path.splitext(filename) _, ext = os.path.splitext(filename)
if ext.lower() == '.pem': if ext.lower() == '.pem':
@@ -35,6 +37,7 @@ def _guess_loader(filename, loader_pem, loader_der):
else: # pragma: no cover else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension") raise ValueError("Loader could not be recognized based on extension")
def load_cert(*names): def load_cert(*names):
"""Load certificate.""" """Load certificate."""
loader = _guess_loader( loader = _guess_loader(
@@ -42,6 +45,7 @@ def load_cert(*names):
return jose.ComparableX509(OpenSSL.crypto.load_certificate( return jose.ComparableX509(OpenSSL.crypto.load_certificate(
loader, load_vector(*names))) loader, load_vector(*names)))
def load_csr(*names): def load_csr(*names):
"""Load certificate request.""" """Load certificate request."""
loader = _guess_loader( loader = _guess_loader(
@@ -49,6 +53,7 @@ def load_csr(*names):
return jose.ComparableX509(OpenSSL.crypto.load_certificate_request( return jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
loader, load_vector(*names))) loader, load_vector(*names)))
def load_rsa_private_key(*names): def load_rsa_private_key(*names):
"""Load RSA private key.""" """Load RSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key, loader = _guess_loader(names[-1], serialization.load_pem_private_key,
@@ -56,6 +61,7 @@ def load_rsa_private_key(*names):
return jose.ComparableRSAKey(loader( return jose.ComparableRSAKey(loader(
load_vector(*names), password=None, backend=default_backend())) load_vector(*names), password=None, backend=default_backend()))
def load_pyopenssl_private_key(*names): def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key.""" """Load pyOpenSSL private key."""
loader = _guess_loader( loader = _guess_loader(
+7
View File
@@ -0,0 +1,7 @@
"""ACME utilities."""
import six
def map_keys(dikt, func):
"""Map dictionary keys."""
return dict((func(key), value) for key, value in six.iteritems(dikt))
+16
View File
@@ -0,0 +1,16 @@
"""Tests for acme.util."""
import unittest
class MapKeysTest(unittest.TestCase):
"""Tests for acme.util.map_keys."""
def test_it(self):
from acme.util import map_keys
self.assertEqual({'a': 'b', 'c': 'd'},
map_keys({'a': 'b', 'c': 'd'}, lambda key: key))
self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1))
if __name__ == '__main__':
unittest.main() # pragma: no cover
@@ -84,7 +84,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
description = "Apache Web Server - Alpha" description = "Apache Web Server - Alpha"
@classmethod @classmethod
def add_parser_arguments(cls, add): def add_parser_arguments(cls, add):
add("ctl", default=constants.CLI_DEFAULTS["ctl"], add("ctl", default=constants.CLI_DEFAULTS["ctl"],
@@ -283,7 +282,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.assoc[target_name] = vhost self.assoc[target_name] = vhost
return vhost return vhost
def _find_best_vhost(self, target_name): def _find_best_vhost(self, target_name):
"""Finds the best vhost for a target_name. """Finds the best vhost for a target_name.
@@ -492,7 +490,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
if "ssl_module" not in self.parser.modules: if "ssl_module" not in self.parser.modules:
logger.info("Loading mod_ssl into Apache Server")
self.enable_mod("ssl", temp=temp) self.enable_mod("ssl", temp=temp)
# Check for Listen <port> # Check for Listen <port>
@@ -583,7 +580,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
ssl_vhost = self._create_vhost(vh_p) ssl_vhost = self._create_vhost(vh_p)
self.vhosts.append(ssl_vhost) self.vhosts.append(ssl_vhost)
# NOTE: Searches through Augeas seem to ruin changes to directives # NOTE: Searches through Augeas seem to ruin changes to directives
# The configuration must also be saved before being searched # The configuration must also be saved before being searched
# for the new directives; For these reasons... this is tacked # for the new directives; For these reasons... this is tacked
@@ -794,7 +790,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.PluginError( raise errors.PluginError(
"Let's Encrypt has already enabled redirection") "Let's Encrypt has already enabled redirection")
def _create_redirect_vhost(self, ssl_vhost): def _create_redirect_vhost(self, ssl_vhost):
"""Creates an http_vhost specifically to redirect for the ssl_vhost. """Creates an http_vhost specifically to redirect for the ssl_vhost.
@@ -997,22 +992,41 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
""" """
# Support Debian specific setup # Support Debian specific setup
if (not os.path.isdir(os.path.join(self.parser.root, "mods-available")) avail_path = os.path.join(self.parser.root, "mods-available")
or not os.path.isdir( enabled_path = os.path.join(self.parser.root, "mods-enabled")
os.path.join(self.parser.root, "mods-enabled"))): if not os.path.isdir(avail_path) or not os.path.isdir(enabled_path):
raise errors.NotSupportedError( raise errors.NotSupportedError(
"Unsupported directory layout. You may try to enable mod %s " "Unsupported directory layout. You may try to enable mod %s "
"and try again." % mod_name) "and try again." % mod_name)
deps = _get_mod_deps(mod_name)
# Enable all dependencies
for dep in deps:
if (dep + "_module") not in self.parser.modules:
self._enable_mod_debian(dep, temp)
self._add_parser_mod(dep)
note = "Enabled dependency of %s module - %s" % (mod_name, dep)
if not temp:
self.save_notes += note + os.linesep
logger.debug(note)
# Enable actual module
self._enable_mod_debian(mod_name, temp) self._enable_mod_debian(mod_name, temp)
self.save_notes += "Enabled %s module in Apache" % mod_name self._add_parser_mod(mod_name)
logger.debug("Enabled Apache %s module", mod_name)
if not temp:
self.save_notes += "Enabled %s module in Apache\n" % mod_name
logger.info("Enabled Apache %s module", mod_name)
# Modules can enable additional config files. Variables may be defined # Modules can enable additional config files. Variables may be defined
# within these new configuration sections. # within these new configuration sections.
# Restart is not necessary as DUMP_RUN_CFG uses latest config. # Restart is not necessary as DUMP_RUN_CFG uses latest config.
self.parser.update_runtime_variables(self.conf("ctl")) self.parser.update_runtime_variables(self.conf("ctl"))
def _add_parser_mod(self, mod_name):
"""Shortcut for updating parser modules."""
self.parser.modules.add(mod_name + "_module") self.parser.modules.add(mod_name + "_module")
self.parser.modules.add("mod_" + mod_name + ".c") self.parser.modules.add("mod_" + mod_name + ".c")
@@ -1140,6 +1154,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.parser.init_modules() self.parser.init_modules()
def _get_mod_deps(mod_name):
"""Get known module dependencies.
.. note:: This does not need to be accurate in order for the client to
run. This simply keeps things clean if the user decides to revert
changes.
.. warning:: If all deps are not included, it may cause incorrect parsing
behavior, due to enable_mod's shortcut for updating the parser's
currently defined modules (:method:`.ApacheConfigurator._add_parser_mod`)
This would only present a major problem in extremely atypical
configs that use ifmod for the missing deps.
"""
deps = {
"ssl": ["setenvif", "mime", "socache_shmcb"]
}
return deps.get(mod_name, [])
def apache_restart(apache_init_script): def apache_restart(apache_init_script):
"""Restarts the Apache Server. """Restarts the Apache Server.
+2 -2
View File
@@ -14,8 +14,8 @@ class Addr(common.Addr):
""" """
if isinstance(other, self.__class__): if isinstance(other, self.__class__):
return ((self.tup == other.tup) or return ((self.tup == other.tup) or
(self.tup[0] == other.tup[0] (self.tup[0] == other.tup[0] and
and self.is_wildcard() and other.is_wildcard())) self.is_wildcard() and other.is_wildcard()))
return False return False
def __ne__(self, other): def __ne__(self, other):
@@ -195,8 +195,7 @@ class ApacheParser(object):
self.aug.set(nvh_path + "/arg", args[0]) self.aug.set(nvh_path + "/arg", args[0])
else: else:
for i, arg in enumerate(args): for i, arg in enumerate(args):
self.aug.set("%s/arg[%d]" % (nvh_path, i+1), arg) self.aug.set("%s/arg[%d]" % (nvh_path, i + 1), arg)
def _get_ifmod(self, aug_conf_path, mod): def _get_ifmod(self, aug_conf_path, mod):
"""Returns the path to <IfMod mod> and creates one if it doesn't exist. """Returns the path to <IfMod mod> and creates one if it doesn't exist.
@@ -568,7 +567,7 @@ def case_i(string):
:param str string: string to make case i regex :param str string: string to make case i regex
""" """
return "".join(["["+c.upper()+c.lower()+"]" return "".join(["[" + c.upper() + c.lower() + "]"
if c.isalpha() else c for c in re.escape(string)]) if c.isalpha() else c for c in re.escape(string)])
@@ -56,7 +56,6 @@ class ComplexParserTest(util.ParserTest):
self.assertRaises( self.assertRaises(
errors.PluginError, self.parser.get_arg, matches[0]) errors.PluginError, self.parser.get_arg, matches[0])
def test_basic_ifdefine(self): def test_basic_ifdefine(self):
self.assertEqual(len(self.parser.find_dir("VAR_DIRECTIVE")), 2) self.assertEqual(len(self.parser.find_dir("VAR_DIRECTIVE")), 2)
self.assertEqual(len(self.parser.find_dir("INVALID_VAR_DIRECTIVE")), 0) self.assertEqual(len(self.parser.find_dir("INVALID_VAR_DIRECTIVE")), 0)
@@ -71,7 +70,6 @@ class ComplexParserTest(util.ParserTest):
self.assertEqual( self.assertEqual(
len(self.parser.find_dir("INVALID_NESTED_DIRECTIVE")), 0) len(self.parser.find_dir("INVALID_NESTED_DIRECTIVE")), 0)
def test_load_modules(self): def test_load_modules(self):
"""If only first is found, there is bad variable parsing.""" """If only first is found, there is bad variable parsing."""
self.assertTrue("status_module" in self.parser.modules) self.assertTrue("status_module" in self.parser.modules)
@@ -551,6 +551,7 @@ class TwoVhost80Test(util.ApacheTest):
self.assertRaises( self.assertRaises(
errors.PluginError, errors.PluginError,
self.config.enhance, "letsencrypt.demo", "redirect") self.config.enhance, "letsencrypt.demo", "redirect")
def test_unknown_rewrite2(self): def test_unknown_rewrite2(self):
# Skip the enable mod # Skip the enable mod
self.config.parser.modules.add("rewrite_module") self.config.parser.modules.add("rewrite_module")
@@ -143,7 +143,7 @@ class BasicParserTest(util.ParserTest):
'Group: name="www-data" id=33 not_used\n' 'Group: name="www-data" id=33 not_used\n'
) )
expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443", expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443",
"example_path":"Documents/path"} "example_path": "Documents/path"}
self.parser.update_runtime_variables("ctl") self.parser.update_runtime_variables("ctl")
self.assertEqual(self.parser.variables, expected_vars) self.assertEqual(self.parser.variables, expected_vars)
@@ -11,7 +11,7 @@ from letsencrypt_compatibility_test.configurators.apache import common as apache
# config uses mod_heartbeat or mod_heartmonitor (which aren't installed and # config uses mod_heartbeat or mod_heartmonitor (which aren't installed and
# therefore the config won't be loaded), I believe this isn't a problem # therefore the config won't be loaded), I believe this isn't a problem
# http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html # http://httpd.apache.org/docs/2.4/mod/mod_watchdog.html
STATIC_MODULES = {"core", "so", "http", "mpm_event", "watchdog",} STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"])
SHARED_MODULES = { SHARED_MODULES = {
@@ -31,7 +31,7 @@ SHARED_MODULES = {
"session_cookie", "session_crypto", "session_dbd", "setenvif", "session_cookie", "session_crypto", "session_dbd", "setenvif",
"slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb", "slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb",
"speling", "ssl", "status", "substitute", "unique_id", "userdir", "speling", "ssl", "status", "substitute", "unique_id", "userdir",
"vhost_alias",} "vhost_alias"}
class Proxy(apache_common.Proxy): class Proxy(apache_common.Proxy):
@@ -72,11 +72,10 @@ class Proxy(object):
logger.debug(line) logger.debug(line)
host_config = docker.utils.create_host_config( host_config = docker.utils.create_host_config(
binds={ binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}},
self._temp_dir : {"bind" : self._temp_dir, "mode" : "rw"}},
port_bindings={ port_bindings={
80 : ("127.0.0.1", self.http_port), 80: ("127.0.0.1", self.http_port),
443 : ("127.0.0.1", self.https_port)},) 443: ("127.0.0.1", self.https_port)},)
container = self._docker_client.create_container( container = self._docker_client.create_container(
image_name, command, ports=[80, 443], volumes=self._temp_dir, image_name, command, ports=[80, 443], volumes=self._temp_dir,
host_config=host_config) host_config=host_config)
@@ -30,7 +30,7 @@ tests that the plugin supports are performed.
""" """
PLUGINS = {"apache" : apache24.Proxy} PLUGINS = {"apache": apache24.Proxy}
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -191,7 +191,7 @@ def test_enhancements(plugin, domains):
success = True success = True
for domain in domains: for domain in domains:
verify = functools.partial(validator.Validator().redirect, "localhost", verify = functools.partial(validator.Validator().redirect, "localhost",
plugin.http_port, headers={"Host" : domain}) plugin.http_port, headers={"Host": domain})
if not _try_until_true(verify): if not _try_until_true(verify):
logger.error("Improper redirect for domain %s", domain) logger.error("Improper redirect for domain %s", domain)
success = False success = False
@@ -7,6 +7,7 @@ from pyparsing import (
from pyparsing import stringEnd from pyparsing import stringEnd
from pyparsing import restOfLine from pyparsing import restOfLine
class RawNginxParser(object): class RawNginxParser(object):
# pylint: disable=expression-not-assigned # pylint: disable=expression-not-assigned
"""A class that parses nginx configuration with pyparsing.""" """A class that parses nginx configuration with pyparsing."""
@@ -32,10 +33,10 @@ class RawNginxParser(object):
block = Forward() block = Forward()
block << Group( block << Group(
(Group(key + location_statement) ^ Group(if_statement)) (Group(key + location_statement) ^ Group(if_statement)) +
+ left_bracket left_bracket +
+ Group(ZeroOrMore(Group(comment | assignment) | block)) Group(ZeroOrMore(Group(comment | assignment) | block)) +
+ right_bracket) right_bracket)
script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd
@@ -41,7 +41,6 @@ class DvsniPerformTest(util.NginxTest):
domain="www.example.org", account_key=account_key), domain="www.example.org", account_key=account_key),
] ]
def setUp(self): def setUp(self):
super(DvsniPerformTest, self).setUp() super(DvsniPerformTest, self).setUp()
+9 -8
View File
@@ -244,7 +244,7 @@ class AuthHandler(object):
""" """
for authzr_challb in authzr.body.challenges: for authzr_challb in authzr.body.challenges:
if type(authzr_challb.chall) is type(achall.challb.chall): if type(authzr_challb.chall) is type(achall.challb.chall): # noqa
return authzr_challb return authzr_challb
raise errors.AuthorizationError( raise errors.AuthorizationError(
"Target challenge not found in authorization resource") "Target challenge not found in authorization resource")
@@ -493,26 +493,27 @@ _ERROR_HELP_COMMON = (
_ERROR_HELP = { _ERROR_HELP = {
"connection" : "connection":
_ERROR_HELP_COMMON + " Additionally, please check that your computer " _ERROR_HELP_COMMON + " Additionally, please check that your computer "
"has publicly routable IP address and no firewalls are preventing the " "has publicly routable IP address and no firewalls are preventing the "
"server from communicating with the client.", "server from communicating with the client.",
"dnssec" : "dnssec":
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for " _ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
"your domain, please ensure the signature is valid.", "your domain, please ensure the signature is valid.",
"malformed" : "malformed":
"To fix these errors, please make sure that you did not provide any " "To fix these errors, please make sure that you did not provide any "
"invalid information to the client and try running Let's Encrypt " "invalid information to the client and try running Let's Encrypt "
"again.", "again.",
"serverInternal" : "serverInternal":
"Unfortunately, an error on the ACME server prevented you from completing " "Unfortunately, an error on the ACME server prevented you from completing "
"authorization. Please try again later.", "authorization. Please try again later.",
"tls" : "tls":
_ERROR_HELP_COMMON + " Additionally, please check that you have an up " _ERROR_HELP_COMMON + " Additionally, please check that you have an up "
"to date TLS configuration that allows the server to communicate with " "to date TLS configuration that allows the server to communicate with "
"the Let's Encrypt client.", "the Let's Encrypt client.",
"unauthorized" : _ERROR_HELP_COMMON, "unauthorized": _ERROR_HELP_COMMON,
"unknownHost" : _ERROR_HELP_COMMON,} "unknownHost": _ERROR_HELP_COMMON,
}
def _report_failed_challs(failed_achalls): def _report_failed_challs(failed_achalls):
+25 -13
View File
@@ -19,12 +19,16 @@ import zope.component
import zope.interface.exceptions import zope.interface.exceptions
import zope.interface.verify import zope.interface.verify
from acme import client as acme_client
from acme import jose
import letsencrypt import letsencrypt
from letsencrypt import account from letsencrypt import account
from letsencrypt import configuration from letsencrypt import configuration
from letsencrypt import constants from letsencrypt import constants
from letsencrypt import client from letsencrypt import client
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
@@ -345,16 +349,20 @@ def install(args, config, plugins):
le_client.enhance_config(domains, args.redirect) le_client.enhance_config(domains, args.redirect)
def revoke(args, unused_config, unused_plugins): def revoke(args, config, unused_plugins): # TODO: coop with renewal config
"""Revoke a previously obtained certificate.""" """Revoke a previously obtained certificate."""
if args.cert_path is None and args.key_path is None: if args.key_path is not None: # revocation by cert key
return "At least one of --cert-path or --key-path is required" logger.debug("Revoking %s using cert key %s",
args.cert_path[0], args.key_path[0])
# This depends on the renewal config and cannot be completed yet. acme = acme_client.Client(
zope.component.getUtility(interfaces.IDisplay).notification( config.server, key=jose.JWK.load(args.key_path[1]))
"Revocation is not available with the new Boulder server yet.") else: # revocation by account key
#client.revoke(args.installer, config, plugins, args.no_confirm, logger.debug("Revoking %s using Account Key", args.cert_path[0])
# args.cert_path, args.key_path) acc, _ = _determine_account(args, config)
# pylint: disable=protected-access
acme = client._acme_from_config_key(config, acc.key)
acme.revoke(jose.ComparableX509(crypto_util.pyopenssl_load_certificate(
args.cert_path[1])[0]))
def rollback(args, config, plugins): def rollback(args, config, plugins):
@@ -438,6 +446,7 @@ class SilentParser(object): # pylint: disable=too-few-public-methods
""" """
def __init__(self, parser): def __init__(self, parser):
self.parser = parser self.parser = parser
def add_argument(self, *args, **kwargs): def add_argument(self, *args, **kwargs):
"""Wrap, but silence help""" """Wrap, but silence help"""
kwargs["help"] = argparse.SUPPRESS kwargs["help"] = argparse.SUPPRESS
@@ -473,7 +482,7 @@ class HelpfulArgumentParser(object):
help2 = self.prescan_for_flag("--help", self.help_topics) help2 = self.prescan_for_flag("--help", self.help_topics)
assert max(True, "a") == "a", "Gravity changed direction" assert max(True, "a") == "a", "Gravity changed direction"
help_arg = max(help1, help2) help_arg = max(help1, help2)
if help_arg == True: if help_arg:
# just --help with no topic; avoid argparse altogether # just --help with no topic; avoid argparse altogether
print USAGE print USAGE
sys.exit(0) sys.exit(0)
@@ -653,6 +662,7 @@ def create_parser(plugins, args):
def _create_subparsers(helpful): def _create_subparsers(helpful):
subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND") subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND")
def add_subparser(name, func): # pylint: disable=missing-docstring def add_subparser(name, func): # pylint: disable=missing-docstring
subparser = subparsers.add_parser( subparser = subparsers.add_parser(
name, help=func.__doc__.splitlines()[0], description=func.__doc__) name, help=func.__doc__.splitlines()[0], description=func.__doc__)
@@ -683,14 +693,16 @@ def _create_subparsers(helpful):
"--cert-path", required=True, help="Path to a certificate that " "--cert-path", required=True, help="Path to a certificate that "
"is going to be installed.") "is going to be installed.")
parser_install.add_argument( parser_install.add_argument(
"--key-path", required=True, help="Accompynying private key") "--key-path", required=True, help="Accompanying private key")
parser_install.add_argument( parser_install.add_argument(
"--chain-path", help="Accompanying path to a certificate chain.") "--chain-path", help="Accompanying path to a certificate chain.")
parser_revoke.add_argument( parser_revoke.add_argument(
"--cert-path", type=read_file, help="Revoke a specific certificate.") "--cert-path", type=read_file, help="Revoke a specific certificate.",
required=True)
parser_revoke.add_argument( parser_revoke.add_argument(
"--key-path", type=read_file, "--key-path", type=read_file,
help="Revoke all certs generated by the provided authorized key.") help="Revoke certificate using its accompanying key. Useful if "
"Account Key is lost.")
parser_rollback.add_argument( parser_rollback.add_argument(
"--checkpoints", type=int, metavar="N", "--checkpoints", type=int, metavar="N",
+3 -3
View File
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
def _acme_from_config_key(config, key): def _acme_from_config_key(config, key):
# TODO: Allow for other alg types besides RS256 # TODO: Allow for other alg types besides RS256
return acme_client.Client(new_reg_uri=config.server, key=key, return acme_client.Client(directory=config.server, key=key,
verify_ssl=(not config.no_verify_ssl)) verify_ssl=(not config.no_verify_ssl))
@@ -279,8 +279,8 @@ class Client(object):
:param .RenewableCert cert: Newly issued certificate :param .RenewableCert cert: Newly issued certificate
""" """
if ("autorenew" not in cert.configuration if ("autorenew" not in cert.configuration or
or cert.configuration.as_bool("autorenew")): cert.configuration.as_bool("autorenew")):
if ("autodeploy" not in cert.configuration or if ("autodeploy" not in cert.configuration or
cert.configuration.as_bool("autodeploy")): cert.configuration.as_bool("autodeploy")):
msg = "Automatic renewal and deployment has " msg = "Automatic renewal and deployment has "
+1 -1
View File
@@ -45,7 +45,7 @@ class NamespaceConfig(object):
return (parsed.netloc + parsed.path).replace('/', os.path.sep) return (parsed.netloc + parsed.path).replace('/', os.path.sep)
@property @property
def accounts_dir(self): #pylint: disable=missing-docstring def accounts_dir(self): # pylint: disable=missing-docstring
return os.path.join( return os.path.join(
self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path) self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path)
+1 -1
View File
@@ -16,7 +16,7 @@ CLI_DEFAULTS = dict(
"letsencrypt", "cli.ini"), "letsencrypt", "cli.ini"),
], ],
verbose_count=-(logging.WARNING / 10), verbose_count=-(logging.WARNING / 10),
server="https://acme-staging.api.letsencrypt.org/acme/new-reg", server="https://acme-staging.api.letsencrypt.org/directory",
rsa_key_size=2048, rsa_key_size=2048,
rollback_checkpoints=1, rollback_checkpoints=1,
config_dir="/etc/letsencrypt", config_dir="/etc/letsencrypt",
+1
View File
@@ -205,6 +205,7 @@ def _pyopenssl_load(data, method, types=(
raise errors.Error("Unable to load: {0}".format(",".join( raise errors.Error("Unable to load: {0}".format(",".join(
str(error) for error in openssl_errors))) str(error) for error in openssl_errors)))
def pyopenssl_load_certificate(data): def pyopenssl_load_certificate(data):
"""Load PEM/DER certificate. """Load PEM/DER certificate.
+2 -2
View File
@@ -25,8 +25,8 @@ def choose_plugin(prepared, question):
:rtype: `~.PluginEntryPoint` :rtype: `~.PluginEntryPoint`
""" """
opts = [plugin_ep.description_with_name opts = [plugin_ep.description_with_name +
+ (" [Misconfigured]" if plugin_ep.misconfigured else "") (" [Misconfigured]" if plugin_ep.misconfigured else "")
for plugin_ep in prepared] for plugin_ep in prepared]
while True: while True:
+2 -2
View File
@@ -76,7 +76,7 @@ class NcursesDisplay(object):
"help_label": help_label, "help_label": help_label,
"width": self.width, "width": self.width,
"height": self.height, "height": self.height,
"menu_height": self.height-6, "menu_height": self.height - 6,
} }
# Can accept either tuples or just the actual choices # Can accept either tuples or just the actual choices
@@ -315,7 +315,7 @@ class FileDisplay(object):
if index < 1 or index > len(tags): if index < 1 or index > len(tags):
return [] return []
# Transform indices to appropriate tags # Transform indices to appropriate tags
return [tags[index-1] for index in indices] return [tags[index - 1] for index in indices]
def _print_menu(self, message, choices): def _print_menu(self, message, choices):
"""Print a menu on the screen. """Print a menu on the screen.
+1
View File
@@ -73,6 +73,7 @@ class NoInstallationError(PluginError):
class MisconfigurationError(PluginError): class MisconfigurationError(PluginError):
"""Let's Encrypt Misconfiguration error.""" """Let's Encrypt Misconfiguration error."""
class NotSupportedError(PluginError): class NotSupportedError(PluginError):
"""Let's Encrypt Plugin function not supported error.""" """Let's Encrypt Plugin function not supported error."""
+1 -3
View File
@@ -194,8 +194,7 @@ class IConfig(zope.interface.Interface):
filtered, stripped or sanitized. filtered, stripped or sanitized.
""" """
server = zope.interface.Attribute( server = zope.interface.Attribute("ACME Directory Resource URI.")
"ACME new registration URI (including /acme/new-reg).")
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.")
@@ -440,7 +439,6 @@ class IValidator(zope.interface.Interface):
""" """
def hsts(name): def hsts(name):
"""Verify HSTS header is enabled """Verify HSTS header is enabled
+2
View File
@@ -196,6 +196,8 @@ def safely_remove(path):
# start with a period or have two consecutive periods <- this needs to # start with a period or have two consecutive periods <- this needs to
# be done in addition to the regex # be done in addition to the regex
EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$") EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$")
def safe_email(email): def safe_email(email):
"""Scrub email address before using it.""" """Scrub email address before using it."""
if EMAIL_REGEX.match(email) is not None: if EMAIL_REGEX.match(email) is not None:
+2
View File
@@ -18,6 +18,7 @@ def option_namespace(name):
"""ArgumentParser options namespace (prefix of all options).""" """ArgumentParser options namespace (prefix of all options)."""
return name + "-" return name + "-"
def dest_namespace(name): def dest_namespace(name):
"""ArgumentParser dest namespace (prefix of all destinations).""" """ArgumentParser dest namespace (prefix of all destinations)."""
return name.replace("-", "_") + "_" return name.replace("-", "_") + "_"
@@ -86,6 +87,7 @@ class Plugin(object):
# other # other
class Addr(object): class Addr(object):
r"""Represents an virtual host address. r"""Represents an virtual host address.
+1
View File
@@ -101,6 +101,7 @@ class PluginEntryPointTest(unittest.TestCase):
with mock.patch("letsencrypt.plugins." with mock.patch("letsencrypt.plugins."
"disco.zope.interface") as mock_zope: "disco.zope.interface") as mock_zope:
mock_zope.exceptions = exceptions mock_zope.exceptions = exceptions
def verify_object(iface, obj): # pylint: disable=missing-docstring def verify_object(iface, obj): # pylint: disable=missing-docstring
assert obj is plugin assert obj is plugin
assert iface is iface1 or iface is iface2 or iface is iface3 assert iface is iface1 or iface is iface2 or iface is iface3
+2 -2
View File
@@ -38,7 +38,7 @@ class ManualAuthenticator(common.Plugin):
Make sure your web server displays the following content at Make sure your web server displays the following content at
{uri} before continuing: {uri} before continuing:
{achall.token} {validation}
Content-Type header MUST be set to {ct}. Content-Type header MUST be set to {ct}.
@@ -173,7 +173,7 @@ binary for temporary key/certificate generation.""".replace("\n", "")
raise errors.Error("Couldn't execute manual command") raise errors.Error("Couldn't execute manual command")
else: else:
self._notify_and_wait(self.MESSAGE_TEMPLATE.format( self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
achall=achall, response=response, validation=validation.json_dumps(), response=response,
uri=response.uri(achall.domain, achall.challb.chall), uri=response.uri(achall.domain, achall.challb.chall),
ct=response.CONTENT_TYPE, command=command)) ct=response.CONTENT_TYPE, command=command))
+21 -1
View File
@@ -61,7 +61,27 @@ class ManualAuthenticatorTest(unittest.TestCase):
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430) self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430)
message = mock_stdout.write.mock_calls[0][1][0] message = mock_stdout.write.mock_calls[0][1][0]
self.assertTrue(self.achalls[0].token in message) self.assertEqual(message, """\
Make sure your web server displays the following content at
http://foo.com/.well-known/acme-challenge/ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ before continuing:
{"header": {"alg": "RS256", "jwk": {"e": "AQAB", "kty": "RSA", "n": "rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q"}}, "payload": "eyJ0bHMiOiBmYWxzZSwgInRva2VuIjogIlpYWmhSM2htUVVSek5uQlRVbUl5VEVGMk9VbGFaakUzUkhRemFuVjRSMG9yVUVOME9USjNjaXR2UVEiLCAidHlwZSI6ICJzaW1wbGVIdHRwIn0", "signature": "jFPJFC-2eRyBw7Sl0wyEBhsdvRZtKk8hc6HykEPAiofZlIwdIu76u2xHqMVZWSZdpxwMNUnnawTEAqgMWFydMA"}
Content-Type header MUST be set to application/jose+json.
If you don\'t have HTTP server configured, you can run the following
command on the target server (as root):
mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
cd /tmp/letsencrypt/public_html
echo -n \'{"header": {"alg": "RS256", "jwk": {"e": "AQAB", "kty": "RSA", "n": "rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q"}}, "payload": "eyJ0bHMiOiBmYWxzZSwgInRva2VuIjogIlpYWmhSM2htUVVSek5uQlRVbUl5VEVGMk9VbGFaakUzUkhRemFuVjRSMG9yVUVOME9USjNjaXR2UVEiLCAidHlwZSI6ICJzaW1wbGVIdHRwIn0", "signature": "jFPJFC-2eRyBw7Sl0wyEBhsdvRZtKk8hc6HykEPAiofZlIwdIu76u2xHqMVZWSZdpxwMNUnnawTEAqgMWFydMA"}\' > .well-known/acme-challenge/ZXZhR3hmQURzNnBTUmIyTEF2OUlaZjE3RHQzanV4R0orUEN0OTJ3citvQQ
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
"import BaseHTTPServer, SimpleHTTPServer; \\
SimpleHTTPServer.SimpleHTTPRequestHandler.extensions_map = {\'\': \'application/jose+json\'}; \\
s = BaseHTTPServer.HTTPServer((\'\', 4430), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
s.serve_forever()" \n""")
#self.assertTrue(validation in message)
mock_verify.return_value = False mock_verify.return_value = False
self.assertEqual([None], self.auth.perform(self.achalls)) self.assertEqual([None], self.auth.perform(self.achalls))
@@ -321,10 +321,8 @@ class PerformTest(unittest.TestCase):
self.authenticator.already_listening = mock.Mock(return_value=False) self.authenticator.already_listening = mock.Mock(return_value=False)
result = self.authenticator.perform(self.achalls) result = self.authenticator.perform(self.achalls)
self.assertEqual(len(self.authenticator.tasks), 2) self.assertEqual(len(self.authenticator.tasks), 2)
self.assertTrue( self.assertTrue(self.achall1.token in self.authenticator.tasks)
self.authenticator.tasks.has_key(self.achall1.token)) self.assertTrue(self.achall2.token in self.authenticator.tasks)
self.assertTrue(
self.authenticator.tasks.has_key(self.achall2.token))
self.assertTrue(isinstance(result, list)) self.assertTrue(isinstance(result, list))
self.assertEqual(len(result), 3) self.assertEqual(len(result), 3)
self.assertTrue(isinstance(result[0], challenges.ChallengeResponse)) self.assertTrue(isinstance(result[0], challenges.ChallengeResponse))
@@ -340,10 +338,8 @@ class PerformTest(unittest.TestCase):
self.authenticator.already_listening = mock.Mock(return_value=False) self.authenticator.already_listening = mock.Mock(return_value=False)
result = self.authenticator.perform(self.achalls) result = self.authenticator.perform(self.achalls)
self.assertEqual(len(self.authenticator.tasks), 2) self.assertEqual(len(self.authenticator.tasks), 2)
self.assertTrue( self.assertTrue(self.achall1.token in self.authenticator.tasks)
self.authenticator.tasks.has_key(self.achall1.token)) self.assertTrue(self.achall2.token in self.authenticator.tasks)
self.assertTrue(
self.authenticator.tasks.has_key(self.achall2.token))
self.assertTrue(isinstance(result, list)) self.assertTrue(isinstance(result, list))
self.assertEqual(len(result), 3) self.assertEqual(len(result), 3)
self.assertEqual(result, [None, None, False]) self.assertEqual(result, [None, None, False])
+1 -1
View File
@@ -48,7 +48,7 @@ class Revoker(object):
""" """
def __init__(self, installer, config, no_confirm=False): def __init__(self, installer, config, no_confirm=False):
# XXX # XXX
self.acme = acme_client.Client(new_reg_uri=None, key=None, alg=None) self.acme = acme_client.Client(directory=None, key=None, alg=None)
self.installer = installer self.installer = installer
self.config = config self.config = config
+3 -4
View File
@@ -224,7 +224,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
target = os.readlink(link) target = os.readlink(link)
if not os.path.isabs(target): if not os.path.isabs(target):
target = os.path.join(os.path.dirname(link), target) target = os.path.join(os.path.dirname(link), target)
return target return os.path.abspath(target)
def current_version(self, kind): def current_version(self, kind):
"""Returns numerical version of the specified item. """Returns numerical version of the specified item.
@@ -505,8 +505,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:rtype: bool :rtype: bool
""" """
if ("autorenew" not in self.configuration if ("autorenew" not in self.configuration or
or self.configuration.as_bool("autorenew")): self.configuration.as_bool("autorenew")):
# Consider whether to attempt to autorenew this cert now # Consider whether to attempt to autorenew this cert now
# Renewals on the basis of revocation # Renewals on the basis of revocation
@@ -622,7 +622,6 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
new_config.write() new_config.write()
return cls(new_config, config, cli_config) return cls(new_config, config, cli_config)
def save_successor(self, prior_version, new_cert, new_privkey, new_chain): def save_successor(self, prior_version, new_cert, new_privkey, new_chain):
"""Save new cert and chain as a successor of a prior version. """Save new cert and chain as a successor of a prior version.
+14 -10
View File
@@ -37,7 +37,7 @@ 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(
messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES, messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES,
[messages.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( cont_c, dv_c = self.handler._challenge_factory(
@@ -163,7 +163,7 @@ class GetAuthorizationsTest(unittest.TestCase):
messages.STATUS_VALID, messages.STATUS_VALID,
dom, dom,
[challb.chall for challb in azr.body.challenges], [challb.chall for challb in azr.body.challenges],
[messages.STATUS_VALID]*len(azr.body.challenges), [messages.STATUS_VALID] * len(azr.body.challenges),
azr.body.combinations) azr.body.combinations)
@@ -183,15 +183,15 @@ 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(
messages.STATUS_PENDING, self.doms[0], messages.STATUS_PENDING, self.doms[0],
acme_util.DV_CHALLENGES, [messages.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(
messages.STATUS_PENDING, self.doms[1], messages.STATUS_PENDING, self.doms[1],
acme_util.DV_CHALLENGES, [messages.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(
messages.STATUS_PENDING, self.doms[2], messages.STATUS_PENDING, self.doms[2],
acme_util.DV_CHALLENGES, [messages.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:
@@ -282,6 +282,7 @@ class PollChallengesTest(unittest.TestCase):
) )
return (new_authzr, "response") return (new_authzr, "response")
class GenChallengePathTest(unittest.TestCase): class GenChallengePathTest(unittest.TestCase):
"""Tests for letsencrypt.auth_handler.gen_challenge_path. """Tests for letsencrypt.auth_handler.gen_challenge_path.
@@ -427,26 +428,29 @@ class ReportFailedChallsTest(unittest.TestCase):
from letsencrypt import achallenges from letsencrypt import achallenges
kwargs = { kwargs = {
"chall" : acme_util.SIMPLE_HTTP, "chall": acme_util.SIMPLE_HTTP,
"uri": "uri", "uri": "uri",
"status": messages.STATUS_INVALID, "status": messages.STATUS_INVALID,
"error": messages.Error(typ="tls", detail="detail"), "error": messages.Error(typ="tls", detail="detail"),
} }
self.simple_http = achallenges.SimpleHTTP( self.simple_http = achallenges.SimpleHTTP(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args # pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="example.com", domain="example.com",
account_key="key") account_key="key")
kwargs["chall"] = acme_util.DVSNI kwargs["chall"] = acme_util.DVSNI
self.dvsni_same = achallenges.DVSNI( self.dvsni_same = achallenges.DVSNI(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args # pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="example.com", domain="example.com",
account_key="key") account_key="key")
kwargs["error"] = messages.Error(typ="dnssec", detail="detail") kwargs["error"] = messages.Error(typ="dnssec", detail="detail")
self.dvsni_diff = achallenges.DVSNI( self.dvsni_diff = achallenges.DVSNI(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args # pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="foo.bar", domain="foo.bar",
account_key="key") account_key="key")
@@ -477,7 +481,7 @@ 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(
messages.STATUS_PENDING, domain, challs, messages.STATUS_PENDING, domain, challs,
[messages.STATUS_PENDING]*len(challs)) [messages.STATUS_PENDING] * len(challs))
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -65,7 +65,7 @@ class CLITest(unittest.TestCase):
for args in itertools.chain( for args in itertools.chain(
*(itertools.combinations(flags, r) *(itertools.combinations(flags, r)
for r in xrange(len(flags)))): for r in xrange(len(flags)))):
self._call(['plugins',] + list(args)) self._call(['plugins'] + list(args))
@mock.patch("letsencrypt.cli.sys") @mock.patch("letsencrypt.cli.sys")
def test_handle_exception(self, mock_sys): def test_handle_exception(self, mock_sys):
+1 -1
View File
@@ -70,7 +70,7 @@ class ClientTest(unittest.TestCase):
def test_init_acme_verify_ssl(self): def test_init_acme_verify_ssl(self):
self.acme_client.assert_called_once_with( self.acme_client.assert_called_once_with(
new_reg_uri=mock.ANY, key=mock.ANY, verify_ssl=True) directory=mock.ANY, key=mock.ANY, verify_ssl=True)
def _mock_obtain_certificate(self): def _mock_obtain_certificate(self):
self.client.auth_handler = mock.MagicMock() self.client.auth_handler = mock.MagicMock()
+1 -1
View File
@@ -35,7 +35,7 @@ class PerformTest(unittest.TestCase):
self.assertRaises( self.assertRaises(
errors.ContAuthError, self.auth.perform, [ errors.ContAuthError, self.auth.perform, [
achallenges.DVSNI( achallenges.DVSNI(
challb=None, domain="0", account_key="invalid_key"),]) challb=None, domain="0", account_key="invalid_key")])
def test_chall_pref(self): def test_chall_pref(self):
self.assertEqual( self.assertEqual(
+1
View File
@@ -250,6 +250,7 @@ class GenSSLLabURLs(unittest.TestCase):
self.assertTrue("eff.org" in urls[0]) self.assertTrue("eff.org" in urls[0])
self.assertTrue("umich.edu" in urls[1]) self.assertTrue("umich.edu" in urls[1])
class GenHttpsNamesTest(unittest.TestCase): class GenHttpsNamesTest(unittest.TestCase):
"""Test _gen_https_names.""" """Test _gen_https_names."""
def setUp(self): def setUp(self):
+1 -1
View File
@@ -35,7 +35,7 @@ class NcursesDisplayTest(unittest.TestCase):
"help_label": "", "help_label": "",
"width": display_util.WIDTH, "width": display_util.WIDTH,
"height": display_util.HEIGHT, "height": display_util.HEIGHT,
"menu_height": display_util.HEIGHT-6, "menu_height": display_util.HEIGHT - 6,
} }
@mock.patch("letsencrypt.display.util.dialog.Dialog.msgbox") @mock.patch("letsencrypt.display.util.dialog.Dialog.msgbox")
+3 -2
View File
@@ -1,9 +1,10 @@
"""Tests for letsencrypt.notify.""" """Tests for letsencrypt.notify."""
import mock
import socket import socket
import unittest import unittest
import mock
class NotifyTests(unittest.TestCase): class NotifyTests(unittest.TestCase):
"""Tests for the notifier.""" """Tests for the notifier."""
+1 -1
View File
@@ -24,6 +24,7 @@ def unlink_all(rc_object):
for kind in ALL_FOUR: for kind in ALL_FOUR:
os.unlink(getattr(rc_object, kind)) os.unlink(getattr(rc_object, kind))
def fill_with_sample_data(rc_object): def fill_with_sample_data(rc_object):
"""Put dummy data into all four files of this RenewableCert.""" """Put dummy data into all four files of this RenewableCert."""
for kind in ALL_FOUR: for kind in ALL_FOUR:
@@ -628,7 +629,6 @@ class RenewableCertTests(unittest.TestCase):
# This should fail because the renewal itself appears to fail # This should fail because the renewal itself appears to fail
self.assertFalse(renewer.renew(self.test_rc, 1)) self.assertFalse(renewer.renew(self.test_rc, 1))
@mock.patch("letsencrypt.renewer.notify") @mock.patch("letsencrypt.renewer.notify")
@mock.patch("letsencrypt.storage.RenewableCert") @mock.patch("letsencrypt.storage.RenewableCert")
@mock.patch("letsencrypt.renewer.renew") @mock.patch("letsencrypt.renewer.renew")
+6 -5
View File
@@ -38,15 +38,15 @@ class ValidatorTest(unittest.TestCase):
@mock.patch("letsencrypt.validator.requests.get") @mock.patch("letsencrypt.validator.requests.get")
def test_succesful_redirect(self, mock_get_request): def test_succesful_redirect(self, mock_get_request):
mock_get_request.return_value = create_response( mock_get_request.return_value = create_response(
301, {"location" : "https://test.com"}) 301, {"location": "https://test.com"})
self.assertTrue(self.validator.redirect("test.com")) self.assertTrue(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get") @mock.patch("letsencrypt.validator.requests.get")
def test_redirect_with_headers(self, mock_get_request): def test_redirect_with_headers(self, mock_get_request):
mock_get_request.return_value = create_response( mock_get_request.return_value = create_response(
301, {"location" : "https://test.com"}) 301, {"location": "https://test.com"})
self.assertTrue(self.validator.redirect( self.assertTrue(self.validator.redirect(
"test.com", headers={"Host" : "test.com"})) "test.com", headers={"Host": "test.com"}))
@mock.patch("letsencrypt.validator.requests.get") @mock.patch("letsencrypt.validator.requests.get")
def test_redirect_missing_location(self, mock_get_request): def test_redirect_missing_location(self, mock_get_request):
@@ -56,13 +56,13 @@ class ValidatorTest(unittest.TestCase):
@mock.patch("letsencrypt.validator.requests.get") @mock.patch("letsencrypt.validator.requests.get")
def test_redirect_wrong_status_code(self, mock_get_request): def test_redirect_wrong_status_code(self, mock_get_request):
mock_get_request.return_value = create_response( mock_get_request.return_value = create_response(
201, {"location" : "https://test.com"}) 201, {"location": "https://test.com"})
self.assertFalse(self.validator.redirect("test.com")) self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get") @mock.patch("letsencrypt.validator.requests.get")
def test_redirect_wrong_redirect_code(self, mock_get_request): def test_redirect_wrong_redirect_code(self, mock_get_request):
mock_get_request.return_value = create_response( mock_get_request.return_value = create_response(
303, {"location" : "https://test.com"}) 303, {"location": "https://test.com"})
self.assertFalse(self.validator.redirect("test.com")) self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get") @mock.patch("letsencrypt.validator.requests.get")
@@ -106,6 +106,7 @@ class ValidatorTest(unittest.TestCase):
self.assertRaises( self.assertRaises(
NotImplementedError, self.validator.ocsp_stapling, "test.com") NotImplementedError, self.validator.ocsp_stapling, "test.com")
def create_response(status_code=200, headers=None): def create_response(status_code=200, headers=None):
"""Creates a requests.Response object for testing""" """Creates a requests.Response object for testing"""
response = requests.Response() response = requests.Response()
@@ -87,7 +87,7 @@ def copy_config(server_root, temp_dir):
dir_len = len(os.path.dirname(server_root)) dir_len = len(os.path.dirname(server_root))
for config_path, config_dirs, config_files in os.walk(server_root): for config_path, config_dirs, config_files in os.walk(server_root):
temp_path = os.path.join(temp_dir, config_path[dir_len+1:]) temp_path = os.path.join(temp_dir, config_path[dir_len + 1:])
os.mkdir(temp_path) os.mkdir(temp_path)
copied_all = True copied_all = True
@@ -151,7 +151,7 @@ def safe_config_file(config_file):
empty_or_all_comments = False empty_or_all_comments = False
if line.startswith("-----BEGIN"): if line.startswith("-----BEGIN"):
return False return False
elif not ":" in line: elif ":" not in line:
possible_password_file = False possible_password_file = False
# If file isn't empty or commented out and could be a password file, # If file isn't empty or commented out and could be a password file,
# don't include it in selection. It is safe to include the file if # don't include it in selection. It is safe to include the file if
@@ -234,9 +234,9 @@ def locate_config(apache_ctl):
for line in output.splitlines(): for line in output.splitlines():
# Relevant output lines are of the form: -D DIRECTIVE="VALUE" # Relevant output lines are of the form: -D DIRECTIVE="VALUE"
if "HTTPD_ROOT" in line: if "HTTPD_ROOT" in line:
server_root = line[line.find('"')+1:-1] server_root = line[line.find('"') + 1:-1]
elif "SERVER_CONFIG_FILE" in line: elif "SERVER_CONFIG_FILE" in line:
config_file = line[line.find('"')+1:-1] config_file = line[line.find('"') + 1:-1]
if not (server_root and config_file): if not (server_root and config_file):
sys.exit("Unable to locate Apache configuration. Please run this " sys.exit("Unable to locate Apache configuration. Please run this "
@@ -272,7 +272,7 @@ def get_args():
args.config_file = os.path.abspath(args.config_file) args.config_file = os.path.abspath(args.config_file)
if args.config_file.startswith(args.server_root): if args.config_file.startswith(args.server_root):
args.config_file = args.config_file[len(args.server_root)+1:] args.config_file = args.config_file[len(args.server_root) + 1:]
else: else:
sys.exit("This script expects the Apache configuration file to be " sys.exit("This script expects the Apache configuration file to be "
"inside the server root") "inside the server root")
@@ -141,7 +141,7 @@ class LetsHelpApacheTest(unittest.TestCase):
@mock.patch(_MODULE_NAME + ".subprocess.Popen") @mock.patch(_MODULE_NAME + ".subprocess.Popen")
def test_locate_config(self, mock_popen): def test_locate_config(self, mock_popen):
mock_popen().communicate.side_effect = [ mock_popen().communicate.side_effect = [
OSError, ("bad_output", None), (_COMPILE_SETTINGS, None),] OSError, ("bad_output", None), (_COMPILE_SETTINGS, None)]
self.assertRaises( self.assertRaises(
SystemExit, letshelp_le_apache.locate_config, "ctl") SystemExit, letshelp_le_apache.locate_config, "ctl")
Executable
+12
View File
@@ -0,0 +1,12 @@
#!/bin/sh
pep8 \
setup.py \
acme \
letsencrypt \
letsencrypt-apache \
letsencrypt-nginx \
letsencrypt-compatibility-test \
letshelp-letsencrypt \
|| echo "PEP8 checking failed, but it's ignored in Travis"
# echo exits with 0
+1
View File
@@ -68,6 +68,7 @@ testing_extras = [
'coverage', 'coverage',
'nose', 'nose',
'nosexcover', 'nosexcover',
'pep8',
'tox', 'tox',
] ]
+8 -3
View File
@@ -4,9 +4,7 @@
# instance (see ./boulder-start.sh). # instance (see ./boulder-start.sh).
# #
# Environment variables: # Environment variables:
# SERVER: Passed as "letsencrypt --server" argument. Boulder # SERVER: Passed as "letsencrypt --server" argument.
# monolithic defaults to :4000, AMQP defaults to :4300. This
# script defaults to monolithic.
# #
# Note: this script is called by Boulder integration test suite! # Note: this script is called by Boulder integration test suite!
@@ -54,6 +52,13 @@ do
[ "${dir}/${latest}" = "$live" ] # renewer fails this test [ "${dir}/${latest}" = "$live" ] # renewer fails this test
done done
# revoke by account key
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
# revoke renewed
common revoke --cert-path "$root/conf/live/le1.wtf/cert.pem"
# revoke by cert key
common revoke --cert-path "$root/conf/live/le2.wtf/cert.pem" \
--key-path "$root/conf/live/le2.wtf/privkey.pem"
if type nginx; if type nginx;
then then
+1 -1
View File
@@ -13,7 +13,7 @@ export root store_flags
letsencrypt_test () { letsencrypt_test () {
letsencrypt \ letsencrypt \
--server "${SERVER:-http://localhost:4000/acme/new-reg}" \ --server "${SERVER:-http://localhost:4000/directory}" \
--no-verify-ssl \ --no-verify-ssl \
--dvsni-port 5001 \ --dvsni-port 5001 \
--simple-http-port 5001 \ --simple-http-port 5001 \
+1
View File
@@ -47,6 +47,7 @@ basepython = python2.7
# continue, but tox return code will reflect previous error # continue, but tox return code will reflect previous error
commands = commands =
pip install -r requirements.txt -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt pip install -r requirements.txt -e acme -e .[dev] -e letsencrypt-apache -e letsencrypt-nginx -e letsencrypt-compatibility-test -e letshelp-letsencrypt
./pep8.travis.sh
pylint --rcfile=.pylintrc letsencrypt pylint --rcfile=.pylintrc letsencrypt
pylint --rcfile=.pylintrc acme/acme pylint --rcfile=.pylintrc acme/acme
pylint --rcfile=.pylintrc letsencrypt-apache/letsencrypt_apache pylint --rcfile=.pylintrc letsencrypt-apache/letsencrypt_apache