Merge remote-tracking branch 'origin/master' into cli2

This commit is contained in:
Peter Eckersley
2015-09-15 17:35:36 -07:00
81 changed files with 762 additions and 248 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
# 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
max-module-lines=1250
+21 -6
View File
@@ -1,8 +1,5 @@
language: python
go:
- 1.5
services:
- rabbitmq
- mysql
@@ -10,8 +7,6 @@ services:
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
before_install:
- travis_retry sudo ./bootstrap/ubuntu.sh
- travis_retry sudo apt-get install --no-install-recommends nginx-light openssl
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5)"'
# using separate envs with different TOXENVs creates 4x1 Travis build
@@ -24,14 +19,34 @@ env:
matrix:
- TOXENV=py26 BOULDER_INTEGRATION=1
- TOXENV=py27 BOULDER_INTEGRATION=1
- TOXENV=py33
- TOXENV=py34
- TOXENV=lint
- TOXENV=cover
# make sure simplehttp simple verification works (custom /etc/hosts)
sudo: false # containers
addons:
# make sure simplehttp simple verification works (custom /etc/hosts)
hosts:
- le.wtf
mariadb: "10.0"
apt:
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
- lsb-release
- python
- python-dev
- python-virtualenv
- gcc
- dialog
- libaugeas0
- libssl-dev
- libffi-dev
- ca-certificates
# For letsencrypt-nginx integration testing
- nginx-light
- openssl
# For Boulder integration testing
- rsyslog
install: "travis_retry pip install tox coveralls"
before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh'
+92 -2
View File
@@ -514,10 +514,100 @@ class DNS(DVChallenge):
"""
typ = "dns"
token = jose.Field("token")
LABEL = "_acme-challenge"
"""Label clients prepend to the domain name being validated."""
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
def gen_validation(self, account_key, alg=jose.RS256, **kwargs):
"""Generate validation.
:param .JWK account_key: Private account key.
:param .JWA alg:
:returns: This challenge wrapped in `.JWS`
:rtype: .JWS
"""
return jose.JWS.sign(
payload=self.json_dumps(sort_keys=True).encode('utf-8'),
key=account_key, alg=alg, **kwargs)
def check_validation(self, validation, account_public_key):
"""Check validation.
:param validation
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:rtype: bool
"""
if not validation.verify(key=account_public_key):
return False
try:
return self == self.json_loads(
validation.payload.decode('utf-8'))
except jose.DeserializationError as error:
logger.debug("Checking validation for DNS failed: %s", error)
return False
def gen_response(self, account_key, **kwargs):
"""Generate response.
:param .JWK account_key: Private account key.
:param .JWA alg:
:rtype: DNSResponse
"""
return DNSResponse(validation=self.gen_validation(
self, account_key, **kwargs))
def validation_domain_name(self, name):
"""Domain name for TXT validation record.
:param unicode name: Domain name being validated.
"""
return "{0}.{1}".format(self.LABEL, name)
@ChallengeResponse.register
class DNSResponse(ChallengeResponse):
"""ACME "dns" challenge response."""
"""ACME "dns" challenge response.
:param JWS validation:
"""
typ = "dns"
validation = jose.Field("validation", decoder=jose.JWS.from_json)
def check_validation(self, chall, account_public_key):
"""Check validation.
:param challenges.DNS chall:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:rtype: bool
"""
return chall.check_validation(self.validation, account_public_key)
+78 -15
View File
@@ -136,7 +136,7 @@ class SimpleHTTPResponseTest(unittest.TestCase):
jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'),
alg=jose.RS256, key=account_key)
for bad_resource in (resource.update(tls=True),
resource.update(token=b'x'*20))
resource.update(token=(b'x' * 20)))
)
for validation in validations:
self.assertFalse(self.resp_http.check_validation(
@@ -144,7 +144,7 @@ class SimpleHTTPResponseTest(unittest.TestCase):
account_public_key=account_key.public_key()))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_good_token(self, mock_get):
def test_simple_verify_good_validation(self, mock_get):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
for resp in self.resp_http, self.resp_https:
mock_get.reset_mock()
@@ -156,9 +156,9 @@ class SimpleHTTPResponseTest(unittest.TestCase):
"local", self.chall), verify=False)
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_token(self, mock_get):
def test_simple_verify_bad_validation(self, mock_get):
mock_get.return_value = mock.MagicMock(
text=self.chall.token + "!", headers=self.good_headers)
text="!", headers=self.good_headers)
self.assertFalse(self.resp_http.simple_verify(
self.chall, "local", None))
@@ -320,7 +320,7 @@ class DVSNIResponseTest(unittest.TestCase):
def test_simple_verify_wrong_token(self):
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))
self.assertFalse(msg.simple_verify(
self.chall, self.domain, self.key.public_key()))
@@ -350,9 +350,9 @@ class RecoveryContactTest(unittest.TestCase):
contact='c********n@example.com')
self.jmsg = {
'type': 'recoveryContact',
'activationURL' : 'https://example.ca/sendrecovery/a5bd99383fb0',
'successURL' : 'https://example.ca/confirmrecovery/bb1b9928932',
'contact' : 'c********n@example.com',
'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0',
'successURL': 'https://example.ca/confirmrecovery/bb1b9928932',
'contact': 'c********n@example.com',
}
def test_to_partial_json(self):
@@ -570,9 +570,15 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
class DNSTest(unittest.TestCase):
def setUp(self):
self.account_key = jose.JWKRSA.load(
test_util.load_vector('rsa512_key.pem'))
from acme.challenges import DNS
self.msg = DNS(token='17817c66b60ce2e4012dfad92657527a')
self.jmsg = {'type': 'dns', 'token': '17817c66b60ce2e4012dfad92657527a'}
self.msg = DNS(token=jose.b64decode(
b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))
self.jmsg = {
'type': 'dns',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
@@ -585,27 +591,84 @@ class DNSTest(unittest.TestCase):
from acme.challenges import DNS
hash(DNS.from_json(self.jmsg))
def test_gen_check_validation(self):
self.assertTrue(self.msg.check_validation(
self.msg.gen_validation(self.account_key),
self.account_key.public_key()))
def test_gen_check_validation_wrong_key(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))
self.assertFalse(self.msg.check_validation(
self.msg.gen_validation(self.account_key), key2.public_key()))
def test_check_validation_wrong_payload(self):
validations = tuple(
jose.JWS.sign(payload=payload, alg=jose.RS256, key=self.account_key)
for payload in (b'', b'{}')
)
for validation in validations:
self.assertFalse(self.msg.check_validation(
validation, self.account_key.public_key()))
def test_check_validation_wrong_fields(self):
bad_validation = jose.JWS.sign(
payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'),
alg=jose.RS256, key=self.account_key)
self.assertFalse(self.msg.check_validation(
bad_validation, self.account_key.public_key()))
def test_gen_response(self):
with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen:
mock_gen.return_value = mock.sentinel.validation
response = self.msg.gen_response(self.account_key)
from acme.challenges import DNSResponse
self.assertTrue(isinstance(response, DNSResponse))
self.assertEqual(response.validation, mock.sentinel.validation)
def test_validation_domain_name(self):
self.assertEqual(
'_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf'))
class DNSResponseTest(unittest.TestCase):
def setUp(self):
self.key = jose.JWKRSA(key=KEY)
from acme.challenges import DNS
self.chall = DNS(token=jose.b64decode(
b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"))
self.validation = jose.JWS.sign(
payload=self.chall.json_dumps(sort_keys=True).encode(),
key=self.key, alg=jose.RS256)
from acme.challenges import DNSResponse
self.msg = DNSResponse()
self.jmsg = {
self.msg = DNSResponse(validation=self.validation)
self.jmsg_to = {
'resource': 'challenge',
'type': 'dns',
'validation': self.validation,
}
self.jmsg_from = {
'resource': 'challenge',
'type': 'dns',
'validation': self.validation.to_json(),
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import DNSResponse
self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg))
self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import DNSResponse
hash(DNSResponse.from_json(self.jmsg))
hash(DNSResponse.from_json(self.jmsg_from))
def test_check_validation(self):
self.assertTrue(
self.msg.check_validation(self.chall, self.key.public_key()))
if __name__ == '__main__':
+24 -10
View File
@@ -4,11 +4,12 @@ import heapq
import logging
import time
import six
from six.moves import http_client # pylint: disable=import-error
import OpenSSL
import requests
import six
import sys
import werkzeug
from acme import errors
@@ -19,8 +20,9 @@ from acme import messages
logger = logging.getLogger(__name__)
# Python does not validate certificates by default before version 2.7.9
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if six.PY2:
if sys.version_info < (2, 7, 9): # pragma: no cover
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
@@ -31,7 +33,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
Clean up raised error types hierarchy, document, and handle (wrap)
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 alg: `.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates?
@@ -42,12 +44,23 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
DER_CONTENT_TYPE = 'application/pkix-cert'
def __init__(self, new_reg_uri, key, alg=jose.RS256,
verify_ssl=True, net=None):
self.new_reg_uri = new_reg_uri
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
net=None):
"""Initialize.
:param directory: Directory Resource (`.messages.Directory`) or
URI from which the resource will be downloaded.
"""
self.key = key
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
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
terms_of_service=None):
@@ -81,7 +94,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
new_reg = messages.NewRegistration() if new_reg is None else new_reg
assert isinstance(new_reg, messages.NewRegistration)
response = self.net.post(self.new_reg_uri, new_reg)
response = self.net.post(self.directory[new_reg], new_reg)
# TODO: handle errors
assert response.status_code == http_client.CREATED
@@ -440,8 +453,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .ClientError: If revocation is unsuccessful.
"""
response = self.net.post(messages.Revocation.url(self.new_reg_uri),
messages.Revocation(certificate=cert))
response = self.net.post(self.directory[messages.Revocation],
messages.Revocation(certificate=cert),
content_type=None)
if response.status_code != http_client.OK:
raise errors.ClientError(
'Successful revocation must return HTTP OK status')
@@ -559,7 +573,7 @@ class ClientNetwork(object):
"""Send HEAD request without checking the response.
Note, that `_check_response` is not called, as it is expected
that status code other than successfuly 2xx will be returned, or
that status code other than successfully 2xx will be returned, or
messages2.Error will be raised by the server.
"""
+25 -8
View File
@@ -33,10 +33,14 @@ class ClientTest(unittest.TestCase):
self.net.post.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
self.client = Client(
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
key=KEY, alg=jose.RS256, net=self.net)
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)
self.identifier = messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='example.com')
@@ -55,7 +59,8 @@ class ClientTest(unittest.TestCase):
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
challb = messages.ChallengeBody(
uri=(authzr_uri + '/1'), status=messages.STATUS_VALID,
chall=challenges.DNS(token='foo'))
chall=challenges.DNS(token=jose.b64decode(
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')))
self.challr = messages.ChallengeResource(
body=challb, authzr_uri=authzr_uri)
self.authz = messages.Authorization(
@@ -72,6 +77,13 @@ class ClientTest(unittest.TestCase):
uri='https://www.letsencrypt-demo.org/acme/cert/1',
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):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
@@ -155,7 +167,7 @@ class ClientTest(unittest.TestCase):
self.response.links['up'] = {'url': self.challr.authzr_uri}
self.response.json.return_value = self.challr.body.to_json()
chall_response = challenges.DNSResponse()
chall_response = challenges.DNSResponse(validation=None)
self.client.answer_challenge(self.challr.body, chall_response)
@@ -164,8 +176,9 @@ class ClientTest(unittest.TestCase):
self.challr.body.update(uri='foo'), chall_response)
def test_answer_challenge_missing_next(self):
self.assertRaises(errors.ClientError, self.client.answer_challenge,
self.challr.body, challenges.DNSResponse())
self.assertRaises(
errors.ClientError, self.client.answer_challenge,
self.challr.body, challenges.DNSResponse(validation=None))
def test_retry_after_date(self):
self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT'
@@ -348,8 +361,8 @@ class ClientTest(unittest.TestCase):
def test_revoke(self):
self.client.revoke(self.certr.body)
self.net.post.assert_called_once_with(messages.Revocation.url(
self.client.new_reg_uri), mock.ANY)
self.net.post.assert_called_once_with(
self.directory[messages.Revocation], mock.ANY, content_type=None)
def test_revoke_bad_status_raises_error(self):
self.response.status_code = http_client.METHOD_NOT_ALLOWED
@@ -379,11 +392,14 @@ class ClientNetworkTest(unittest.TestCase):
# pylint: disable=missing-docstring
def __init__(self, value):
self.value = value
def to_partial_json(self):
return {'foo': self.value}
@classmethod
def from_json(cls, value):
pass # pragma: no cover
# pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce=b'Tg')
@@ -487,6 +503,7 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')]
self.available_nonces = self.all_nonces[:]
def send_request(*args, **kwargs):
# pylint: disable=unused-argument,missing-docstring
if self.available_nonces:
+5 -4
View File
@@ -55,10 +55,11 @@ class ServeProbeSNITest(unittest.TestCase):
def test_probe_not_recognized_name(self):
self.assertRaises(errors.Error, self._probe, b'bar')
def test_probe_connection_error(self):
self._probe(b'foo')
time.sleep(1) # TODO: avoid race conditions in other way
self.assertRaises(errors.Error, self._probe, b'bar')
# TODO: py33/py34 tox hangs forever on do_hendshake in second probe
#def probe_connection_error(self):
# self._probe(b'foo')
# #time.sleep(1) # TODO: avoid race conditions in other way
# self.assertRaises(errors.Error, self._probe, b'bar')
class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
+1 -1
View File
@@ -41,7 +41,7 @@ class JSONDeSerializable(object):
be encoded into a JSON document. **Full serialization** produces
a Python object composed of only basic types as required by the
:ref:`conversion table <conversion-table>`. **Partial
serialization** (acomplished by :meth:`to_partial_json`)
serialization** (accomplished by :meth:`to_partial_json`)
produces a Python object that might also be built from other
:class:`JSONDeSerializable` objects.
+11 -4
View File
@@ -307,6 +307,7 @@ def encode_b64jose(data):
# b64encode produces ASCII characters only
return b64.b64encode(data).decode('ascii')
def decode_b64jose(data, size=None, minimum=False):
"""Decode JOSE Base-64 field.
@@ -324,13 +325,14 @@ def decode_b64jose(data, size=None, minimum=False):
except error_cls as error:
raise errors.DeserializationError(error)
if size is not None and ((not minimum and len(decoded) != size)
or (minimum and len(decoded) < size)):
if size is not None and ((not minimum and len(decoded) != size) or
(minimum and len(decoded) < size)):
raise errors.DeserializationError(
"Expected at least or exactly {0} bytes".format(size))
return decoded
def encode_hex16(value):
"""Hexlify.
@@ -340,6 +342,7 @@ def encode_hex16(value):
"""
return binascii.hexlify(value).decode()
def decode_hex16(value, size=None, minimum=False):
"""Decode hexlified field.
@@ -352,8 +355,8 @@ def decode_hex16(value, size=None, minimum=False):
"""
value = value.encode()
if size is not None and ((not minimum and len(value) != size * 2)
or (minimum and len(value) < size * 2)):
if size is not None and ((not minimum and len(value) != size * 2) or
(minimum and len(value) < size * 2)):
raise errors.DeserializationError()
error_cls = TypeError if six.PY2 else binascii.Error
try:
@@ -361,6 +364,7 @@ def decode_hex16(value, size=None, minimum=False):
except error_cls as error:
raise errors.DeserializationError(error)
def encode_cert(cert):
"""Encode certificate as JOSE Base-64 DER.
@@ -371,6 +375,7 @@ def encode_cert(cert):
return encode_b64jose(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert))
def decode_cert(b64der):
"""Decode JOSE Base-64 DER-encoded certificate.
@@ -384,6 +389,7 @@ def decode_cert(b64der):
except OpenSSL.crypto.Error as error:
raise errors.DeserializationError(error)
def encode_csr(csr):
"""Encode CSR as JOSE Base-64 DER.
@@ -394,6 +400,7 @@ def encode_csr(csr):
return encode_b64jose(OpenSSL.crypto.dump_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, csr))
def decode_csr(b64der):
"""Decode JOSE Base-64 DER-encoded CSR.
+5
View File
@@ -52,6 +52,7 @@ class FieldTest(unittest.TestCase):
# pylint: disable=missing-docstring
def to_partial_json(self):
return 'foo' # pragma: no cover
@classmethod
def from_json(cls, jobj):
pass # pragma: no cover
@@ -93,14 +94,18 @@ class JSONObjectWithFieldsMetaTest(unittest.TestCase):
self.field2 = Field('Baz2')
# pylint: disable=invalid-name,missing-docstring,too-few-public-methods
# pylint: disable=blacklisted-name
@six.add_metaclass(JSONObjectWithFieldsMeta)
class A(object):
__slots__ = ('bar',)
baz = self.field
class B(A):
pass
class C(A):
baz = self.field2
self.a_cls = A
self.b_cls = B
self.c_cls = C
+2 -2
View File
@@ -21,7 +21,7 @@ from acme.jose import jwk
logger = logging.getLogger(__name__)
class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method
class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method
# pylint: disable=too-few-public-methods
# for some reason disable=abstract-method has to be on the line
# above...
@@ -159,7 +159,7 @@ class _JWAES(JWASignature): # pylint: disable=abstract-class-not-used
def sign(self, key, msg): # pragma: no cover
raise NotImplementedError()
def verify(self, key, msg, sig): # pragma: no cover
def verify(self, key, msg, sig): # pragma: no cover
raise NotImplementedError()
+1 -1
View File
@@ -231,7 +231,7 @@ class JWKRSA(JWK):
'n': numbers.n,
'e': numbers.e,
}
else: # rsa.RSAPrivateKey
else: # rsa.RSAPrivateKey
private = self.key.private_numbers()
public = self.key.public_key().public_numbers()
params = {
+6 -5
View File
@@ -53,7 +53,7 @@ class Header(json_util.JSONObjectWithFields):
.. warning:: This class does not support any extensions through
the "crit" (Critical) Header Parameter (4.1.11) and as a
conforming implementation, :meth:`from_json` treats its
occurence as an error. Please subclass if you seek for
occurrence as an error. Please subclass if you seek for
a different behaviour.
:ivar x5tS256: "x5t#S256"
@@ -294,10 +294,10 @@ class JWS(json_util.JSONObjectWithFields):
# ... it must be in protected
return (
b64.b64encode(self.signature.protected.encode('utf-8'))
+ b'.' +
b64.b64encode(self.payload)
+ b'.' +
b64.b64encode(self.signature.protected.encode('utf-8')) +
b'.' +
b64.b64encode(self.payload) +
b'.' +
b64.b64encode(self.signature.signature))
@classmethod
@@ -345,6 +345,7 @@ class JWS(json_util.JSONObjectWithFields):
signatures=tuple(cls.signature_cls.from_json(sig)
for sig in jobj['signatures']))
class CLI(object):
"""JWS CLI."""
+2 -2
View File
@@ -107,8 +107,8 @@ class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods
"""Wrapper for `cryptography` RSA keys.
Wraps around:
- `cryptography.hazmat.primitives.assymetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.assymetric.RSAPublicKey`
- `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.asymmetric.RSAPublicKey`
"""
+60 -15
View File
@@ -1,11 +1,10 @@
"""ACME protocol messages."""
import collections
from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error
from acme import challenges
from acme import fields
from acme import jose
from acme import util
class Error(jose.JSONObjectWithFields, Exception):
@@ -128,6 +127,56 @@ class Identifier(jose.JSONObjectWithFields):
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):
"""ACME Resource.
@@ -216,16 +265,20 @@ class Registration(ResourceBody):
"""All emails found in the ``contact`` field."""
return self._filter_contact(self.email_prefix)
@Directory.register
class NewRegistration(Registration):
"""New registration."""
resource_type = 'new-reg'
resource = fields.Resource(resource_type)
class UpdateRegistration(Registration):
"""Update registration."""
resource_type = 'reg'
resource = fields.Resource(resource_type)
class RegistrationResource(ResourceWithURI):
"""Registration Resource.
@@ -328,11 +381,14 @@ class Authorization(ResourceBody):
return tuple(tuple(self.challenges[idx] for idx in combo)
for combo in self.combinations)
@Directory.register
class NewAuthorization(Authorization):
"""New authorization."""
resource_type = 'new-authz'
resource = fields.Resource(resource_type)
class AuthorizationResource(ResourceWithURI):
"""Authorization Resource.
@@ -344,6 +400,7 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri = jose.Field('new_cert_uri')
@Directory.register
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request.
@@ -369,6 +426,7 @@ class CertificateResource(ResourceWithURI):
authzrs = jose.Field('authzrs')
@Directory.register
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
@@ -380,16 +438,3 @@ class Revocation(jose.JSONObjectWithFields):
resource = fields.Resource(resource_type)
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
def url(cls, base):
"""Get revocation URL.
:param str base: New Registration Resource or server (root) URL.
"""
return urllib_parse.urljoin(base, cls.PATH)
+45 -11
View File
@@ -60,6 +60,7 @@ class ConstantTest(unittest.TestCase):
def setUp(self):
from acme.messages import _Constant
class MockConstant(_Constant): # pylint: disable=missing-docstring
POSSIBLE_NAMES = {}
@@ -92,6 +93,45 @@ class ConstantTest(unittest.TestCase):
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):
"""Tests for acme.messages.Registration."""
@@ -185,7 +225,8 @@ class ChallengeBodyTest(unittest.TestCase):
"""Tests for acme.messages.ChallengeBody."""
def setUp(self):
self.chall = challenges.DNS(token='foo')
self.chall = challenges.DNS(token=jose.b64decode(
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))
from acme.messages import ChallengeBody
from acme.messages import Error
@@ -201,7 +242,7 @@ class ChallengeBodyTest(unittest.TestCase):
'uri': 'http://challb',
'status': self.status,
'type': 'dns',
'token': 'foo',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
'error': error,
}
self.jobj_from = self.jobj_to.copy()
@@ -211,7 +252,6 @@ class ChallengeBodyTest(unittest.TestCase):
'detail': 'Unable to communicate with DNS server',
}
def test_to_partial_json(self):
self.assertEqual(self.jobj_to, self.challb.to_partial_json())
@@ -224,7 +264,8 @@ class ChallengeBodyTest(unittest.TestCase):
hash(ChallengeBody.from_json(self.jobj_from))
def test_proxy(self):
self.assertEqual('foo', self.challb.token)
self.assertEqual(jose.b64decode(
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'), self.challb.token)
class AuthorizationTest(unittest.TestCase):
@@ -320,13 +361,6 @@ class CertificateResourceTest(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):
from acme.messages import Revocation
self.rev = Revocation(certificate=CERT)
+1 -1
View File
@@ -36,7 +36,7 @@ class Signature(jose.JSONObjectWithFields):
:param bytes msg: Message to be signed.
:param key: Key used for signing.
:type key: `cryptography.hazmat.primitives.assymetric.rsa.RSAPrivateKey`
:type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
(optionally wrapped in `.ComparableRSAKey`).
:param bytes nonce: Nonce to be used. If None, nonce of
+7 -1
View File
@@ -1,4 +1,4 @@
# Symlinked in letsencrypt/tests/test_util.py, casues duplicate-code
# Symlinked in letsencrypt/tests/test_util.py, causes duplicate-code
# warning that cannot be disabled locally.
"""Test utilities.
@@ -20,12 +20,14 @@ def vector_path(*names):
return pkg_resources.resource_filename(
__name__, os.path.join('testdata', *names))
def load_vector(*names):
"""Load contents of a test vector."""
# luckily, resource_string opens file in binary mode
return pkg_resources.resource_string(
__name__, os.path.join('testdata', *names))
def _guess_loader(filename, loader_pem, loader_der):
_, ext = os.path.splitext(filename)
if ext.lower() == '.pem':
@@ -35,6 +37,7 @@ def _guess_loader(filename, loader_pem, loader_der):
else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension")
def load_cert(*names):
"""Load certificate."""
loader = _guess_loader(
@@ -42,6 +45,7 @@ def load_cert(*names):
return jose.ComparableX509(OpenSSL.crypto.load_certificate(
loader, load_vector(*names)))
def load_csr(*names):
"""Load certificate request."""
loader = _guess_loader(
@@ -49,6 +53,7 @@ def load_csr(*names):
return jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
loader, load_vector(*names)))
def load_rsa_private_key(*names):
"""Load RSA 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(
load_vector(*names), password=None, backend=default_backend()))
def load_pyopenssl_private_key(*names):
"""Load pyOpenSSL private key."""
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
+1 -2
View File
@@ -5,16 +5,15 @@ from setuptools import find_packages
install_requires = [
'argparse',
# load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8)
'cryptography>=0.8',
'mock<1.1.0', # py26
'pyrfc3339',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
'PyOpenSSL>=0.15',
'pyrfc3339',
'pytz',
'requests',
'six',
+12 -1
View File
@@ -4,8 +4,19 @@
# - Fedora 22 (x64)
# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet)
if type yum 2>/dev/null
then
tool=yum
elif type dnf 2>/dev/null
then
tool=dnf
else
echo "Neither yum nor dnf found. Aborting bootstrap!"
exit 1
fi
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
yum install -y \
$tool install -y \
git-core \
python \
python-devel \
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh -xe
pkg install -Ay \
git \
python \
py27-virtualenv \
augeas \
libffi \
+4 -3
View File
@@ -52,7 +52,8 @@ The following tools are there to help you:
before submitting a new pull request.
- ``tox -e cover`` checks the test coverage only. Calling the
``./tox.cover.sh`` script directly might be a bit quicker, though.
``./tox.cover.sh`` script directly (or even ``./tox.cover.sh $pkg1
$pkg2 ...`` for any subpackages) might be a bit quicker, though.
- ``tox -e lint`` checks the style of the whole project, while
``pylint --rcfile=.pylintrc path`` will check a single file or
@@ -60,7 +61,7 @@ The following tools are there to help you:
- For debugging, we recommend ``pip install ipdb`` and putting
``import ipdb; ipdb.set_trace()`` statement inside the source
code. Alternatively, you can use Python'd standard library `pdb`,
code. Alternatively, you can use Python's standard library `pdb`,
but you won't get TAB completion...
@@ -80,7 +81,7 @@ patient - it will take some time... Once its ready, you will see
If you would like to test `letsencrypt_nginx` plugin (highly
encouraged) make sure to install prerequisites as listed in
``tests/integration/nginx.sh``:
``letsencrypt-nginx/tests/boulder-integration.sh``:
.. include:: ../letsencrypt-nginx/tests/boulder-integration.sh
:start-line: 1
+16 -1
View File
@@ -102,6 +102,21 @@ Centos 7
sudo ./bootstrap/centos.sh
FreeBSD
-------
.. code-block:: shell
sudo ./bootstrap/freebsd.sh
Bootstrap script for FreeBSD uses ``pkg`` for package installation,
i.e. it does not use ports.
FreeBSD by default uses ``tcsh``. In order to activate virtulenv (see
below), you will need a compatbile shell, e.g. ``pkg install bash &&
bash``.
Installation
============
@@ -129,7 +144,7 @@ To get a new certificate run:
.. code-block:: shell
./venv/bin/letsencrypt auth
sudo ./venv/bin/letsencrypt auth
The ``letsencrypt`` commandline tool has a builtin help:
@@ -84,7 +84,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
description = "Apache Web Server - Alpha"
@classmethod
def add_parser_arguments(cls, add):
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
@@ -283,7 +282,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
self.assoc[target_name] = vhost
return vhost
def _find_best_vhost(self, 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:
logger.info("Loading mod_ssl into Apache Server")
self.enable_mod("ssl", temp=temp)
# Check for Listen <port>
@@ -583,7 +580,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
ssl_vhost = self._create_vhost(vh_p)
self.vhosts.append(ssl_vhost)
# NOTE: Searches through Augeas seem to ruin changes to directives
# The configuration must also be saved before being searched
# for the new directives; For these reasons... this is tacked
@@ -794,7 +790,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
raise errors.PluginError(
"Let's Encrypt has already enabled redirection")
def _create_redirect_vhost(self, 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
if (not os.path.isdir(os.path.join(self.parser.root, "mods-available"))
or not os.path.isdir(
os.path.join(self.parser.root, "mods-enabled"))):
avail_path = os.path.join(self.parser.root, "mods-available")
enabled_path = 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(
"Unsupported directory layout. You may try to enable mod %s "
"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.save_notes += "Enabled %s module in Apache" % mod_name
logger.debug("Enabled Apache %s module", mod_name)
self._add_parser_mod(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
# within these new configuration sections.
# Restart is not necessary as DUMP_RUN_CFG uses latest config.
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_" + mod_name + ".c")
@@ -1140,6 +1154,25 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
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):
"""Restarts the Apache Server.
+2 -2
View File
@@ -14,8 +14,8 @@ class Addr(common.Addr):
"""
if isinstance(other, self.__class__):
return ((self.tup == other.tup) or
(self.tup[0] == other.tup[0]
and self.is_wildcard() and other.is_wildcard()))
(self.tup[0] == other.tup[0] and
self.is_wildcard() and other.is_wildcard()))
return False
def __ne__(self, other):
@@ -195,8 +195,7 @@ class ApacheParser(object):
self.aug.set(nvh_path + "/arg", args[0])
else:
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):
"""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
"""
return "".join(["["+c.upper()+c.lower()+"]"
return "".join(["[" + c.upper() + c.lower() + "]"
if c.isalpha() else c for c in re.escape(string)])
@@ -56,7 +56,6 @@ class ComplexParserTest(util.ParserTest):
self.assertRaises(
errors.PluginError, self.parser.get_arg, matches[0])
def test_basic_ifdefine(self):
self.assertEqual(len(self.parser.find_dir("VAR_DIRECTIVE")), 2)
self.assertEqual(len(self.parser.find_dir("INVALID_VAR_DIRECTIVE")), 0)
@@ -71,7 +70,6 @@ class ComplexParserTest(util.ParserTest):
self.assertEqual(
len(self.parser.find_dir("INVALID_NESTED_DIRECTIVE")), 0)
def test_load_modules(self):
"""If only first is found, there is bad variable parsing."""
self.assertTrue("status_module" in self.parser.modules)
@@ -551,6 +551,7 @@ class TwoVhost80Test(util.ApacheTest):
self.assertRaises(
errors.PluginError,
self.config.enhance, "letsencrypt.demo", "redirect")
def test_unknown_rewrite2(self):
# Skip the enable mod
self.config.parser.modules.add("rewrite_module")
@@ -143,7 +143,7 @@ class BasicParserTest(util.ParserTest):
'Group: name="www-data" id=33 not_used\n'
)
expected_vars = {"TEST": "", "U_MICH": "", "TLS": "443",
"example_path":"Documents/path"}
"example_path": "Documents/path"}
self.parser.update_runtime_variables("ctl")
self.assertEqual(self.parser.variables, expected_vars)
+1 -1
View File
@@ -18,7 +18,7 @@ setup(
entry_points={
'letsencrypt.plugins': [
'apache = letsencrypt_apache.configurator:ApacheConfigurator',
],
],
},
include_package_data=True,
)
@@ -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
# 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
STATIC_MODULES = {"core", "so", "http", "mpm_event", "watchdog",}
STATIC_MODULES = set(["core", "so", "http", "mpm_event", "watchdog"])
SHARED_MODULES = {
@@ -31,7 +31,7 @@ SHARED_MODULES = {
"session_cookie", "session_crypto", "session_dbd", "setenvif",
"slotmem_shm", "socache_dbm", "socache_memcache", "socache_shmcb",
"speling", "ssl", "status", "substitute", "unique_id", "userdir",
"vhost_alias",}
"vhost_alias"}
class Proxy(apache_common.Proxy):
@@ -72,11 +72,10 @@ class Proxy(object):
logger.debug(line)
host_config = docker.utils.create_host_config(
binds={
self._temp_dir : {"bind" : self._temp_dir, "mode" : "rw"}},
binds={self._temp_dir: {"bind": self._temp_dir, "mode": "rw"}},
port_bindings={
80 : ("127.0.0.1", self.http_port),
443 : ("127.0.0.1", self.https_port)},)
80: ("127.0.0.1", self.http_port),
443: ("127.0.0.1", self.https_port)},)
container = self._docker_client.create_container(
image_name, command, ports=[80, 443], volumes=self._temp_dir,
host_config=host_config)
@@ -23,7 +23,7 @@ class IPluginProxy(zope.interface.Interface):
def cleanup_from_tests():
"""Performs any necessary cleanup from running plugin tests.
This is guarenteed to be called before the program exits.
This is guaranteed to be called before the program exits.
"""
@@ -30,7 +30,7 @@ tests that the plugin supports are performed.
"""
PLUGINS = {"apache" : apache24.Proxy}
PLUGINS = {"apache": apache24.Proxy}
logger = logging.getLogger(__name__)
@@ -191,7 +191,7 @@ def test_enhancements(plugin, domains):
success = True
for domain in domains:
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):
logger.error("Improper redirect for domain %s", domain)
success = False
@@ -34,7 +34,7 @@ def create_le_config(parent_dir):
os.mkdir(config["work_dir"])
os.mkdir(config["logs_dir"])
return argparse.Namespace(**config) # pylint: disable=star-args
return argparse.Namespace(**config) # pylint: disable=star-args
def extract_configs(configs, parent_dir):
@@ -7,6 +7,7 @@ from pyparsing import (
from pyparsing import stringEnd
from pyparsing import restOfLine
class RawNginxParser(object):
# pylint: disable=expression-not-assigned
"""A class that parses nginx configuration with pyparsing."""
@@ -32,10 +33,10 @@ class RawNginxParser(object):
block = Forward()
block << Group(
(Group(key + location_statement) ^ Group(if_statement))
+ left_bracket
+ Group(ZeroOrMore(Group(comment | assignment) | block))
+ right_bracket)
(Group(key + location_statement) ^ Group(if_statement)) +
left_bracket +
Group(ZeroOrMore(Group(comment | assignment) | block)) +
right_bracket)
script = OneOrMore(Group(comment | assignment) ^ block) + stringEnd
@@ -41,7 +41,6 @@ class DvsniPerformTest(util.NginxTest):
domain="www.example.org", account_key=account_key),
]
def setUp(self):
super(DvsniPerformTest, self).setUp()
+3 -2
View File
@@ -5,8 +5,9 @@ from setuptools import find_packages
install_requires = [
'acme',
'letsencrypt',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
'mock<1.1.0', # py26
'PyOpenSSL',
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
'zope.interface',
]
@@ -17,7 +18,7 @@ setup(
entry_points={
'letsencrypt.plugins': [
'nginx = letsencrypt_nginx.configurator:NginxConfigurator',
],
],
},
include_package_data=True,
)
+1 -1
View File
@@ -62,7 +62,7 @@ class Account(object): # pylint: disable=too-few-public-methods
# Implementation note: Email? Multiple accounts can have the
# same email address. Registration URI? Assigned by the
# server, not guaranteed to be stable over time, nor
# cannonical URI can be generated. ACME protocol doesn't allow
# canonical URI can be generated. ACME protocol doesn't allow
# account key (and thus its fingerprint) to be updated...
@property
+9 -8
View File
@@ -244,7 +244,7 @@ class AuthHandler(object):
"""
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
raise errors.AuthorizationError(
"Target challenge not found in authorization resource")
@@ -493,26 +493,27 @@ _ERROR_HELP_COMMON = (
_ERROR_HELP = {
"connection" :
"connection":
_ERROR_HELP_COMMON + " Additionally, please check that your computer "
"has publicly routable IP address and no firewalls are preventing the "
"server from communicating with the client.",
"dnssec" :
"dnssec":
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
"your domain, please ensure the signature is valid.",
"malformed" :
"malformed":
"To fix these errors, please make sure that you did not provide any "
"invalid information to the client and try running Let's Encrypt "
"again.",
"serverInternal" :
"serverInternal":
"Unfortunately, an error on the ACME server prevented you from completing "
"authorization. Please try again later.",
"tls" :
"tls":
_ERROR_HELP_COMMON + " Additionally, please check that you have an up "
"to date TLS configuration that allows the server to communicate with "
"the Let's Encrypt client.",
"unauthorized" : _ERROR_HELP_COMMON,
"unknownHost" : _ERROR_HELP_COMMON,}
"unauthorized": _ERROR_HELP_COMMON,
"unknownHost": _ERROR_HELP_COMMON,
}
def _report_failed_challs(failed_achalls):
+29 -17
View File
@@ -16,12 +16,16 @@ import zope.component
import zope.interface.exceptions
import zope.interface.verify
from acme import client as acme_client
from acme import jose
import letsencrypt
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import client
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
@@ -69,7 +73,7 @@ Choice of server for authentication/installation:
More detailed help:
-h, --help [topic] print this message, or detailed help on a topic;
-h, --help [topic] print this message, or detailed help on a topic;
the available topics are:
all, apache, automation, nginx, paths, security, testing, or any of the
@@ -241,16 +245,20 @@ def install(args, config, plugins):
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."""
if args.cert_path is None and args.key_path is None:
return "At least one of --cert-path or --key-path is required"
# This depends on the renewal config and cannot be completed yet.
zope.component.getUtility(interfaces.IDisplay).notification(
"Revocation is not available with the new Boulder server yet.")
#client.revoke(args.installer, config, plugins, args.no_confirm,
# args.cert_path, args.key_path)
if args.key_path is not None: # revocation by cert key
logger.debug("Revoking %s using cert key %s",
args.cert_path[0], args.key_path[0])
acme = acme_client.Client(
config.server, key=jose.JWK.load(args.key_path[1]))
else: # revocation by account key
logger.debug("Revoking %s using Account Key", args.cert_path[0])
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):
@@ -334,6 +342,7 @@ class SilentParser(object): # pylint: disable=too-few-public-methods
"""
def __init__(self, parser):
self.parser = parser
def add_argument(self, *args, **kwargs):
"""Wrap, but silence help"""
kwargs["help"] = argparse.SUPPRESS
@@ -361,7 +370,7 @@ class HelpfulArgumentParser(object):
default_config_files=flag_default("config_files"))
# This is the only way to turn off overly verbose config flag documentation
self.parser._add_config_file_help = False # pylint: disable=protected-access
self.parser._add_config_file_help = False # pylint: disable=protected-access
self.silent_parser = SilentParser(self.parser)
self.args = self.preprocess_args(args)
@@ -369,7 +378,7 @@ class HelpfulArgumentParser(object):
help2 = self.prescan_for_flag("--help", self.help_topics)
assert max(True, "a") == "a", "Gravity changed direction"
help_arg = max(help1, help2)
if help_arg == True:
if help_arg:
# just --help with no topic; avoid argparse altogether
print USAGE
sys.exit(0)
@@ -562,6 +571,7 @@ VERBS = ["run", "auth", "install", "revoke", "rollback", "config_changes",\
def _create_subparsers(helpful):
subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND")
def add_subparser(name, func): # pylint: disable=missing-docstring
subparser = subparsers.add_parser(
name, help=func.__doc__.splitlines()[0], description=func.__doc__)
@@ -592,14 +602,16 @@ def _create_subparsers(helpful):
"--cert-path", required=True, help="Path to a certificate that "
"is going to be installed.")
parser_install.add_argument(
"--key-path", required=True, help="Accompynying private key")
"--key-path", required=True, help="Accompanying private key")
parser_install.add_argument(
"--chain-path", help="Accompanying path to a certificate chain.")
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(
"--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(
"--checkpoints", type=int, metavar="N",
@@ -639,7 +651,7 @@ def _plugins_parsing(helpful, plugins):
"plugins", description="Let's Encrypt client supports an "
"extensible plugins architecture. See '%(prog)s plugins' for a "
"list of all available plugins and their names. You can force "
"a particular plugin by setting options provided below. Futher "
"a particular plugin by setting options provided below. Further "
"down this help message you will find plugin-specific options "
"(prefixed by --{plugin_name}).")
helpful.add(
@@ -717,7 +729,7 @@ def _handle_exception(exc_type, exc_value, trace, args):
with open(logfile, "w") as logfd:
traceback.print_exception(
exc_type, exc_value, trace, file=logfd)
except: # pylint: disable=bare-except
except: # pylint: disable=bare-except
sys.exit("".join(
traceback.format_exception(exc_type, exc_value, trace)))
+3 -3
View File
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
def _acme_from_config_key(config, key):
# 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))
@@ -279,8 +279,8 @@ class Client(object):
:param .RenewableCert cert: Newly issued certificate
"""
if ("autorenew" not in cert.configuration
or cert.configuration.as_bool("autorenew")):
if ("autorenew" not in cert.configuration or
cert.configuration.as_bool("autorenew")):
if ("autodeploy" not in cert.configuration or
cert.configuration.as_bool("autodeploy")):
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)
@property
def accounts_dir(self): #pylint: disable=missing-docstring
def accounts_dir(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path)
+1 -1
View File
@@ -16,7 +16,7 @@ CLI_DEFAULTS = dict(
"letsencrypt", "cli.ini"),
],
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,
rollback_checkpoints=1,
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(
str(error) for error in openssl_errors)))
def pyopenssl_load_certificate(data):
"""Load PEM/DER certificate.
+3 -3
View File
@@ -16,7 +16,7 @@ util = zope.component.getUtility # pylint: disable=invalid-name
def choose_plugin(prepared, question):
"""Allow the user to choose ther plugin.
"""Allow the user to choose their plugin.
:param list prepared: List of `~.PluginEntryPoint`.
:param str question: Question to be presented to the user.
@@ -25,8 +25,8 @@ def choose_plugin(prepared, question):
:rtype: `~.PluginEntryPoint`
"""
opts = [plugin_ep.description_with_name
+ (" [Misconfigured]" if plugin_ep.misconfigured else "")
opts = [plugin_ep.description_with_name +
(" [Misconfigured]" if plugin_ep.misconfigured else "")
for plugin_ep in prepared]
while True:
+2 -2
View File
@@ -76,7 +76,7 @@ class NcursesDisplay(object):
"help_label": help_label,
"width": self.width,
"height": self.height,
"menu_height": self.height-6,
"menu_height": self.height - 6,
}
# Can accept either tuples or just the actual choices
@@ -315,7 +315,7 @@ class FileDisplay(object):
if index < 1 or index > len(tags):
return []
# 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):
"""Print a menu on the screen.
+1
View File
@@ -73,6 +73,7 @@ class NoInstallationError(PluginError):
class MisconfigurationError(PluginError):
"""Let's Encrypt Misconfiguration error."""
class NotSupportedError(PluginError):
"""Let's Encrypt Plugin function not supported error."""
+2 -4
View File
@@ -142,7 +142,7 @@ class IAuthenticator(IPlugin):
:param str domain: Domain for which challenge preferences are sought.
:returns: List of challege types (subclasses of
:returns: List of challenge types (subclasses of
:class:`acme.challenges.Challenge`) with the most
preferred challenges first. If a type is not specified, it means the
Authenticator cannot perform the challenge.
@@ -194,8 +194,7 @@ class IConfig(zope.interface.Interface):
filtered, stripped or sanitized.
"""
server = zope.interface.Attribute(
"ACME new registration URI (including /acme/new-reg).")
server = zope.interface.Attribute("ACME Directory Resource URI.")
email = zope.interface.Attribute(
"Email used for registration and recovery contact.")
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
@@ -440,7 +439,6 @@ class IValidator(zope.interface.Interface):
"""
def hsts(name):
"""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
# be done in addition to the regex
EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$")
def safe_email(email):
"""Scrub email address before using it."""
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)."""
return name + "-"
def dest_namespace(name):
"""ArgumentParser dest namespace (prefix of all destinations)."""
return name.replace("-", "_") + "_"
@@ -86,6 +87,7 @@ class Plugin(object):
# other
class Addr(object):
r"""Represents an virtual host address.
+1
View File
@@ -101,6 +101,7 @@ class PluginEntryPointTest(unittest.TestCase):
with mock.patch("letsencrypt.plugins."
"disco.zope.interface") as mock_zope:
mock_zope.exceptions = exceptions
def verify_object(iface, obj): # pylint: disable=missing-docstring
assert obj is plugin
assert iface is iface1 or iface is iface2 or iface is iface3
+21 -6
View File
@@ -4,6 +4,7 @@ import logging
import pipes
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
@@ -37,7 +38,7 @@ class ManualAuthenticator(common.Plugin):
Make sure your web server displays the following content at
{uri} before continuing:
{achall.token}
{validation}
Content-Type header MUST be set to {ct}.
@@ -122,6 +123,20 @@ binary for temporary key/certificate generation.""".replace("\n", "")
responses.append(self._perform_single(achall))
return responses
@classmethod
def _test_mode_busy_wait(cls, port):
while True:
time.sleep(1)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.connect(("localhost", port))
except socket.error: # pragma: no cover
pass
else:
break
finally:
sock.close()
def _perform_single(self, achall):
# same path for each challenge response would be easier for
# users, but will not work if multiple domains point at the
@@ -129,13 +144,13 @@ binary for temporary key/certificate generation.""".replace("\n", "")
response, validation = achall.gen_response_and_validation(
tls=(not self.config.no_simple_http_tls))
port = (response.port if self.config.simple_http_port is None
else int(self.config.simple_http_port))
command = self.template.format(
root=self._root, achall=achall, response=response,
validation=pipes.quote(validation.json_dumps()),
encoded_token=achall.chall.encode("token"),
ct=response.CONTENT_TYPE, port=(
response.port if self.config.simple_http_port is None
else self.config.simple_http_port))
ct=response.CONTENT_TYPE, port=port)
if self.conf("test-mode"):
logger.debug("Test mode. Executing the manual command: %s", command)
try:
@@ -153,12 +168,12 @@ binary for temporary key/certificate generation.""".replace("\n", "")
logger.debug("Manual command running as PID %s.", self._httpd.pid)
# give it some time to bootstrap, before we try to verify
# (cert generation in case of simpleHttpS might take time)
time.sleep(4) # XXX
self._test_mode_busy_wait(port)
if self._httpd.poll() is not None:
raise errors.Error("Couldn't execute manual command")
else:
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),
ct=response.CONTENT_TYPE, command=command))
+27 -3
View File
@@ -61,7 +61,27 @@ class ManualAuthenticatorTest(unittest.TestCase):
self.achalls[0].challb.chall, "foo.com", KEY.public_key(), 4430)
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
self.assertEqual([None], self.auth.perform(self.achalls))
@@ -71,25 +91,29 @@ class ManualAuthenticatorTest(unittest.TestCase):
mock_popen.side_effect = OSError
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
@mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_command_run_failure(
self, mock_popen, unused_mock_sleep):
self, mock_popen, unused_mock_sleep, unused_mock_socket):
mock_popen.poll.return_value = 10
mock_popen.return_value.pid = 1234
self.assertRaises(
errors.Error, self.auth_test_mode.perform, self.achalls)
@mock.patch("letsencrypt.plugins.manual.socket.socket", autospec=True)
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep):
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep,
mock_socket):
mock_popen.return_value.poll.side_effect = [None, 10]
mock_popen.return_value.pid = 1234
mock_verify.return_value = False
self.assertEqual([False], self.auth_test_mode.perform(self.achalls))
self.assertEqual(1, mock_sleep.call_count)
self.assertEqual(1, mock_socket.call_count)
def test_cleanup_test_mode_already_terminated(self):
# pylint: disable=protected-access
@@ -321,10 +321,8 @@ class PerformTest(unittest.TestCase):
self.authenticator.already_listening = mock.Mock(return_value=False)
result = self.authenticator.perform(self.achalls)
self.assertEqual(len(self.authenticator.tasks), 2)
self.assertTrue(
self.authenticator.tasks.has_key(self.achall1.token))
self.assertTrue(
self.authenticator.tasks.has_key(self.achall2.token))
self.assertTrue(self.achall1.token in self.authenticator.tasks)
self.assertTrue(self.achall2.token in self.authenticator.tasks)
self.assertTrue(isinstance(result, list))
self.assertEqual(len(result), 3)
self.assertTrue(isinstance(result[0], challenges.ChallengeResponse))
@@ -340,10 +338,8 @@ class PerformTest(unittest.TestCase):
self.authenticator.already_listening = mock.Mock(return_value=False)
result = self.authenticator.perform(self.achalls)
self.assertEqual(len(self.authenticator.tasks), 2)
self.assertTrue(
self.authenticator.tasks.has_key(self.achall1.token))
self.assertTrue(
self.authenticator.tasks.has_key(self.achall2.token))
self.assertTrue(self.achall1.token in self.authenticator.tasks)
self.assertTrue(self.achall2.token in self.authenticator.tasks)
self.assertTrue(isinstance(result, list))
self.assertEqual(len(result), 3)
self.assertEqual(result, [None, None, False])
+2 -2
View File
@@ -17,7 +17,7 @@ from letsencrypt.display import util as display_util
logger = logging.getLogger(__name__)
class ProofOfPossession(object): # pylint: disable=too-few-public-methods
class ProofOfPossession(object): # pylint: disable=too-few-public-methods
"""Proof of Possession Identifier Validation Challenge.
Based on draft-barnes-acme, section 6.5.
@@ -71,7 +71,7 @@ class ProofOfPossession(object): # pylint: disable=too-few-public-methods
# If we get here, the key wasn't found
return False
def _gen_response(self, achall, key_path): # pylint: disable=no-self-use
def _gen_response(self, achall, key_path): # pylint: disable=no-self-use
"""Create the response to the Proof of Possession Challenge.
:param achall: Proof of Possession Challenge
+1 -1
View File
@@ -48,7 +48,7 @@ class Revoker(object):
"""
def __init__(self, installer, config, no_confirm=False):
# 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.config = config
+4 -5
View File
@@ -223,7 +223,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
target = os.readlink(link)
if not os.path.isabs(target):
target = os.path.join(os.path.dirname(link), target)
return target
return os.path.abspath(target)
def current_version(self, kind):
"""Returns numerical version of the specified item.
@@ -486,8 +486,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
:rtype: bool
"""
if ("autorenew" not in self.configuration
or self.configuration.as_bool("autorenew")):
if ("autorenew" not in self.configuration or
self.configuration.as_bool("autorenew")):
# Consider whether to attempt to autorenew this cert now
# Renewals on the basis of revocation
@@ -603,7 +603,6 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
new_config.write()
return cls(new_config, config, cli_config)
def save_successor(self, prior_version, new_cert, new_privkey, new_chain):
"""Save new cert and chain as a successor of a prior version.
@@ -626,7 +625,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
"""
# XXX: assumes official archive location rather than examining links
# XXX: consider using os.open for availablity of os.O_EXCL
# XXX: consider using os.open for availability of os.O_EXCL
# XXX: ensure file permissions are correct; also create directories
# if needed (ensuring their permissions are correct)
# Figure out what the new version is and hence where to save things
+1 -1
View File
@@ -30,7 +30,7 @@ POP = challenges.ProofOfPossession(
"16d95b7b63f1972b980b14c20291f3c0d1855d95",
"48b46570d9fc6358108af43ad1649484def0debf"
),
certs=(), # TODO
certs=(), # TODO
subject_key_identifiers=("d0083162dcc4c8a23ecb8aecbd86120e56fd24e5"),
serial_numbers=(34234239832, 23993939911, 17),
issuers=(
+15 -11
View File
@@ -37,7 +37,7 @@ class ChallengeFactoryTest(unittest.TestCase):
self.dom = "test"
self.handler.authzr[self.dom] = acme_util.gen_authzr(
messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES,
[messages.STATUS_PENDING]*6, False)
[messages.STATUS_PENDING] * 6, False)
def test_all(self):
cont_c, dv_c = self.handler._challenge_factory(
@@ -163,7 +163,7 @@ class GetAuthorizationsTest(unittest.TestCase):
messages.STATUS_VALID,
dom,
[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)
@@ -183,15 +183,15 @@ class PollChallengesTest(unittest.TestCase):
self.doms = ["0", "1", "2"]
self.handler.authzr[self.doms[0]] = acme_util.gen_authzr(
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(
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(
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 = {}
for dom in self.doms:
@@ -282,6 +282,7 @@ class PollChallengesTest(unittest.TestCase):
)
return (new_authzr, "response")
class GenChallengePathTest(unittest.TestCase):
"""Tests for letsencrypt.auth_handler.gen_challenge_path.
@@ -321,7 +322,7 @@ class GenChallengePathTest(unittest.TestCase):
combos = acme_util.gen_combos(challbs)
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
# dumb_path() trivial test
# dumb_path() trivial test
self.assertTrue(self._call(challbs, prefs, None))
def test_full_cont_server(self):
@@ -427,26 +428,29 @@ class ReportFailedChallsTest(unittest.TestCase):
from letsencrypt import achallenges
kwargs = {
"chall" : acme_util.SIMPLE_HTTP,
"chall": acme_util.SIMPLE_HTTP,
"uri": "uri",
"status": messages.STATUS_INVALID,
"error": messages.Error(typ="tls", detail="detail"),
}
self.simple_http = achallenges.SimpleHTTP(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
# pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="example.com",
account_key="key")
kwargs["chall"] = acme_util.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",
account_key="key")
kwargs["error"] = messages.Error(typ="dnssec", detail="detail")
self.dvsni_diff = achallenges.DVSNI(
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
# pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="foo.bar",
account_key="key")
@@ -477,7 +481,7 @@ def gen_dom_authzr(domain, unused_new_authzr_uri, challs):
"""Generates new authzr for domains."""
return acme_util.gen_authzr(
messages.STATUS_PENDING, domain, challs,
[messages.STATUS_PENDING]*len(challs))
[messages.STATUS_PENDING] * len(challs))
if __name__ == "__main__":
+1 -1
View File
@@ -60,7 +60,7 @@ class CLITest(unittest.TestCase):
for args in itertools.chain(
*(itertools.combinations(flags, r)
for r in xrange(len(flags)))):
self._call(['plugins',] + list(args))
self._call(['plugins'] + list(args))
@mock.patch("letsencrypt.cli.sys")
def test_handle_exception(self, mock_sys):
+2 -2
View File
@@ -70,7 +70,7 @@ class ClientTest(unittest.TestCase):
def test_init_acme_verify_ssl(self):
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):
self.client.auth_handler = mock.MagicMock()
@@ -166,7 +166,7 @@ class RollbackTest(unittest.TestCase):
self.assertEqual(self.m_install().restart.call_count, 1)
def test_no_installer(self):
self._call(1, None) # Just make sure no exceptions are raised
self._call(1, None) # Just make sure no exceptions are raised
if __name__ == "__main__":
+1 -1
View File
@@ -35,7 +35,7 @@ class PerformTest(unittest.TestCase):
self.assertRaises(
errors.ContAuthError, self.auth.perform, [
achallenges.DVSNI(
challb=None, domain="0", account_key="invalid_key"),])
challb=None, domain="0", account_key="invalid_key")])
def test_chall_pref(self):
self.assertEqual(
+1
View File
@@ -250,6 +250,7 @@ class GenSSLLabURLs(unittest.TestCase):
self.assertTrue("eff.org" in urls[0])
self.assertTrue("umich.edu" in urls[1])
class GenHttpsNamesTest(unittest.TestCase):
"""Test _gen_https_names."""
def setUp(self):
+1 -1
View File
@@ -35,7 +35,7 @@ class NcursesDisplayTest(unittest.TestCase):
"help_label": "",
"width": display_util.WIDTH,
"height": display_util.HEIGHT,
"menu_height": display_util.HEIGHT-6,
"menu_height": display_util.HEIGHT - 6,
}
@mock.patch("letsencrypt.display.util.dialog.Dialog.msgbox")
+3 -2
View File
@@ -1,9 +1,10 @@
"""Tests for letsencrypt.notify."""
import mock
import socket
import unittest
import mock
class NotifyTests(unittest.TestCase):
"""Tests for the notifier."""
@@ -80,4 +80,4 @@ class ProofOfPossessionTest(unittest.TestCase):
if __name__ == "__main__":
unittest.main() # pragma: no cover
unittest.main() # pragma: no cover
+2 -2
View File
@@ -24,6 +24,7 @@ def unlink_all(rc_object):
for kind in ALL_FOUR:
os.unlink(getattr(rc_object, kind))
def fill_with_sample_data(rc_object):
"""Put dummy data into all four files of this RenewableCert."""
for kind in ALL_FOUR:
@@ -97,7 +98,7 @@ class RenewableCertTests(unittest.TestCase):
self.assertRaises(
errors.CertStorageError, storage.RenewableCert, config, defaults)
def test_consistent(self): # pylint: disable=too-many-statements
def test_consistent(self): # pylint: disable=too-many-statements
oldcert = self.test_rc.cert
self.test_rc.cert = "relative/path"
# Absolute path for item requirement
@@ -608,7 +609,6 @@ class RenewableCertTests(unittest.TestCase):
# This should fail because the renewal itself appears to fail
self.assertFalse(renewer.renew(self.test_rc, 1))
@mock.patch("letsencrypt.renewer.notify")
@mock.patch("letsencrypt.storage.RenewableCert")
@mock.patch("letsencrypt.renewer.renew")
+7 -6
View File
@@ -38,15 +38,15 @@ class ValidatorTest(unittest.TestCase):
@mock.patch("letsencrypt.validator.requests.get")
def test_succesful_redirect(self, mock_get_request):
mock_get_request.return_value = create_response(
301, {"location" : "https://test.com"})
301, {"location": "https://test.com"})
self.assertTrue(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
def test_redirect_with_headers(self, mock_get_request):
mock_get_request.return_value = create_response(
301, {"location" : "https://test.com"})
301, {"location": "https://test.com"})
self.assertTrue(self.validator.redirect(
"test.com", headers={"Host" : "test.com"}))
"test.com", headers={"Host": "test.com"}))
@mock.patch("letsencrypt.validator.requests.get")
def test_redirect_missing_location(self, mock_get_request):
@@ -56,13 +56,13 @@ class ValidatorTest(unittest.TestCase):
@mock.patch("letsencrypt.validator.requests.get")
def test_redirect_wrong_status_code(self, mock_get_request):
mock_get_request.return_value = create_response(
201, {"location" : "https://test.com"})
201, {"location": "https://test.com"})
self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
def test_redirect_wrong_redirect_code(self, mock_get_request):
mock_get_request.return_value = create_response(
303, {"location" : "https://test.com"})
303, {"location": "https://test.com"})
self.assertFalse(self.validator.redirect("test.com"))
@mock.patch("letsencrypt.validator.requests.get")
@@ -106,6 +106,7 @@ class ValidatorTest(unittest.TestCase):
self.assertRaises(
NotImplementedError, self.validator.ocsp_stapling, "test.com")
def create_response(status_code=200, headers=None):
"""Creates a requests.Response object for testing"""
response = requests.Response()
@@ -118,4 +119,4 @@ def create_response(status_code=200, headers=None):
if __name__ == '__main__':
unittest.main() # pragma: no cover
unittest.main() # pragma: no cover
@@ -87,7 +87,7 @@ def copy_config(server_root, temp_dir):
dir_len = len(os.path.dirname(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)
copied_all = True
@@ -151,7 +151,7 @@ def safe_config_file(config_file):
empty_or_all_comments = False
if line.startswith("-----BEGIN"):
return False
elif not ":" in line:
elif ":" not in line:
possible_password_file = False
# 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
@@ -234,9 +234,9 @@ def locate_config(apache_ctl):
for line in output.splitlines():
# Relevant output lines are of the form: -D DIRECTIVE="VALUE"
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:
config_file = line[line.find('"')+1:-1]
config_file = line[line.find('"') + 1:-1]
if not (server_root and config_file):
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)
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:
sys.exit("This script expects the Apache configuration file to be "
"inside the server root")
@@ -300,4 +300,4 @@ def main():
if __name__ == "__main__":
main() # pragma: no cover
main() # pragma: no cover
@@ -141,7 +141,7 @@ class LetsHelpApacheTest(unittest.TestCase):
@mock.patch(_MODULE_NAME + ".subprocess.Popen")
def test_locate_config(self, mock_popen):
mock_popen().communicate.side_effect = [
OSError, ("bad_output", None), (_COMPILE_SETTINGS, None),]
OSError, ("bad_output", None), (_COMPILE_SETTINGS, None)]
self.assertRaises(
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
+2
View File
@@ -41,6 +41,7 @@ install_requires = [
'pyrfc3339',
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
'pytz',
'requests',
'zope.component',
'zope.interface',
]
@@ -67,6 +68,7 @@ testing_extras = [
'coverage',
'nose',
'nosexcover',
'pep8',
'tox',
]
+8 -3
View File
@@ -4,9 +4,7 @@
# instance (see ./boulder-start.sh).
#
# Environment variables:
# SERVER: Passed as "letsencrypt --server" argument. Boulder
# monolithic defaults to :4000, AMQP defaults to :4300. This
# script defaults to monolithic.
# SERVER: Passed as "letsencrypt --server" argument.
#
# Note: this script is called by Boulder integration test suite!
@@ -54,6 +52,13 @@ do
[ "${dir}/${latest}" = "$live" ] # renewer fails this test
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;
then
+1 -1
View File
@@ -13,7 +13,7 @@ export root store_flags
letsencrypt_test () {
letsencrypt \
--server "${SERVER:-http://localhost:4000/acme/new-reg}" \
--server "${SERVER:-http://localhost:4000/directory}" \
--no-verify-ssl \
--dvsni-port 5001 \
--simple-http-port 5001 \
Executable
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
#
# Find all Python imports.
#
# ./deps.sh letsencrypt
# ./deps.sh acme
# ./deps.sh letsencrypt-apache
# ...
#
# Manually compare the output with deps in setup.py.
git grep -h -E '^(import|from.*import)' $1/ | \
awk '{print $2}' | \
grep -vE "^$1" | \
sort -u
+31 -11
View File
@@ -1,10 +1,35 @@
#!/bin/sh
#!/bin/sh -xe
# USAGE: ./tox.cover.sh [package]
#
# This script is used by tox.ini (and thus Travis CI) in order to
# generate separate stats for each package. It should be removed once
# those packages are moved to separate repo.
#
# -e makes sure we fail fast and don't submit coveralls submit
if [ "xxx$1" = "xxx" ]; then
pkgs="letsencrypt acme letsencrypt_apache letsencrypt_nginx letshelp_letsencrypt"
else
pkgs="$@"
fi
cover () {
if [ "$1" = "letsencrypt" ]; then
min=97
elif [ "$1" = "acme" ]; then
min=100
elif [ "$1" = "letsencrypt_apache" ]; then
min=100
elif [ "$1" = "letsencrypt_nginx" ]; then
min=96
elif [ "$1" = "letshelp_letsencrypt" ]; then
min=100
else
echo "Unrecognized package: $1"
exit 1
fi
# "-c /dev/null" makes sure setup.cfg is not loaded (multiple
# --with-cover add up, --cover-erase must not be set for coveralls
# to get all the data); --with-cover scopes coverage to only
@@ -12,16 +37,11 @@ cover () {
# specific package directory; --cover-tests makes sure every tests
# is run (c.f. #403)
nosetests -c /dev/null --with-cover --cover-tests --cover-package \
"$1" --cover-min-percentage="$2" "$1"
"$1" --cover-min-percentage="$min" "$1"
}
rm -f .coverage # --cover-erase is off, make sure stats are correct
# don't use sequential composition (;), if letsencrypt_nginx returns
# 0, coveralls submit will be triggered (c.f. .travis.yml,
# after_success)
cover letsencrypt 97 && \
cover acme 100 && \
cover letsencrypt_apache 100 && \
cover letsencrypt_nginx 96 && \
cover letshelp_letsencrypt 100
for pkg in $pkgs
do
cover $pkg
done
+12 -1
View File
@@ -6,7 +6,7 @@
# acme and letsencrypt are not yet on pypi, so when Tox invokes
# "install *.zip", it will not find deps
skipsdist = true
envlist = py26,py27,cover,lint
envlist = py26,py27,py33,py34,cover,lint
[testenv]
commands =
@@ -23,6 +23,16 @@ setenv =
PYTHONHASHSEED = 0
# https://testrun.org/tox/latest/example/basic.html#special-handling-of-pythonhas
[testenv:py33]
commands =
pip install -e acme[testing]
nosetests acme
[testenv:py34]
commands =
pip install -e acme[testing]
nosetests acme
[testenv:cover]
basepython = python2.7
commands =
@@ -37,6 +47,7 @@ basepython = python2.7
# continue, but tox return code will reflect previous error
commands =
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 acme/acme
pylint --rcfile=.pylintrc letsencrypt-apache/letsencrypt_apache