1
0
mirror of https://github.com/certbot/certbot.git synced 2026-01-21 19:01:07 +03:00

merge with letsencrypt master br

This commit is contained in:
sagi
2015-11-15 07:18:26 +00:00
74 changed files with 1411 additions and 813 deletions

View File

@@ -2,11 +2,12 @@ language: python
services:
- rabbitmq
- mariadb
# 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:
- sudo apt-get install -y mariadb-server mariadb-server-10.0
- 'dpkg -s libaugeas0'
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"'
# using separate envs with different TOXENVs creates 4x1 Travis build
@@ -31,9 +32,8 @@ branches:
- master
- /^test-.*$/
# enable Trusty beta on travis
sudo: required
dist: trusty
# container-based infrastructure
sudo: false
addons:
# make sure simplehttp simple verification works (custom /etc/hosts)
@@ -41,6 +41,8 @@ addons:
- le.wtf
mariadb: "10.0"
apt:
sources:
- augeas
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
- python
- python-dev

View File

@@ -21,7 +21,8 @@ WORKDIR /opt/letsencrypt
# If <dest> doesn't exist, it is created along with all missing
# directories in its path.
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
RUN /opt/letsencrypt/src/ubuntu.sh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \

View File

@@ -22,7 +22,7 @@ WORKDIR /opt/letsencrypt
# TODO: Install non-default Python versions for tox.
# TODO: Install Apache/Nginx for plugin development.
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
RUN /opt/letsencrypt/src/ubuntu.sh && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \

View File

@@ -35,11 +35,11 @@ It's all automated:
All you need to do to sign a single domain is::
user@www:~$ sudo letsencrypt -d www.example.org auth
user@www:~$ sudo letsencrypt -d www.example.org certonly
For multiple domains (SAN) use::
user@www:~$ sudo letsencrypt -d www.example.org -d example.org auth
user@www:~$ sudo letsencrypt -d www.example.org -d example.org certonly
and if you have a compatible web server (Apache or Nginx), Let's Encrypt can
not only get a new certificate, but also deploy it and configure your

View File

@@ -187,7 +187,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge):
key_authorization=self.key_authorization(account_key))
@abc.abstractmethod
def validation(self, account_key):
def validation(self, account_key, **kwargs):
"""Generate validation for the challenge.
Subclasses must implement this method, but they are likely to
@@ -201,7 +201,7 @@ class KeyAuthorizationChallenge(_TokenDVChallenge):
"""
raise NotImplementedError() # pragma: no cover
def response_and_validation(self, account_key):
def response_and_validation(self, account_key, *args, **kwargs):
"""Generate response and validation.
Convenience function that return results of `response` and
@@ -211,7 +211,8 @@ class KeyAuthorizationChallenge(_TokenDVChallenge):
:rtype: tuple
"""
return (self.response(account_key), self.validation(account_key))
return (self.response(account_key),
self.validation(account_key, *args, **kwargs))
@ChallengeResponse.register
@@ -220,6 +221,12 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
typ = "http-01"
PORT = 80
"""Verification port as defined by the protocol.
You can override it (e.g. for testing) by passing ``port`` to
`simple_verify`.
"""
def simple_verify(self, chall, domain, account_public_key, port=None):
"""Simple verify.
@@ -246,7 +253,7 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
# request URI, if it's standard.
if port is not None and port != self.PORT:
logger.warning(
"Using non-standard port for SimpleHTTP verification: %s", port)
"Using non-standard port for http-01 verification: %s", port)
domain += ":{0}".format(port)
uri = chall.uri(domain)
@@ -308,7 +315,7 @@ class HTTP01(KeyAuthorizationChallenge):
"""
return "http://" + domain + self.path
def validation(self, account_key):
def validation(self, account_key, **unused_kwargs):
"""Generate validation.
:param JWK account_key:
@@ -318,89 +325,50 @@ class HTTP01(KeyAuthorizationChallenge):
return self.key_authorization(account_key)
@Challenge.register # pylint: disable=too-many-ancestors
class DVSNI(_TokenDVChallenge):
"""ACME "dvsni" challenge.
:ivar bytes token: Random data, **not** base64-encoded.
"""
typ = "dvsni"
PORT = 443
"""Port to perform DVSNI challenge."""
def gen_response(self, account_key, alg=jose.RS256, **kwargs):
"""Generate response.
:param .JWK account_key: Private account key.
:rtype: .DVSNIResponse
"""
return DVSNIResponse(validation=jose.JWS.sign(
payload=self.json_dumps(sort_keys=True).encode('utf-8'),
key=account_key, alg=alg, **kwargs))
@ChallengeResponse.register
class DVSNIResponse(ChallengeResponse):
"""ACME "dvsni" challenge response.
:param bytes s: Random data, **not** base64-encoded.
"""
typ = "dvsni"
class TLSSNI01Response(KeyAuthorizationChallengeResponse):
"""ACME tls-sni-01 challenge response."""
typ = "tls-sni-01"
DOMAIN_SUFFIX = b".acme.invalid"
"""Domain name suffix."""
PORT = DVSNI.PORT
"""Port to perform DVSNI challenge."""
PORT = 443
"""Verification port as defined by the protocol.
validation = jose.Field("validation", decoder=jose.JWS.from_json)
You can override it (e.g. for testing) by passing ``port`` to
`simple_verify`.
"""
@property
def z(self): # pylint: disable=invalid-name
"""The ``z`` parameter.
def z(self):
"""``z`` value used for verification.
:rtype: bytes
:rtype bytes:
"""
# Instance of 'Field' has no 'signature' member
# pylint: disable=no-member
return hashlib.sha256(self.validation.signature.encode(
"signature").encode("utf-8")).hexdigest().encode()
return hashlib.sha256(
self.key_authorization.encode("utf-8")).hexdigest().lower().encode()
@property
def z_domain(self):
"""Domain name for certificate subjectAltName.
"""Domain name used for verification, generated from `z`.
:rtype: bytes
:rtype bytes:
"""
z = self.z # pylint: disable=invalid-name
return z[:32] + b'.' + z[32:] + self.DOMAIN_SUFFIX
@property
def chall(self):
"""Get challenge encoded in the `validation` payload.
:rtype: challenges.DVSNI
"""
# pylint: disable=no-member
return DVSNI.json_loads(self.validation.payload.decode('utf-8'))
return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX
def gen_cert(self, key=None, bits=2048):
"""Generate DVSNI certificate.
"""Generate tls-sni-01 certificate.
:param OpenSSL.crypto.PKey key: Optional private key used in
certificate generation. If not provided (``None``), then
fresh key will be generated.
:param int bits: Number of bits for newly generated key.
:rtype: `tuple` of `OpenSSL.crypto.X509` and
`OpenSSL.crypto.PKey`
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
if key is None:
@@ -411,11 +379,12 @@ class DVSNIResponse(ChallengeResponse):
'dummy', self.z_domain.decode()], force_san=True), key
def probe_cert(self, domain, **kwargs):
"""Probe DVSNI challenge certificate.
"""Probe tls-sni-01 challenge certificate.
:param unicode domain:
"""
# TODO: domain is not necessary if host is provided
if "host" not in kwargs:
host = socket.gethostbyname(domain)
logging.debug('%s resolved to %s', domain, host)
@@ -428,7 +397,7 @@ class DVSNIResponse(ChallengeResponse):
return crypto_util.probe_sni(**kwargs)
def verify_cert(self, cert):
"""Verify DVSNI challenge certificate."""
"""Verify tls-sni-01 challenge certificate."""
# pylint: disable=protected-access
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
@@ -439,14 +408,15 @@ class DVSNIResponse(ChallengeResponse):
"""Simple verify.
Verify ``validation`` using ``account_public_key``, optionally
probe DVSNI certificate and check using `verify_cert`.
probe tls-sni-01 certificate and check using `verify_cert`.
:param .challenges.DVSNI chall: Corresponding challenge.
:param .challenges.TLSSNI01 chall: Corresponding challenge.
:param str domain: Domain name being validated.
:param JWK account_public_key:
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:param int port: Port used to probe the certificate.
:returns: ``True`` iff client's control of the domain has been
@@ -454,20 +424,8 @@ class DVSNIResponse(ChallengeResponse):
:rtype: bool
"""
# pylint: disable=no-member
if not self.validation.verify(key=account_public_key):
return False
# TODO: it's not checked that payload has exectly 2 fields!
try:
decoded_chall = self.chall
except jose.DeserializationError as error:
logger.debug(error, exc_info=True)
return False
if decoded_chall.token != chall.token:
logger.debug("Wrong token: expected %r, found %r",
chall.token, decoded_chall.token)
if not self.verify(chall, account_public_key):
logger.debug("Verification of key authorization in response failed")
return False
if cert is None:
@@ -480,6 +438,29 @@ class DVSNIResponse(ChallengeResponse):
return self.verify_cert(cert)
@Challenge.register # pylint: disable=too-many-ancestors
class TLSSNI01(KeyAuthorizationChallenge):
"""ACME tls-sni-01 challenge."""
response_cls = TLSSNI01Response
typ = response_cls.typ
# boulder#962, ietf-wg-acme#22
#n = jose.Field("n", encoder=int, decoder=int)
def validation(self, account_key, **kwargs):
"""Generate validation.
:param JWK account_key:
:param OpenSSL.crypto.PKey cert_key: Optional private key used
in certificate generation. If not provided (``None``), then
fresh key will be generated.
:rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey`
"""
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
@Challenge.register
class RecoveryContact(ContinuityChallenge):
"""ACME "recoveryContact" challenge.

View File

@@ -186,14 +186,112 @@ class HTTP01Test(unittest.TestCase):
self.msg.update(token=b'..').good_token)
class DVSNITest(unittest.TestCase):
class TLSSNI01ResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import DVSNI
self.msg = DVSNI(
from acme.challenges import TLSSNI01
self.chall = TLSSNI01(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
self.response = self.chall.response(KEY)
self.jmsg = {
'resource': 'challenge',
'type': 'tls-sni-01',
'keyAuthorization': self.response.key_authorization,
}
# pylint: disable=invalid-name
label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f'
label2 = b'b7793728f084394f2a1afd459556bb5c'
self.z = label1 + label2
self.z_domain = label1 + b'.' + label2 + b'.acme.invalid'
self.domain = 'foo.com'
def test_z_and_domain(self):
self.assertEqual(self.z, self.response.z)
self.assertEqual(self.z_domain, self.response.z_domain)
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.response.to_partial_json())
def test_from_json(self):
from acme.challenges import TLSSNI01Response
self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import TLSSNI01Response
hash(TLSSNI01Response.from_json(self.jmsg))
@mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util.probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1'
self.response.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with(
host='127.0.0.1', port=self.response.PORT,
name=self.z_domain)
self.response.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with(
host='8.8.8.8', port=mock.ANY, name=mock.ANY)
self.response.probe_cert('foo.com', port=1234)
mock_probe_sni.assert_called_with(
host=mock.ANY, port=1234, name=mock.ANY)
self.response.probe_cert('foo.com', bar='baz')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz')
self.response.probe_cert('foo.com', name=b'xxx')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY,
name=self.z_domain)
def test_gen_verify_cert(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.response.gen_cert(key1)
self.assertEqual(key1, key2)
self.assertTrue(self.response.verify_cert(cert))
def test_gen_verify_cert_gen_key(self):
cert, key = self.response.gen_cert()
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
self.assertTrue(self.response.verify_cert(cert))
def test_verify_bad_cert(self):
self.assertFalse(self.response.verify_cert(
test_util.load_cert('cert.pem')))
def test_simple_verify_bad_key_authorization(self):
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
self.response.simple_verify(self.chall, "local", key2.public_key())
@mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True)
def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification
self.assertEqual(mock.sentinel.verification, self.response.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(self.response, mock.sentinel.cert)
@mock.patch('acme.challenges.TLSSNI01Response.probe_cert')
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
mock_probe_cert.side_effect = errors.Error
self.assertFalse(self.response.simple_verify(
self.chall, self.domain, KEY.public_key()))
class TLSSNI01Test(unittest.TestCase):
def setUp(self):
from acme.challenges import TLSSNI01
self.msg = TLSSNI01(
token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e'))
self.jmsg = {
'type': 'dvsni',
'type': 'tls-sni-01',
'token': 'a82d5ff8ef740d12881f6d3c2277ab2e',
}
@@ -201,144 +299,25 @@ class DVSNITest(unittest.TestCase):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import DVSNI
self.assertEqual(self.msg, DVSNI.from_json(self.jmsg))
from acme.challenges import TLSSNI01
self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import DVSNI
hash(DVSNI.from_json(self.jmsg))
from acme.challenges import TLSSNI01
hash(TLSSNI01.from_json(self.jmsg))
def test_from_json_invalid_token_length(self):
from acme.challenges import DVSNI
from acme.challenges import TLSSNI01
self.jmsg['token'] = jose.encode_b64jose(b'abcd')
self.assertRaises(
jose.DeserializationError, DVSNI.from_json, self.jmsg)
jose.DeserializationError, TLSSNI01.from_json, self.jmsg)
def test_gen_response(self):
from acme.challenges import DVSNI
self.assertEqual(self.msg, DVSNI.json_loads(
self.msg.gen_response(KEY).validation.payload.decode()))
class DVSNIResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import DVSNI
self.chall = DVSNI(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
from acme.challenges import DVSNIResponse
self.validation = jose.JWS.sign(
payload=self.chall.json_dumps(sort_keys=True).encode(),
key=KEY, alg=jose.RS256)
self.msg = DVSNIResponse(validation=self.validation)
self.jmsg_to = {
'resource': 'challenge',
'type': 'dvsni',
'validation': self.validation,
}
self.jmsg_from = {
'resource': 'challenge',
'type': 'dvsni',
'validation': self.validation.to_json(),
}
# pylint: disable=invalid-name
label1 = b'e2df3498860637c667fedadc5a8494ec'
label2 = b'09dcc75553c9b3bd73662b50e71b1e42'
self.z = label1 + label2
self.z_domain = label1 + b'.' + label2 + b'.acme.invalid'
self.domain = 'foo.com'
def test_z_and_domain(self):
self.assertEqual(self.z, self.msg.z)
self.assertEqual(self.z_domain, self.msg.z_domain)
def test_to_partial_json(self):
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import DVSNIResponse
self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import DVSNIResponse
hash(DVSNIResponse.from_json(self.jmsg_from))
@mock.patch('acme.challenges.socket.gethostbyname')
@mock.patch('acme.challenges.crypto_util.probe_sni')
def test_probe_cert(self, mock_probe_sni, mock_gethostbyname):
mock_gethostbyname.return_value = '127.0.0.1'
self.msg.probe_cert('foo.com')
mock_gethostbyname.assert_called_once_with('foo.com')
mock_probe_sni.assert_called_once_with(
host='127.0.0.1', port=self.msg.PORT,
name=self.z_domain)
self.msg.probe_cert('foo.com', host='8.8.8.8')
mock_probe_sni.assert_called_with(
host='8.8.8.8', port=mock.ANY, name=mock.ANY)
self.msg.probe_cert('foo.com', port=1234)
mock_probe_sni.assert_called_with(
host=mock.ANY, port=1234, name=mock.ANY)
self.msg.probe_cert('foo.com', bar='baz')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz')
self.msg.probe_cert('foo.com', name=b'xxx')
mock_probe_sni.assert_called_with(
host=mock.ANY, port=mock.ANY,
name=self.z_domain)
def test_gen_verify_cert(self):
key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem')
cert, key2 = self.msg.gen_cert(key1)
self.assertEqual(key1, key2)
self.assertTrue(self.msg.verify_cert(cert))
def test_gen_verify_cert_gen_key(self):
cert, key = self.msg.gen_cert()
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
self.assertTrue(self.msg.verify_cert(cert))
def test_verify_bad_cert(self):
self.assertFalse(self.msg.verify_cert(test_util.load_cert('cert.pem')))
def test_simple_verify_wrong_account_key(self):
self.assertFalse(self.msg.simple_verify(
self.chall, self.domain, jose.JWKRSA.load(
test_util.load_vector('rsa256_key.pem')).public_key()))
def test_simple_verify_wrong_payload(self):
for payload in b'', b'{}':
msg = self.msg.update(validation=jose.JWS.sign(
payload=payload, key=KEY, alg=jose.RS256))
self.assertFalse(msg.simple_verify(
self.chall, self.domain, KEY.public_key()))
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(),
key=KEY, alg=jose.RS256))
self.assertFalse(msg.simple_verify(
self.chall, self.domain, KEY.public_key()))
@mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True)
def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification
self.assertEqual(mock.sentinel.verification, self.msg.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert)
@mock.patch('acme.challenges.DVSNIResponse.probe_cert')
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
mock_probe_cert.side_effect = errors.Error
self.assertFalse(self.msg.simple_verify(
self.chall, self.domain, KEY.public_key()))
@mock.patch('acme.challenges.TLSSNI01Response.gen_cert')
def test_validation(self, mock_gen_cert):
mock_gen_cert.return_value = ('cert', 'key')
self.assertEqual(('cert', 'key'), self.msg.validation(
KEY, cert_key=mock.sentinel.cert_key))
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
class RecoveryContactTest(unittest.TestCase):
@@ -571,8 +550,6 @@ 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=jose.b64decode(
b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))
@@ -594,34 +571,33 @@ class DNSTest(unittest.TestCase):
def test_gen_check_validation(self):
self.assertTrue(self.msg.check_validation(
self.msg.gen_validation(self.account_key),
self.account_key.public_key()))
self.msg.gen_validation(KEY), 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()))
self.msg.gen_validation(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)
jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY)
for payload in (b'', b'{}')
)
for validation in validations:
self.assertFalse(self.msg.check_validation(
validation, self.account_key.public_key()))
validation, 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)
alg=jose.RS256, key=KEY)
self.assertFalse(self.msg.check_validation(
bad_validation, self.account_key.public_key()))
bad_validation, 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)
response = self.msg.gen_response(KEY)
from acme.challenges import DNSResponse
self.assertTrue(isinstance(response, DNSResponse))
self.assertEqual(response.validation, mock.sentinel.validation)

View File

@@ -481,11 +481,13 @@ class ClientNetwork(object):
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
REPLAY_NONCE_HEADER = 'Replay-Nonce'
def __init__(self, key, alg=jose.RS256, verify_ssl=True):
def __init__(self, key, alg=jose.RS256, verify_ssl=True,
user_agent='acme-python'):
self.key = key
self.alg = alg
self.verify_ssl = verify_ssl
self._nonces = set()
self.user_agent = user_agent
def _wrap_in_jws(self, obj, nonce):
"""Wrap `JSONDeSerializable` object in JWS.
@@ -578,6 +580,8 @@ class ClientNetwork(object):
logging.debug('Sending %s request to %s. args: %r, kwargs: %r',
method, url, args, kwargs)
kwargs['verify'] = self.verify_ssl
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('User-Agent', self.user_agent)
response = requests.request(method, url, *args, **kwargs)
logging.debug('Received %s. Headers: %s. Content: %r',
response, response.headers, response.content)

View File

@@ -396,7 +396,8 @@ class ClientNetworkTest(unittest.TestCase):
from acme.client import ClientNetwork
self.net = ClientNetwork(
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl)
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl,
user_agent='acme-python-test')
self.response = mock.MagicMock(ok=True, status_code=http_client.OK)
self.response.headers = {}
@@ -479,7 +480,7 @@ class ClientNetworkTest(unittest.TestCase):
self.assertEqual(self.response, self.net._send_request(
'HEAD', 'url', 'foo', bar='baz'))
mock_requests.request.assert_called_once_with(
'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz')
'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY)
@mock.patch('acme.client.requests')
def test_send_request_verify_ssl(self, mock_requests):
@@ -492,7 +493,20 @@ class ClientNetworkTest(unittest.TestCase):
self.assertEqual(
self.response, self.net._send_request('GET', 'url'))
mock_requests.request.assert_called_once_with(
'GET', 'url', verify=verify)
'GET', 'url', verify=verify, headers=mock.ANY)
@mock.patch('acme.client.requests')
def test_send_request_user_agent(self, mock_requests):
mock_requests.request.return_value = self.response
# pylint: disable=protected-access
self.net._send_request('GET', 'url', headers={'bar': 'baz'})
mock_requests.request.assert_called_once_with(
'GET', 'url', verify=mock.ANY,
headers={'User-Agent': 'acme-python-test', 'bar': 'baz'})
self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'})
mock_requests.request.assert_called_with(
'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'})
@mock.patch('acme.client.requests')
def test_requests_error_passthrough(self, mock_requests):

View File

@@ -13,7 +13,7 @@ from acme import errors
logger = logging.getLogger(__name__)
# DVSNI certificate serving and probing is not affected by SSL
# TLSSNI01 certificate serving and probing is not affected by SSL
# vulnerabilities: prober needs to check certificate for expected
# contents anyway. Working SNI is the only thing that's necessary for
# the challenge and thus scoping down SSL/TLS method (version) would
@@ -23,7 +23,7 @@ logger = logging.getLogger(__name__)
# https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni
# should be changed to use "set_options" to disable SSLv2 and SSLv3,
# in case it's used for things other than probing/serving!
_DEFAULT_DVSNI_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD
_DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD
class SSLSocket(object): # pylint: disable=too-few-public-methods
@@ -35,7 +35,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
:ivar method: See `OpenSSL.SSL.Context` for allowed values.
"""
def __init__(self, sock, certs, method=_DEFAULT_DVSNI_SSL_METHOD):
def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD):
self.sock = sock
self.certs = certs
self.method = method
@@ -103,7 +103,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
def probe_sni(name, host, port=443, timeout=300,
method=_DEFAULT_DVSNI_SSL_METHOD, source_address=('0', 0)):
method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)):
"""Probe SNI server for SSL certificate.
:param bytes name: Byte string to send as the server name in the

View File

@@ -176,5 +176,5 @@ PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384))
PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512))
ES256 = JWASignature.register(_JWAES('ES256'))
ES256 = JWASignature.register(_JWAES('ES384'))
ES256 = JWASignature.register(_JWAES('ES512'))
ES384 = JWASignature.register(_JWAES('ES384'))
ES512 = JWASignature.register(_JWAES('ES512'))

View File

@@ -6,7 +6,6 @@ import logging
import os
import sys
import six
from six.moves import BaseHTTPServer # pylint: disable=import-error
from six.moves import http_client # pylint: disable=import-error
from six.moves import socketserver # pylint: disable=import-error
@@ -30,7 +29,7 @@ class TLSServer(socketserver.TCPServer):
self.certs = kwargs.pop("certs", {})
self.method = kwargs.pop(
# pylint: disable=protected-access
"method", crypto_util._DEFAULT_DVSNI_SSL_METHOD)
"method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD)
self.allow_reuse_address = kwargs.pop("allow_reuse_address", True)
socketserver.TCPServer.__init__(self, *args, **kwargs)
@@ -50,12 +49,25 @@ class ACMEServerMixin: # pylint: disable=old-style-class
allow_reuse_address = True
class DVSNIServer(TLSServer, ACMEServerMixin):
"""DVSNI Server."""
class TLSSNI01Server(TLSServer, ACMEServerMixin):
"""TLSSNI01 Server."""
def __init__(self, server_address, certs):
TLSServer.__init__(
self, server_address, socketserver.BaseRequestHandler, certs=certs)
self, server_address, BaseRequestHandlerWithLogging, certs=certs)
class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler):
"""BaseRequestHandler with logging."""
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Log arbitrary message."""
logger.debug("%s - - %s", self.client_address[0], format % args)
def handle(self):
"""Handle request."""
self.log_message("Incoming request")
socketserver.BaseRequestHandler.handle(self)
class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin):
@@ -83,6 +95,15 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
socketserver.BaseRequestHandler.__init__(self, *args, **kwargs)
def log_message(self, format, *args): # pylint: disable=redefined-builtin
"""Log arbitrary message."""
logger.debug("%s - - %s", self.client_address[0], format % args)
def handle(self):
"""Handle request."""
self.log_message("Incoming request")
BaseHTTPServer.BaseHTTPRequestHandler.handle(self)
def do_GET(self): # pylint: disable=invalid-name,missing-docstring
if self.path == "/":
self.handle_index()
@@ -109,17 +130,17 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""Handle HTTP01 provisioned resources."""
for resource in self.simple_http_resources:
if resource.chall.path == self.path:
logger.debug("Serving HTTP01 with token %r",
resource.chall.encode("token"))
self.log_message("Serving HTTP01 with token %r",
resource.chall.encode("token"))
self.send_response(http_client.OK)
self.send_header("Content-type", resource.chall.CONTENT_TYPE)
self.end_headers()
self.wfile.write(resource.validation.encode())
return
else: # pylint: disable=useless-else-on-loop
logger.debug("No resources to serve")
logger.debug("%s does not correspond to any resource. ignoring",
self.path)
self.log_message("No resources to serve")
self.log_message("%s does not correspond to any resource. ignoring",
self.path)
@classmethod
def partial_init(cls, simple_http_resources):
@@ -134,8 +155,8 @@ class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
cls, simple_http_resources=simple_http_resources)
def simple_dvsni_server(cli_args, forever=True):
"""Run simple standalone DVSNI server."""
def simple_tls_sni_01_server(cli_args, forever=True):
"""Run simple standalone TLSSNI01 server."""
logging.basicConfig(level=logging.DEBUG)
parser = argparse.ArgumentParser()
@@ -158,9 +179,8 @@ def simple_dvsni_server(cli_args, forever=True):
OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert_contents))
server = DVSNIServer(('', int(args.port)), certs=certs)
six.print_("Serving at https://localhost:{0}...".format(
server.socket.getsockname()[1]))
server = TLSSNI01Server(('', int(args.port)), certs=certs)
logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2])
if forever: # pragma: no cover
server.serve_forever()
else:
@@ -168,4 +188,4 @@ def simple_dvsni_server(cli_args, forever=True):
if __name__ == "__main__":
sys.exit(simple_dvsni_server(sys.argv)) # pragma: no cover
sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover

View File

@@ -28,8 +28,8 @@ class TLSServerTest(unittest.TestCase):
server.server_close() # pylint: disable=no-member
class DVSNIServerTest(unittest.TestCase):
"""Test for acme.standalone.DVSNIServer."""
class TLSSNI01ServerTest(unittest.TestCase):
"""Test for acme.standalone.TLSSNI01Server."""
def setUp(self):
self.certs = {
@@ -37,8 +37,8 @@ class DVSNIServerTest(unittest.TestCase):
# pylint: disable=protected-access
test_util.load_cert('cert.pem')._wrapped),
}
from acme.standalone import DVSNIServer
self.server = DVSNIServer(("", 0), certs=self.certs)
from acme.standalone import TLSSNI01Server
self.server = TLSSNI01Server(("", 0), certs=self.certs)
# pylint: disable=no-member
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
@@ -106,8 +106,8 @@ class HTTP01ServerTest(unittest.TestCase):
self.assertFalse(self._test_http01(add=False))
class TestSimpleDVSNIServer(unittest.TestCase):
"""Tests for acme.standalone.simple_dvsni_server."""
class TestSimpleTLSSNI01Server(unittest.TestCase):
"""Tests for acme.standalone.simple_tls_sni_01_server."""
def setUp(self):
# mirror ../examples/standalone
@@ -118,12 +118,14 @@ class TestSimpleDVSNIServer(unittest.TestCase):
shutil.copy(test_util.vector_path('rsa512_key.pem'),
os.path.join(localhost_dir, 'key.pem'))
from acme.standalone import simple_dvsni_server
from acme.standalone import simple_tls_sni_01_server
self.port = 1234
self.thread = threading.Thread(target=simple_dvsni_server, kwargs={
'cli_args': ('xxx', '--port', str(self.port)),
'forever': False,
})
self.thread = threading.Thread(
target=simple_tls_sni_01_server, kwargs={
'cli_args': ('xxx', '--port', str(self.port)),
'forever': False,
},
)
self.old_cwd = os.getcwd()
os.chdir(self.test_cwd)
self.thread.start()

View File

@@ -227,25 +227,25 @@ htmlhelp_basename = 'acme-pythondoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'acme-python.tex', u'acme-python Documentation',
u'Let\'s Encrypt Project', 'manual'),
(master_doc, 'acme-python.tex', u'acme-python Documentation',
u'Let\'s Encrypt Project', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -289,9 +289,9 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'acme-python', u'acme-python Documentation',
author, 'acme-python', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'acme-python', u'acme-python Documentation',
author, 'acme-python', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.

View File

@@ -44,7 +44,7 @@ apt-get install -y --no-install-recommends \
libffi-dev \
ca-certificates \
if ! which virtualenv > /dev/null ; then
if ! command -v virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi

View File

@@ -16,10 +16,12 @@ else
fi
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
# Amazon Linux 2015.03 needs python27-virtualenv rather than python-virtualenv
$tool install -y \
git-core \
python \
python-devel \
python27-virtualenv \
python-virtualenv \
gcc \
dialog \

14
bootstrap/_suse_common.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# SLE12 dont have python-virtualenv
zypper -nq in -l git-core \
python \
python-devel \
python-virtualenv \
gcc \
dialog \
augeas-lenses \
libopenssl-devel \
libffi-devel \
ca-certificates \

View File

@@ -29,6 +29,9 @@ elif [ -f /etc/gentoo-release ] ; then
elif uname | grep -iq FreeBSD ; then
echo "Bootstrapping dependencies for FreeBSD..."
$SUDO $BOOTSTRAP/freebsd.sh
elif `grep -qs openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE.."
$SUDO $BOOTSTRAP/suse.sh
elif uname | grep -iq Darwin ; then
echo "Bootstrapping dependencies for Mac OS X..."
echo "WARNING: Mac support is very experimental at present..."

1
bootstrap/suse.sh Symbolic link
View File

@@ -0,0 +1 @@
_suse_common.sh

View File

@@ -230,25 +230,25 @@ htmlhelp_basename = 'LetsEncryptdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation',
u'Let\'s Encrypt Project', 'manual'),
('index', 'LetsEncrypt.tex', u'Let\'s Encrypt Documentation',
u'Let\'s Encrypt Project', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -295,9 +295,9 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation',
u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.',
'Miscellaneous'),
('index', 'LetsEncrypt', u'Let\'s Encrypt Documentation',
u'Let\'s Encrypt Project', 'LetsEncrypt', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.

View File

@@ -151,7 +151,7 @@ certificate for some domain name by solving challenges received from
the ACME server. From the protocol, there are essentially two
different types of challenges. Challenges that must be solved by
individual plugins in order to satisfy domain validation (subclasses
of `~.DVChallenge`, i.e. `~.challenges.DVSNI`,
of `~.DVChallenge`, i.e. `~.challenges.TLSSNI01`,
`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific
challenges (subclasses of `~.ContinuityChallenge`,
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
@@ -160,7 +160,7 @@ always handled by the `~.ContinuityAuthenticator`, while plugins are
expected to handle `~.DVChallenge` types.
Right now, we have two authenticator plugins, the `~.ApacheConfigurator`
and the `~.StandaloneAuthenticator`. The Standalone and Apache
authenticators only solve the `~.challenges.DVSNI` challenge currently.
authenticators only solve the `~.challenges.TLSSNI01` challenge currently.
(You can set which challenges your authenticator can handle through the
:meth:`~.IAuthenticator.get_chall_pref`.

View File

@@ -86,8 +86,15 @@ in ``/etc/letsencrypt/live`` on the host.
.. _`install Docker`: https://docs.docker.com/userguide/
Distro packages
---------------
Operating System Packages
--------------------------
**FreeBSD**
* Port: ``cd /usr/ports/security/py-letsencrypt && make install clean``
* Package: ``pkg install py27-letsencrypt``
**Other Operating Systems**
Unfortunately, this is an ongoing effort. If you'd like to package
Let's Encrypt client for your distribution of choice please have a
@@ -128,7 +135,7 @@ Plugin A I Notes and status
========== = = ================================================================
standalone Y N Very stable. Uses port 80 (force by
``--standalone-supported-challenges http-01``) or 443
(force by ``--standalone-supported-challenges dvsni``).
(force by ``--standalone-supported-challenges tls-sni-01``).
apache Y Y Alpha. Automates Apache installation, works fairly well but on
Debian-based distributions only for now.
webroot Y N Works with already running webserver, by writing necessary files
@@ -197,7 +204,7 @@ The following files are available:
.. warning:: This **must be kept secret at all times**! Never share
it with anyone, including Let's Encrypt developers. You cannot
put it into safe, however - your server still needs to access
put it into a safe, however - your server still needs to access
this file in order for SSL/TLS to work.
This is what Apache needs for `SSLCertificateKeyFile

View File

@@ -16,7 +16,7 @@ server = https://acme-staging.api.letsencrypt.org/directory
# Uncomment to use the standalone authenticator on port 443
# authenticator = standalone
# standalone-supported-challenges = dvsni
# standalone-supported-challenges = tls-sni-01
# Uncomment to use the webroot authenticator. Replace webroot-path with the
# path to the public_html / webroot folder being served by your web server.

View File

@@ -232,25 +232,25 @@ htmlhelp_basename = 'letsencrypt-apachedoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation',
u'Let\'s Encrypt Project', 'manual'),
(master_doc, 'letsencrypt-apache.tex', u'letsencrypt-apache Documentation',
u'Let\'s Encrypt Project', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -293,9 +293,9 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation',
author, 'letsencrypt-apache', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'letsencrypt-apache', u'letsencrypt-apache Documentation',
author, 'letsencrypt-apache', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.

View File

@@ -73,7 +73,8 @@ class AugeasConfigurator(common.Plugin):
This function first checks for save errors, if none are found,
all configuration changes made will be saved. According to the
function parameters.
function parameters. If an exception is raised, a new checkpoint
was not created.
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a
@@ -82,8 +83,9 @@ class AugeasConfigurator(common.Plugin):
:param bool temporary: Indicates whether the changes made will
be quickly reversed in the future (ie. challenges)
:raises .errors.PluginError: If there was an error in Augeas, in an
attempt to save the configuration, or an error creating a checkpoint
:raises .errors.PluginError: If there was an error in Augeas, in
an attempt to save the configuration, or an error creating a
checkpoint
"""
save_state = self.aug.get("/augeas/save")
@@ -122,16 +124,16 @@ class AugeasConfigurator(common.Plugin):
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.aug.set("/augeas/save", save_state)
self.save_notes = ""
self.aug.save()
if title and not temporary:
try:
self.reverter.finalize_checkpoint(title)
except errors.ReverterError as err:
raise errors.PluginError(str(err))
self.aug.set("/augeas/save", save_state)
self.save_notes = ""
self.aug.save()
def _log_save_errors(self, ex_errs):
"""Log errors due to bad Augeas save.

View File

@@ -13,7 +13,6 @@ import zope.interface
from acme import challenges
from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
@@ -163,7 +162,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# Get all of the available vhosts
self.vhosts = self.get_virtual_hosts()
temp_install(self.mod_ssl_conf)
install_ssl_options_conf(self.mod_ssl_conf)
def deploy_cert(self, domain, cert_path, key_path,
chain_path=None, fullchain_path=None): # pylint: disable=unused-argument
@@ -308,6 +307,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
best_points = 0
for vhost in self.vhosts:
if vhost.modmacro is True:
continue
if target_name in vhost.get_names():
points = 2
elif any(addr.get_addr() == target_name for addr in vhost.addrs):
@@ -327,7 +328,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
# No winners here... is there only one reasonable vhost?
if best_candidate is None:
# reasonable == Not all _default_ addrs
reasonable_vhosts = self._non_default_vhosts()
vhosts = self._non_default_vhosts()
# remove mod_macro hosts from reasonable vhosts
reasonable_vhosts = [vh for vh
in vhosts if vh.modmacro is False]
if len(reasonable_vhosts) == 1:
best_candidate = reasonable_vhosts[0]
@@ -349,8 +353,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"""
all_names = set()
vhost_macro = []
for vhost in self.vhosts:
all_names.update(vhost.get_names())
if vhost.modmacro:
vhost_macro.append(vhost.filep)
for addr in vhost.addrs:
if common.hostname_regex.match(addr.get_addr()):
@@ -360,6 +368,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
if name:
all_names.add(name)
if len(vhost_macro) > 0:
zope.component.getUtility(interfaces.IDisplay).notification(
"Apache mod_macro seems to be in use in file(s):\n{0}"
"\n\nUnfortunately mod_macro is not yet supported".format(
"\n ".join(vhost_macro)))
return all_names
def get_name_from_ip(self, addr): # pylint: disable=no-self-use
@@ -396,11 +410,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
"ServerAlias", None, start=host.path, exclude=False)
for alias in serveralias_match:
host.aliases.add(self.parser.get_arg(alias))
serveralias = self.parser.get_arg(alias)
if not host.modmacro:
host.aliases.add(serveralias)
if servername_match:
# Get last ServerName as each overwrites the previous
host.name = self.parser.get_arg(servername_match[-1])
servername = self.parser.get_arg(servername_match[-1])
if not host.modmacro:
host.name = servername
def _create_vhost(self, path):
"""Used by get_virtual_hosts to create vhost objects
@@ -423,7 +441,12 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
filename = get_file_path(path)
is_enabled = self.is_site_enabled(filename)
vhost = obj.VirtualHost(filename, path, addrs, is_ssl, is_enabled)
macro = False
if "/macro/" in path.lower():
macro = True
vhost = obj.VirtualHost(filename, path, addrs, is_ssl,
is_enabled, modmacro=macro)
self._add_servernames(vhost)
return vhost
@@ -1179,7 +1202,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
###########################################################################
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
"""Return list of challenge preferences."""
return [challenges.DVSNI]
return [challenges.TLSSNI01]
def perform(self, achalls):
"""Perform the configuration related challenge.
@@ -1194,11 +1217,10 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
apache_dvsni = dvsni.ApacheDvsni(self)
for i, achall in enumerate(achalls):
if isinstance(achall, achallenges.DVSNI):
# Currently also have dvsni hold associated index
# of the challenge. This helps to put all of the responses back
# together when they are all complete.
apache_dvsni.add_chall(achall, i)
# Currently also have dvsni hold associated index
# of the challenge. This helps to put all of the responses back
# together when they are all complete.
apache_dvsni.add_chall(achall, i)
sni_response = apache_dvsni.perform()
if sni_response:
@@ -1298,7 +1320,7 @@ def get_file_path(vhost_path):
avail_fp = vhost_path[6:]
# This can be optimized...
while True:
# Cast both to lowercase to be case insensitive
# Cast all to lowercase to be case insensitive
find_if = avail_fp.lower().find("/ifmodule")
if find_if != -1:
avail_fp = avail_fp[:find_if]
@@ -1307,16 +1329,26 @@ def get_file_path(vhost_path):
if find_vh != -1:
avail_fp = avail_fp[:find_vh]
continue
find_macro = avail_fp.lower().find("/macro")
if find_macro != -1:
avail_fp = avail_fp[:find_macro]
continue
break
return avail_fp
def temp_install(options_ssl):
"""Temporary install for convenience."""
# WARNING: THIS IS A POTENTIAL SECURITY VULNERABILITY
# THIS SHOULD BE HANDLED BY THE PACKAGE MANAGER
# AND TAKEN OUT BEFORE RELEASE, INSTEAD
# SHOWING A NICE ERROR MESSAGE ABOUT THE PROBLEM.
def install_ssl_options_conf(options_ssl):
"""
Copy Let's Encrypt's SSL options file into the system's config dir if
required.
"""
# XXX if we ever try to enforce a local privilege boundary (eg, running
# letsencrypt for unprivileged users via setuid), this function will need
# to be modified.
# XXX if the user is in security-autoupdate mode, we should be willing to
# overwrite the options_ssl file at least if it's unmodified:
# https://github.com/letsencrypt/letsencrypt/issues/1123
# Check to make sure options-ssl.conf is installed
if not os.path.isfile(options_ssl):

View File

@@ -7,14 +7,14 @@ from letsencrypt_apache import obj
from letsencrypt_apache import parser
class ApacheDvsni(common.Dvsni):
class ApacheDvsni(common.TLSSNI01):
"""Class performs DVSNI challenges within the Apache configurator.
:ivar configurator: ApacheConfigurator object
:type configurator: :class:`~apache.configurator.ApacheConfigurator`
:ivar list achalls: Annotated :class:`~letsencrypt.achallenges.DVSNI`
challenges.
:ivar list achalls: Annotated tls-sni-01
(`.KeyAuthorizationAnnotatedChallenge`) challenges.
:param list indices: Meant to hold indices of challenges in a
larger array. ApacheDvsni is capable of solving many challenges
@@ -62,7 +62,7 @@ class ApacheDvsni(common.Dvsni):
# Prepare the server for HTTPS
self.configurator.prepare_server_https(
str(self.configurator.config.dvsni_port), True)
str(self.configurator.config.tls_sni_01_port), True)
responses = []
@@ -114,14 +114,15 @@ class ApacheDvsni(common.Dvsni):
# TODO: Checkout _default_ rules.
dvsni_addrs = set()
default_addr = obj.Addr(("*", str(self.configurator.config.dvsni_port)))
default_addr = obj.Addr(("*", str(
self.configurator.config.tls_sni_01_port)))
for addr in vhost.addrs:
if "_default_" == addr.get_addr():
dvsni_addrs.add(default_addr)
else:
dvsni_addrs.add(
addr.get_sni_addr(self.configurator.config.dvsni_port))
addr.get_sni_addr(self.configurator.config.tls_sni_01_port))
return dvsni_addrs
@@ -144,8 +145,8 @@ class ApacheDvsni(common.Dvsni):
def _get_config_text(self, achall, ip_addrs):
"""Chocolate virtual server configuration text
:param achall: Annotated DVSNI challenge.
:type achall: :class:`letsencrypt.achallenges.DVSNI`
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
DVSNI challenge.
:param list ip_addrs: addresses of challenged domain
:class:`list` of type `~.obj.Addr`
@@ -164,7 +165,7 @@ class ApacheDvsni(common.Dvsni):
# https://docs.python.org/2.7/reference/lexical_analysis.html
return self.VHOST_TEMPLATE.format(
vhost=ips,
server_name=achall.gen_response(achall.account_key).z_domain,
server_name=achall.response(achall.account_key).z_domain,
ssl_options_conf_path=self.configurator.mod_ssl_conf,
cert_path=self.get_cert_path(achall),
key_path=self.get_key_path(achall),

View File

@@ -102,6 +102,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
:ivar bool ssl: SSLEngine on in vhost
:ivar bool enabled: Virtual host is enabled
:ivar bool modmacro: VirtualHost is using mod_macro
https://httpd.apache.org/docs/2.4/vhosts/details.html
@@ -112,7 +113,9 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
# ?: is used for not returning enclosed characters
strip_name = re.compile(r"^(?:.+://)?([^ :$]*)")
def __init__(self, filep, path, addrs, ssl, enabled, name=None, aliases=None):
def __init__(self, filep, path, addrs, ssl, enabled, name=None,
aliases=None, modmacro=False):
# pylint: disable=too-many-arguments
"""Initialize a VH."""
self.filep = filep
@@ -122,6 +125,7 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
self.aliases = aliases if aliases is not None else set()
self.ssl = ssl
self.enabled = enabled
self.modmacro = modmacro
def get_names(self):
"""Return a set of all names."""
@@ -141,21 +145,25 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
"Name: {name}\n"
"Aliases: {aliases}\n"
"TLS Enabled: {tls}\n"
"Site Enabled: {active}".format(
"Site Enabled: {active}\n"
"mod_macro Vhost: {modmacro}".format(
filename=self.filep,
vhpath=self.path,
addrs=", ".join(str(addr) for addr in self.addrs),
name=self.name if self.name is not None else "",
aliases=", ".join(name for name in self.aliases),
tls="Yes" if self.ssl else "No",
active="Yes" if self.enabled else "No"))
active="Yes" if self.enabled else "No",
modmacro="Yes" if self.modmacro else "No"))
def __eq__(self, other):
if isinstance(other, self.__class__):
return (self.filep == other.filep and self.path == other.path and
self.addrs == other.addrs and
self.get_names() == other.get_names() and
self.ssl == other.ssl and self.enabled == other.enabled)
self.ssl == other.ssl and
self.enabled == other.enabled and
self.modmacro == other.modmacro)
return False

View File

@@ -8,12 +8,6 @@ SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA25
SSLHonorCipherOrder on
SSLCompression off
ServerSignature Off
AcceptPathInfo Off
AddOutputFilterByType DEFLATE text/html text/plain text/xml application/pdf
AddDefaultCharset UTF-8
SSLOptions +StrictRequire
# Add vhost name to log entries:

View File

@@ -59,14 +59,20 @@ class TwoVhost80Test(util.ApacheTest):
# Weak test..
ApacheConfigurator.add_parser_arguments(mock.MagicMock())
def test_get_all_names(self):
@mock.patch("zope.component.getUtility")
def test_get_all_names(self, mock_getutility):
mock_getutility.notification = mock.MagicMock(return_value=True)
names = self.config.get_all_names()
self.assertEqual(names, set(
["letsencrypt.demo", "encryption-example.demo", "ip-172-30-0-17"]))
@mock.patch("zope.component.getUtility")
@mock.patch("letsencrypt_apache.configurator.socket.gethostbyaddr")
def test_get_all_names_addrs(self, mock_gethost):
def test_get_all_names_addrs(self, mock_gethost, mock_getutility):
mock_gethost.side_effect = [("google.com", "", ""), socket.error]
notification = mock.Mock()
notification.notification = mock.Mock(return_value=True)
mock_getutility.return_value = notification
vhost = obj.VirtualHost(
"fp", "ap",
set([obj.Addr(("8.8.8.8", "443")),
@@ -97,7 +103,7 @@ class TwoVhost80Test(util.ApacheTest):
"""
vhs = self.config.get_virtual_hosts()
self.assertEqual(len(vhs), 4)
self.assertEqual(len(vhs), 5)
found = 0
for vhost in vhs:
@@ -108,7 +114,7 @@ class TwoVhost80Test(util.ApacheTest):
else:
raise Exception("Missed: %s" % vhost) # pragma: no cover
self.assertEqual(found, 4)
self.assertEqual(found, 5)
@mock.patch("letsencrypt_apache.display_ops.select_vhost")
def test_choose_vhost_none_avail(self, mock_select):
@@ -174,7 +180,7 @@ class TwoVhost80Test(util.ApacheTest):
def test_non_default_vhosts(self):
# pylint: disable=protected-access
self.assertEqual(len(self.config._non_default_vhosts()), 3)
self.assertEqual(len(self.config._non_default_vhosts()), 4)
def test_is_site_enabled(self):
"""Test if site is enabled.
@@ -345,7 +351,7 @@ class TwoVhost80Test(util.ApacheTest):
self.assertEqual(self.config.is_name_vhost(self.vh_truth[0]),
self.config.is_name_vhost(ssl_vhost))
self.assertEqual(len(self.config.vhosts), 5)
self.assertEqual(len(self.config.vhosts), 6)
def test_make_vhost_ssl_extra_vhs(self):
self.config.aug.match = mock.Mock(return_value=["p1", "p2"])
@@ -382,8 +388,8 @@ class TwoVhost80Test(util.ApacheTest):
account_key, achall1, achall2 = self.get_achalls()
dvsni_ret_val = [
achall1.gen_response(account_key),
achall2.gen_response(account_key),
achall1.response(account_key),
achall2.response(account_key),
]
mock_dvsni_perform.return_value = dvsni_ret_val
@@ -492,10 +498,10 @@ class TwoVhost80Test(util.ApacheTest):
def test_get_chall_pref(self):
self.assertTrue(isinstance(self.config.get_chall_pref(""), list))
def test_temp_install(self):
from letsencrypt_apache.configurator import temp_install
def test_install_ssl_options_conf(self):
from letsencrypt_apache.configurator import install_ssl_options_conf
path = os.path.join(self.work_dir, "test_it")
temp_install(path)
install_ssl_options_conf(path)
self.assertTrue(os.path.isfile(path))
# TEST ENHANCEMENTS
@@ -665,20 +671,20 @@ class TwoVhost80Test(util.ApacheTest):
self.vh_truth[1].aliases = set(["yes.default.com"])
self.config._enable_redirect(self.vh_truth[1], "") # pylint: disable=protected-access
self.assertEqual(len(self.config.vhosts), 5)
self.assertEqual(len(self.config.vhosts), 6)
def get_achalls(self):
"""Return testing achallenges."""
account_key = self.rsa512jwk
achall1 = achallenges.DVSNI(
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
challenges.TLSSNI01(
token="jIq_Xy1mXGN37tb4L6Xj_es58fW571ZNyXekdZzhh7Q"),
"pending"),
domain="encryption-example.demo", account_key=account_key)
achall2 = achallenges.DVSNI(
achall2 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
challenges.TLSSNI01(
token="uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"),
"pending"),
domain="letsencrypt.demo", account_key=account_key)

View File

@@ -57,7 +57,7 @@ class SelectVhostTest(unittest.TestCase):
@mock.patch("letsencrypt_apache.display_ops.zope.component.getUtility")
def test_multiple_names(self, mock_util):
mock_util().menu.return_value = (display_util.OK, 4)
mock_util().menu.return_value = (display_util.OK, 5)
self.vhosts.append(
obj.VirtualHost(
@@ -65,7 +65,7 @@ class SelectVhostTest(unittest.TestCase):
False, False,
"wildcard.com", set(["*.wildcard.com"])))
self.assertEqual(self.vhosts[4], self._call(self.vhosts))
self.assertEqual(self.vhosts[5], self._call(self.vhosts))
if __name__ == "__main__":

View File

@@ -13,15 +13,15 @@ from letsencrypt_apache.tests import util
class DvsniPerformTest(util.ApacheTest):
"""Test the ApacheDVSNI challenge."""
auth_key = common_test.DvsniTest.auth_key
achalls = common_test.DvsniTest.achalls
auth_key = common_test.TLSSNI01Test.auth_key
achalls = common_test.TLSSNI01Test.achalls
def setUp(self): # pylint: disable=arguments-differ
super(DvsniPerformTest, self).setUp()
config = util.get_apache_configurator(
self.config_path, self.config_dir, self.work_dir)
config.config.dvsni_port = 443
config.config.tls_sni_01_port = 443
from letsencrypt_apache import dvsni
self.sni = dvsni.ApacheDvsni(config)
@@ -46,7 +46,7 @@ class DvsniPerformTest(util.ApacheTest):
achall = self.achalls[0]
self.sni.add_chall(achall)
response = self.achalls[0].gen_response(self.auth_key)
response = self.achalls[0].response(self.auth_key)
mock_setup_cert = mock.MagicMock(return_value=response)
# pylint: disable=protected-access
self.sni._setup_challenge_cert = mock_setup_cert
@@ -72,7 +72,7 @@ class DvsniPerformTest(util.ApacheTest):
acme_responses = []
for achall in self.achalls:
self.sni.add_chall(achall)
acme_responses.append(achall.gen_response(self.auth_key))
acme_responses.append(achall.response(self.auth_key))
mock_setup_cert = mock.MagicMock(side_effect=acme_responses)
# pylint: disable=protected-access
@@ -100,7 +100,7 @@ class DvsniPerformTest(util.ApacheTest):
z_domains = []
for achall in self.achalls:
self.sni.add_chall(achall)
z_domain = achall.gen_response(self.auth_key).z_domain
z_domain = achall.response(self.auth_key).z_domain
z_domains.append(set([z_domain]))
self.sni._mod_config() # pylint: disable=protected-access

View File

@@ -52,7 +52,7 @@ class BasicParserTest(util.ParserTest):
test2 = self.parser.find_dir("documentroot")
self.assertEqual(len(test), 1)
self.assertEqual(len(test2), 3)
self.assertEqual(len(test2), 4)
def test_add_dir(self):
aug_default = "/files" + self.parser.loc["default"]

View File

@@ -0,0 +1,15 @@
<Macro VHost $name $domain>
<VirtualHost *:80>
ServerName $domain
ServerAlias www.$domain
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
</Macro>
Use VHost macro1 test.com
Use VHost macro2 hostname.org
Use VHost macro3 apache.org
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View File

@@ -0,0 +1 @@
../sites-available/mod_macro-example.conf

View File

@@ -124,6 +124,11 @@ def get_vh_truth(temp_dir, config_name):
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True,
"letsencrypt.demo"),
obj.VirtualHost(
os.path.join(prefix, "mod_macro-example.conf"),
os.path.join(aug_pre,
"mod_macro-example.conf/Macro/VirtualHost"),
set([obj.Addr.fromstring("*:80")]), False, True, modmacro=True)
]
return vh_truth

View File

@@ -8,17 +8,16 @@
# without requiring specific versions of its dependencies from the operating
# system.
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
VENV_BIN=${VENV_PATH}/bin
if test "`id -u`" -ne "0" ; then
SUDO=sudo
else
SUDO=
fi
# This script takes the same arguments as the main letsencrypt program, but it
# additionally responds to --verbose (more output) and --debug (allow support
# for experimental platforms)
for arg in "$@" ; do
# This first clause is redundant with the third, but hedging on portability
if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then
@@ -28,12 +27,52 @@ for arg in "$@" ; do
fi
done
# letsencrypt-auto needs root access to bootstrap OS dependencies, and
# letsencrypt itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
# `su`
if test "`id -u`" -ne "0" ; then
if command -v sudo 1>/dev/null 2>&1; then
SUDO=sudo
else
echo \"sudo\" is not available, will use \"su\" for installation steps...
# Because the parameters in `su -c` has to be a string,
# we need properly escape it
su_sudo() {
args=""
# This `while` loop iterates over all parameters given to this function.
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
# will be wrap in a pair of `'`, then append to `$args` string
# For example, `echo "It's only 1\$\!"` will be escaped to:
# 'echo' 'It'"'"'s only 1$!'
# │ │└┼┘│
# │ │ │ └── `'s only 1$!'` the literal string
# │ │ └── `\"'\"` is a single quote (as a string)
# │ └── `'It'`, to be concatenated with the strings followed it
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
while [ $# -ne 0 ]; do
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
shift
done
su root -c "$args"
}
SUDO=su_sudo
fi
else
SUDO=
fi
ExperimentalBootstrap() {
# Arguments: Platform name, boostrap script name, SUDO command (iff needed)
if [ "$DEBUG" = 1 ] ; then
if [ "$2" != "" ] ; then
echo "Bootstrapping dependencies for $1..."
"$3" "$BOOTSTRAP/$2"
if [ "$3" != "" ] ; then
"$3" "$BOOTSTRAP/$2"
else
"$BOOTSTRAP/$2"
fi
fi
else
echo "WARNING: $1 support is very experimental at present..."
@@ -43,13 +82,12 @@ ExperimentalBootstrap() {
fi
}
DeterminePythonVersion() {
if which python2 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2}
elif which python2.7 > /dev/null ; then
if command -v python2.7 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2.7}
elif which python > /dev/null ; then
elif command -v python2 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2}
elif command -v python > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python}
else
echo "Cannot find any Pythons... please install one!"
@@ -82,6 +120,9 @@ then
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO $BOOTSTRAP/_rpm_common.sh
elif `grep -q openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE-based OSes..."
$SUDO $BOOTSTRAP/_suse_common.sh
elif [ -f /etc/arch-release ] ; then
echo "Bootstrapping dependencies for Archlinux..."
$SUDO $BOOTSTRAP/archlinux.sh
@@ -93,6 +134,8 @@ then
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
elif uname | grep -iq Darwin ; then
ExperimentalBootstrap "Mac OS X" mac.sh
elif grep -iq "Amazon Linux" /etc/issue ; then
ExperimentalBootstrap "Amazon Linux" amazon_linux.sh
else
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
echo
@@ -130,7 +173,7 @@ else
$VENV_BIN/pip install -U pip > /dev/null
printf .
# nginx is buggy / disabled for now...
$VENV_BIN/pip install -r py26reqs.txt
$VENV_BIN/pip install -r py26reqs.txt > /dev/null
printf .
$VENV_BIN/pip install -U letsencrypt > /dev/null
printf .

View File

@@ -226,25 +226,26 @@ htmlhelp_basename = 'letsencrypt-compatibility-testdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'letsencrypt-compatibility-test.tex', u'letsencrypt-compatibility-test Documentation',
u'Let\'s Encrypt Project', 'manual'),
(master_doc, 'letsencrypt-compatibility-test.tex',
u'letsencrypt-compatibility-test Documentation',
u'Let\'s Encrypt Project', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -273,7 +274,8 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
(master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation',
(master_doc, 'letsencrypt-compatibility-test',
u'letsencrypt-compatibility-test Documentation',
[author], 1)
]
@@ -287,9 +289,10 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'letsencrypt-compatibility-test', u'letsencrypt-compatibility-test Documentation',
author, 'letsencrypt-compatibility-test', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'letsencrypt-compatibility-test',
u'letsencrypt-compatibility-test Documentation',
author, 'letsencrypt-compatibility-test',
'One line description of project.', 'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
@@ -309,6 +312,8 @@ intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org/en/latest/', None),
'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org/en/latest/', None),
'letsencrypt-apache': (
'https://letsencrypt-apache.readthedocs.org/en/latest/', None),
'letsencrypt-nginx': (
'https://letsencrypt-nginx.readthedocs.org/en/latest/', None),
}

View File

@@ -60,7 +60,7 @@ def test_authenticator(plugin, config, temp_dir):
"Plugin failed to complete %s for %s in %s",
type(achalls[i]), achalls[i].domain, config)
success = False
elif isinstance(responses[i], challenges.DVSNIResponse):
elif isinstance(responses[i], challenges.TLSSNI01):
verify = functools.partial(responses[i].simple_verify, achalls[i],
achalls[i].domain,
util.JWK.public_key(),
@@ -68,10 +68,10 @@ def test_authenticator(plugin, config, temp_dir):
port=plugin.https_port)
if _try_until_true(verify):
logger.info(
"DVSNI verification for %s succeeded", achalls[i].domain)
"tls-sni-01 verification for %s succeeded", achalls[i].domain)
else:
logger.error(
"DVSNI verification for %s in %s failed",
"tls-sni-01 verification for %s in %s failed",
achalls[i].domain, config)
success = False
@@ -99,12 +99,12 @@ def _create_achalls(plugin):
for domain in names:
prefs = plugin.get_chall_pref(domain)
for chall_type in prefs:
if chall_type == challenges.DVSNI:
chall = challenges.DVSNI(
token=os.urandom(challenges.DVSNI.TOKEN_SIZE))
if chall_type == challenges.TLSSNI01:
chall = challenges.TLSSNI01(
token=os.urandom(challenges.TLSSNI01.TOKEN_SIZE))
challb = acme_util.chall_to_challb(
chall, messages.STATUS_PENDING)
achall = achallenges.DVSNI(
achall = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=challb, domain=domain, account_key=util.JWK)
achalls.append(achall)

View File

@@ -225,25 +225,25 @@ htmlhelp_basename = 'letsencrypt-nginxdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation',
u'Let\'s Encrypt Project', 'manual'),
(master_doc, 'letsencrypt-nginx.tex', u'letsencrypt-nginx Documentation',
u'Let\'s Encrypt Project', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -286,9 +286,9 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation',
author, 'letsencrypt-nginx', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'letsencrypt-nginx', u'letsencrypt-nginx Documentation',
author, 'letsencrypt-nginx', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.

View File

@@ -14,7 +14,6 @@ import zope.interface
from acme import challenges
from acme import crypto_util as acme_crypto_util
from letsencrypt import achallenges
from letsencrypt import constants as core_constants
from letsencrypt import crypto_util
from letsencrypt import errors
@@ -108,6 +107,10 @@ class NginxConfigurator(common.Plugin):
# This is called in determine_authenticator and determine_installer
def prepare(self):
"""Prepare the authenticator/installer."""
# Verify Nginx is installed
if not le_util.exe_exists(self.conf('ctl')):
raise errors.NoInstallationError
self.parser = parser.NginxParser(
self.conf('server-root'), self.mod_ssl_conf)
@@ -297,7 +300,7 @@ class NginxConfigurator(common.Plugin):
"""Make a server SSL.
Make a server SSL based on server_name and filename by adding a
``listen IConfig.dvsni_port ssl`` directive to the server block.
``listen IConfig.tls_sni_01_port ssl`` directive to the server block.
.. todo:: Maybe this should create a new block instead of modifying
the existing one?
@@ -307,7 +310,7 @@ class NginxConfigurator(common.Plugin):
"""
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
ssl_block = [['listen', '{0} ssl'.format(self.config.dvsni_port)],
ssl_block = [['listen', '{0} ssl'.format(self.config.tls_sni_01_port)],
# access and error logs necessary for integration
# testing (non-root)
['access_log', os.path.join(
@@ -321,7 +324,8 @@ class NginxConfigurator(common.Plugin):
vhost.filep, vhost.names, ssl_block)
vhost.ssl = True
vhost.raw.extend(ssl_block)
vhost.addrs.add(obj.Addr('', str(self.config.dvsni_port), True, False))
vhost.addrs.add(obj.Addr(
'', str(self.config.tls_sni_01_port), True, False))
def get_all_certs_keys(self):
"""Find all existing keys, certs from configuration.
@@ -536,7 +540,7 @@ class NginxConfigurator(common.Plugin):
###########################################################################
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
"""Return list of challenge preferences."""
return [challenges.DVSNI]
return [challenges.TLSSNI01]
# Entry point in main.py for performing challenges
def perform(self, achalls):
@@ -552,11 +556,10 @@ class NginxConfigurator(common.Plugin):
nginx_dvsni = dvsni.NginxDvsni(self)
for i, achall in enumerate(achalls):
if isinstance(achall, achallenges.DVSNI):
# Currently also have dvsni hold associated index
# of the challenge. This helps to put all of the responses back
# together when they are all complete.
nginx_dvsni.add_chall(achall, i)
# Currently also have dvsni hold associated index
# of the challenge. This helps to put all of the responses back
# together when they are all complete.
nginx_dvsni.add_chall(achall, i)
sni_response = nginx_dvsni.perform()
# Must restart in order to activate the challenges.

View File

@@ -13,7 +13,7 @@ from letsencrypt_nginx import nginxparser
logger = logging.getLogger(__name__)
class NginxDvsni(common.Dvsni):
class NginxDvsni(common.TLSSNI01):
"""Class performs DVSNI challenges within the Nginx configurator.
:ivar configurator: NginxConfigurator object
@@ -48,7 +48,7 @@ class NginxDvsni(common.Dvsni):
addresses = []
default_addr = "{0} default_server ssl".format(
self.configurator.config.dvsni_port)
self.configurator.config.tls_sni_01_port)
for achall in self.achalls:
vhost = self.configurator.choose_vhost(achall.domain)
@@ -99,8 +99,8 @@ class NginxDvsni(common.Dvsni):
for key, body in main:
if key == ['http']:
found_bucket = False
for key, _ in body:
if key == bucket_directive[0]:
for k, _ in body:
if k == bucket_directive[0]:
found_bucket = True
if not found_bucket:
body.insert(0, bucket_directive)
@@ -141,7 +141,7 @@ class NginxDvsni(common.Dvsni):
block = [['listen', str(addr)] for addr in addrs]
block.extend([['server_name',
achall.gen_response(achall.account_key).z_domain],
achall.response(achall.account_key).z_domain],
['include', self.configurator.parser.loc["ssl_options"]],
# access and error logs necessary for
# integration testing (non-root)

View File

@@ -246,7 +246,7 @@ class NginxParser(object):
# Can't be a server block
return False
if item[0] == 'server_name':
if len(item) > 0 and item[0] == 'server_name':
server_names.update(_get_servernames(item[1]))
return server_names == names
@@ -425,7 +425,7 @@ def _is_include_directive(entry):
"""
return (isinstance(entry, list) and
entry[0] == 'include' and len(entry) == 2 and
len(entry) == 2 and entry[0] == 'include' and
isinstance(entry[1], str))

View File

@@ -1,3 +1,4 @@
# pylint: disable=too-many-public-methods
"""Test for letsencrypt_nginx.configurator."""
import os
import shutil
@@ -29,6 +30,12 @@ class NginxConfiguratorTest(util.NginxTest):
shutil.rmtree(self.config_dir)
shutil.rmtree(self.work_dir)
@mock.patch("letsencrypt_nginx.configurator.le_util.exe_exists")
def test_prepare_no_install(self, mock_exe_exists):
mock_exe_exists.return_value = False
self.assertRaises(
errors.NoInstallationError, self.config.prepare)
def test_prepare(self):
self.assertEquals((1, 6, 2), self.config.version)
self.assertEquals(5, len(self.config.parser.parsed))
@@ -51,7 +58,7 @@ class NginxConfiguratorTest(util.NginxTest):
errors.PluginError, self.config.enhance, 'myhost', 'redirect')
def test_get_chall_pref(self):
self.assertEqual([challenges.DVSNI],
self.assertEqual([challenges.TLSSNI01],
self.config.get_chall_pref('myhost'))
def test_save(self):
@@ -210,22 +217,22 @@ class NginxConfiguratorTest(util.NginxTest):
def test_perform(self, mock_restart, mock_dvsni_perform):
# Only tests functionality specific to configurator.perform
# Note: As more challenges are offered this will have to be expanded
achall1 = achallenges.DVSNI(
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=messages.ChallengeBody(
chall=challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"),
chall=challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"),
uri="https://ca.org/chall0_uri",
status=messages.Status("pending"),
), domain="localhost", account_key=self.rsa512jwk)
achall2 = achallenges.DVSNI(
achall2 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=messages.ChallengeBody(
chall=challenges.DVSNI(token="m8TdO1qik4JVFtgPPurJmg"),
chall=challenges.TLSSNI01(token="m8TdO1qik4JVFtgPPurJmg"),
uri="https://ca.org/chall1_uri",
status=messages.Status("pending"),
), domain="example.com", account_key=self.rsa512jwk)
dvsni_ret_val = [
achall1.gen_response(self.rsa512jwk),
achall2.gen_response(self.rsa512jwk),
achall1.response(self.rsa512jwk),
achall2.response(self.rsa512jwk),
]
mock_dvsni_perform.return_value = dvsni_ret_val

View File

@@ -19,22 +19,22 @@ from letsencrypt_nginx.tests import util
class DvsniPerformTest(util.NginxTest):
"""Test the NginxDVSNI challenge."""
account_key = common_test.DvsniTest.auth_key
account_key = common_test.TLSSNI01Test.auth_key
achalls = [
achallenges.DVSNI(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"),
challenges.TLSSNI01(token="kNdwjwOeX0I_A8DXt9Msmg"), "pending"),
domain="www.example.com", account_key=account_key),
achallenges.DVSNI(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
challenges.TLSSNI01(
token="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y"
"\x80\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945"
), "pending"),
domain="blah", account_key=account_key),
achallenges.DVSNI(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(
challenges.TLSSNI01(
token="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd"
"\xeb9\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4"
), "pending"),
@@ -70,7 +70,7 @@ class DvsniPerformTest(util.NginxTest):
@mock.patch("letsencrypt_nginx.configurator.NginxConfigurator.save")
def test_perform1(self, mock_save):
self.sni.add_chall(self.achalls[0])
response = self.achalls[0].gen_response(self.account_key)
response = self.achalls[0].response(self.account_key)
mock_setup_cert = mock.MagicMock(return_value=response)
# pylint: disable=protected-access
@@ -92,7 +92,7 @@ class DvsniPerformTest(util.NginxTest):
acme_responses = []
for achall in self.achalls:
self.sni.add_chall(achall)
acme_responses.append(achall.gen_response(self.account_key))
acme_responses.append(achall.response(self.account_key))
mock_setup_cert = mock.MagicMock(side_effect=acme_responses)
# pylint: disable=protected-access
@@ -139,9 +139,9 @@ class DvsniPerformTest(util.NginxTest):
for vhost in vhs:
if vhost.addrs == set(v_addr1):
response = self.achalls[0].gen_response(self.account_key)
response = self.achalls[0].response(self.account_key)
else:
response = self.achalls[2].gen_response(self.account_key)
response = self.achalls[2].response(self.account_key)
self.assertEqual(vhost.addrs, set(v_addr2))
self.assertEqual(vhost.names, set([response.z_domain]))

View File

@@ -49,21 +49,25 @@ def get_nginx_configurator(
backups = os.path.join(work_dir, "backups")
config = configurator.NginxConfigurator(
config=mock.MagicMock(
nginx_server_root=config_path,
le_vhost_ext="-le-ssl.conf",
config_dir=config_dir,
work_dir=work_dir,
backup_dir=backups,
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
server="https://acme-server.org:443/new",
dvsni_port=5001,
),
name="nginx",
version=version)
config.prepare()
with mock.patch("letsencrypt_nginx.configurator.le_util."
"exe_exists") as mock_exe_exists:
mock_exe_exists.return_value = True
config = configurator.NginxConfigurator(
config=mock.MagicMock(
nginx_server_root=config_path,
le_vhost_ext="-le-ssl.conf",
config_dir=config_dir,
work_dir=work_dir,
backup_dir=backups,
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
server="https://acme-server.org:443/new",
tls_sni_01_port=5001,
),
name="nginx",
version=version)
config.prepare()
# Provide general config utility.
nsconfig = configuration.NamespaceConfig(config.config)

View File

@@ -49,40 +49,10 @@ class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge):
"""Client annotated `KeyAuthorizationChallenge` challenge."""
__slots__ = ('challb', 'domain', 'account_key')
def response_and_validation(self):
def response_and_validation(self, *args, **kwargs):
"""Generate response and validation."""
return self.challb.chall.response_and_validation(self.account_key)
class DVSNI(AnnotatedChallenge):
"""Client annotated "dvsni" ACME challenge.
:ivar .JWK account_key: Authorized Account Key
"""
__slots__ = ('challb', 'domain', 'account_key')
acme_type = challenges.DVSNI
def gen_cert_and_response(self, key=None, bits=2048, alg=jose.RS256):
"""Generate a DVSNI cert and response.
:param OpenSSL.crypto.PKey key: Private key used for
certificate generation. If none provided, a fresh key will
be generated.
:param int bits: Number of bits for fresh key generation.
:param .JWAAlgorithm alg:
:returns: ``(response, cert_pem, key_pem)`` tuple, where
``response`` is an instance of
`acme.challenges.DVSNIResponse`, ``cert`` is a certificate
(`OpenSSL.crypto.X509`) and ``key`` is a private key
(`OpenSSL.crypto.PKey`).
:rtype: tuple
"""
response = self.challb.chall.gen_response(self.account_key, alg=alg)
cert, key = response.gen_cert(key=key, bits=bits)
return response, cert, key
return self.challb.chall.response_and_validation(
self.account_key, *args, **kwargs)
class DNS(AnnotatedChallenge):

View File

@@ -344,8 +344,8 @@ def challb_to_achall(challb, account_key, domain):
chall = challb.chall
logger.info("%s challenge for %s", chall.typ, domain)
if isinstance(chall, challenges.DVSNI):
return achallenges.DVSNI(
if isinstance(chall, challenges.KeyAuthorizationChallenge):
return achallenges.KeyAuthorizationAnnotatedChallenge(
challb=challb, domain=domain, account_key=account_key)
elif isinstance(chall, challenges.DNS):
return achallenges.DNS(challb=challb, domain=domain)
@@ -355,9 +355,6 @@ def challb_to_achall(challb, account_key, domain):
elif isinstance(chall, challenges.ProofOfPossession):
return achallenges.ProofOfPossession(
challb=challb, domain=domain)
elif isinstance(chall, challenges.KeyAuthorizationChallenge):
return achallenges.KeyAuthorizationAnnotatedChallenge(
challb=challb, domain=domain, account_key=account_key)
else:
raise errors.Error(
"Received unsupported challenge of type: %s", chall.typ)

View File

@@ -17,7 +17,6 @@ import zope.component
import zope.interface.exceptions
import zope.interface.verify
from acme import client as acme_client
from acme import jose
import letsencrypt
@@ -28,6 +27,7 @@ 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
from letsencrypt import log
@@ -36,10 +36,8 @@ from letsencrypt import storage
from letsencrypt.display import util as display_util
from letsencrypt.display import ops as display_ops
from letsencrypt.errors import Error, PluginSelectionError, CertStorageError
from letsencrypt.plugins import disco as plugins_disco
logger = logging.getLogger(__name__)
@@ -106,8 +104,8 @@ def _find_domains(args, installer):
domains = args.domains
if not domains:
raise Error("Please specify --domains, or --installer that "
"will help in domain names autodiscovery")
raise errors.Error("Please specify --domains, or --installer that "
"will help in domain names autodiscovery")
return domains
@@ -159,9 +157,9 @@ def _determine_account(args, config):
try:
acc, acme = client.register(
config, account_storage, tos_cb=_tos_cb)
except Error as error:
except errors.Error as error:
logger.debug(error, exc_info=True)
raise Error(
raise errors.Error(
"Unable to register an account with ACME server")
args.account = acc.id
@@ -195,7 +193,7 @@ def _find_duplicative_certs(config, domains):
try:
full_path = os.path.join(configs_dir, renewal_file)
candidate_lineage = storage.RenewableCert(full_path, cli_config)
except (CertStorageError, IOError):
except (errors.CertStorageError, IOError):
logger.warning("Renewal configuration file %s is broken. "
"Skipping.", full_path)
continue
@@ -267,7 +265,7 @@ def _treat_as_renewal(config, domains):
br=os.linesep
),
reporter_util.HIGH_PRIORITY)
raise Error(
raise errors.Error(
"User did not use proper CLI and would like "
"to reinvoke the client.")
@@ -304,7 +302,7 @@ def _report_new_cert(cert_path, fullchain_path):
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
def _auth_from_domains(le_client, config, domains, plugins):
def _auth_from_domains(le_client, config, domains):
"""Authenticate and enroll certificate."""
# Note: This can raise errors... caught above us though.
lineage = _treat_as_renewal(config, domains)
@@ -325,9 +323,9 @@ def _auth_from_domains(le_client, config, domains, plugins):
# configuration values from this attempt? <- Absolutely (jdkasten)
else:
# TREAT AS NEW REQUEST
lineage = le_client.obtain_and_enroll_certificate(domains, plugins)
lineage = le_client.obtain_and_enroll_certificate(domains)
if not lineage:
raise Error("Certificate could not be obtained")
raise errors.Error("Certificate could not be obtained")
_report_new_cert(lineage.cert, lineage.fullchain)
@@ -346,7 +344,7 @@ def set_configurator(previously, now):
if previously:
if previously != now:
msg = "Too many flags setting configurators/installers/authenticators {0} -> {1}"
raise PluginSelectionError(msg.format(repr(previously), repr(now)))
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
return now
@@ -379,7 +377,7 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
'"letsencrypt-auto certonly" to get a cert you can install manually')
else:
msg = "{0} could not be determined or is not installed".format(cfg_type)
raise PluginSelectionError(msg)
raise errors.PluginSelectionError(msg)
def choose_configurator_plugins(args, config, plugins, verb):
@@ -425,21 +423,30 @@ def choose_configurator_plugins(args, config, plugins, verb):
authenticator = display_ops.pick_authenticator(config, req_auth, plugins)
logger.debug("Selected authenticator %s and installer %s", authenticator, installer)
# Report on any failures
if need_inst and not installer:
diagnose_configurator_problem("installer", req_inst, plugins)
if need_auth and not authenticator:
diagnose_configurator_problem("authenticator", req_auth, plugins)
record_chosen_plugins(config, plugins, authenticator, installer)
return installer, authenticator
def record_chosen_plugins(config, plugins, auth, inst):
"Update the config entries to reflect the plugins we actually selected."
cn = config.namespace
cn.authenticator = plugins.find_init(auth).name if auth else "none"
cn.installer = plugins.find_init(inst).name if inst else "none"
# TODO: Make run as close to auth + install as possible
# Possible difficulties: args.csr was hacked into auth
def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-locals
"""Obtain a certificate and install."""
try:
installer, authenticator = choose_configurator_plugins(args, config, plugins, "run")
except PluginSelectionError, e:
except errors.PluginSelectionError, e:
return e.message
domains = _find_domains(args, installer)
@@ -447,7 +454,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(args, config, authenticator, installer)
lineage = _auth_from_domains(le_client, config, domains, plugins)
lineage = _auth_from_domains(le_client, config, domains)
le_client.deploy_certificate(
domains, lineage.privkey, lineage.cert,
@@ -461,7 +468,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
display_ops.success_renewal(domains)
def obtaincert(args, config, plugins):
def obtain_cert(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""
if args.domains is not None and args.csr is not None:
@@ -472,7 +479,7 @@ def obtaincert(args, config, plugins):
try:
# installers are used in auth mode to determine domain names
installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly")
except PluginSelectionError, e:
except errors.PluginSelectionError, e:
return e.message
# TODO: Handle errors from _init_le_client?
@@ -487,7 +494,7 @@ def obtaincert(args, config, plugins):
_report_new_cert(cert_path, cert_fullchain)
else:
domains = _find_domains(args, installer)
_auth_from_domains(le_client, config, domains, plugins)
_auth_from_domains(le_client, config, domains)
def install(args, config, plugins):
@@ -497,7 +504,7 @@ def install(args, config, plugins):
try:
installer, _ = choose_configurator_plugins(args, config,
plugins, "install")
except PluginSelectionError, e:
except errors.PluginSelectionError, e:
return e.message
domains = _find_domains(args, installer)
@@ -512,18 +519,19 @@ def install(args, config, plugins):
def revoke(args, config, unused_plugins): # TODO: coop with renewal config
"""Revoke a previously obtained certificate."""
# For user-agent construction
config.namespace.installer = config.namespace.authenticator = "none"
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]))
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]))
key = acc.key
acme = client.acme_from_config_key(config, key)
cert = crypto_util.pyopenssl_load_certificate(args.cert_path[1])[0]
acme.revoke(jose.ComparableX509(cert))
def rollback(args, config, plugins):
@@ -549,6 +557,7 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
logger.debug("Filtered plugins: %r", filtered)
if not args.init and not args.prepare:
print str(filtered)
return
filtered.init(config)
@@ -556,26 +565,29 @@ def plugins_cmd(args, config, plugins): # TODO: Use IDisplay rather than print
logger.debug("Verified plugins: %r", verified)
if not args.prepare:
print str(verified)
return
verified.prepare()
available = verified.available()
logger.debug("Prepared plugins: %s", available)
print str(available)
def read_file(filename, mode="rb"):
"""Returns the given file's contents.
:param str filename: Filename
:param str filename: path to file
:param str mode: open mode (see `open`)
:returns: A tuple of filename and its contents
:returns: absolute path of filename and its contents
:rtype: tuple
:raises argparse.ArgumentTypeError: File does not exist or is not readable.
"""
try:
filename = os.path.abspath(filename)
return filename, open(filename, mode).read()
except IOError as exc:
raise argparse.ArgumentTypeError(exc.strerror)
@@ -621,7 +633,7 @@ class HelpfulArgumentParser(object):
"""
# Maps verbs/subcommands to the functions that implement them
VERBS = {"auth": obtaincert, "certonly": obtaincert,
VERBS = {"auth": obtain_cert, "certonly": obtain_cert,
"config_changes": config_changes, "everything": run,
"install": install, "plugins": plugins_cmd,
"revoke": revoke, "rollback": rollback, "run": run}
@@ -668,8 +680,32 @@ class HelpfulArgumentParser(object):
parsed_args = self.parser.parse_args(self.args)
parsed_args.func = self.VERBS[self.verb]
parsed_args.domains = self._parse_domains(parsed_args.domains)
return parsed_args
def _parse_domains(self, domains):
"""Helper function for parse_args() that parses domains from a
(possibly) comma separated list and returns list of unique domains.
:param domains: List of domain flags
:type domains: `list` of `string`
:returns: List of unique domains
:rtype: `list` of `string`
"""
uniqd = None
if domains:
dlist = []
for domain in domains:
dlist.extend([d.strip() for d in domain.split(",")])
# Make sure we don't have duplicates
uniqd = [d for i, d in enumerate(dlist) if d not in dlist[:i]]
return uniqd
def determine_verb(self):
"""Determines the verb/subcommand provided by the user.
@@ -811,7 +847,11 @@ def prepare_and_parse_args(plugins, args):
# --domains is useful, because it can be stored in config
#for subparser in parser_run, parser_auth, parser_install:
# subparser.add_argument("domains", nargs="*", metavar="domain")
helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append")
helpful.add(None, "-d", "--domains", dest="domains",
metavar="DOMAIN", action="append",
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains"
"as a parameter.")
helpful.add(
None, "--duplicate", dest="duplicate", action="store_true",
help="Allow getting a certificate that duplicates an existing one")
@@ -850,8 +890,9 @@ def prepare_and_parse_args(plugins, args):
help=config_help("no_verify_ssl"),
default=flag_default("no_verify_ssl"))
helpful.add(
"testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"),
help=config_help("dvsni_port"))
"testing", "--tls-sni-01-port", type=int,
default=flag_default("tls_sni_01_port"),
help=config_help("tls_sni_01_port"))
helpful.add("testing", "--http-01-port", dest="http01_port", type=int,
help=config_help("http01_port"))
@@ -889,7 +930,6 @@ def prepare_and_parse_args(plugins, args):
# parser (--help should display plugin-specific options last)
_plugins_parsing(helpful, plugins)
return helpful.parse_args()
@@ -899,7 +939,13 @@ def _create_subparsers(helpful):
helpful.add_group("revoke", description="Options for revocation of certs")
helpful.add_group("rollback", description="Options for reverting config changes")
helpful.add_group("plugins", description="Plugin options")
helpful.add(
None, "--user-agent", default=None,
help="Set a custom user agent string for the client. User agent strings allow "
"the CA to collect high level statistics about success rates by OS and "
"plugin. If you wish to hide your server OS version from the Let's "
'Encrypt server, set this to "".'
)
helpful.add("certonly",
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER"
@@ -934,26 +980,29 @@ def _paths_parser(helpful):
if verb in ("install", "revoke", "certonly"):
section = verb
if verb == "certonly":
add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph)
add(section, "--cert-path", type=os.path.abspath,
default=flag_default("auth_cert_path"), help=cph)
elif verb == "revoke":
add(section, "--cert-path", type=read_file, required=True, help=cph)
else:
add(section, "--cert-path", help=cph, required=(verb == "install"))
add(section, "--cert-path", type=os.path.abspath,
help=cph, required=(verb == "install"))
section = "paths"
if verb in ("install", "revoke"):
section = verb
# revoke --key-path reads a file, install --key-path takes a string
add(section, "--key-path", type=((verb == "revoke" and read_file) or str),
required=(verb == "install"),
help="Path to private key for cert creation or revocation (if account key is missing)")
add(section, "--key-path", required=(verb == "install"),
type=((verb == "revoke" and read_file) or os.path.abspath),
help="Path to private key for cert installation "
"or revocation (if account key is missing)")
default_cp = None
if verb == "certonly":
default_cp = flag_default("auth_chain_path")
add("paths", "--fullchain-path", default=default_cp,
add("paths", "--fullchain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a full certificate chain (cert plus chain).")
add("paths", "--chain-path", default=default_cp,
add("paths", "--chain-path", default=default_cp, type=os.path.abspath,
help="Accompanying path to a certificate chain.")
add("paths", "--config-dir", default=flag_default("config_dir"),
help=config_help("config_dir"))
@@ -1069,7 +1118,7 @@ def _handle_exception(exc_type, exc_value, trace, args):
sys.exit("".join(
traceback.format_exception(exc_type, exc_value, trace)))
if issubclass(exc_type, Error):
if issubclass(exc_type, errors.Error):
sys.exit(exc_value)
else:
# Tell the user a bit about what happened, without overwhelming
@@ -1133,7 +1182,7 @@ def main(cli_args=sys.argv[1:]):
disclaimer = pkg_resources.resource_string("letsencrypt", "DISCLAIMER")
if not zope.component.getUtility(interfaces.IDisplay).yesno(
disclaimer, "Agree", "Cancel"):
raise Error("Must agree to TOS")
raise errors.Error("Must agree to TOS")
if not os.geteuid() == 0:
logger.warning(
@@ -1148,7 +1197,6 @@ def main(cli_args=sys.argv[1:]):
return args.func(args, config, plugins)
if __name__ == "__main__":
err_string = main()
if err_string:

View File

@@ -5,11 +5,14 @@ import os
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
import OpenSSL
import zope.component
from acme import client as acme_client
from acme import jose
from acme import messages
import letsencrypt
from letsencrypt import account
from letsencrypt import auth_handler
from letsencrypt import configuration
@@ -18,6 +21,7 @@ from letsencrypt import continuity_auth
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import error_handler
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt import reverter
from letsencrypt import storage
@@ -29,10 +33,30 @@ from letsencrypt.display import enhancements
logger = logging.getLogger(__name__)
def _acme_from_config_key(config, key):
def acme_from_config_key(config, key):
"Wrangle ACME client construction"
# TODO: Allow for other alg types besides RS256
return acme_client.Client(directory=config.server, key=key,
verify_ssl=(not config.no_verify_ssl))
net = acme_client.ClientNetwork(key, verify_ssl=(not config.no_verify_ssl),
user_agent=_determine_user_agent(config))
return acme_client.Client(config.server, key=key, net=net)
def _determine_user_agent(config):
"""
Set a user_agent string in the config based on the choice of plugins.
(this wasn't knowable at construction time)
:returns: the client's User-Agent string
:rtype: `str`
"""
if config.user_agent is None:
ua = "LetsEncryptPythonClient/{0} ({1}) Authenticator/{2} Installer/{3}"
ua = ua.format(letsencrypt.__version__, " ".join(le_util.get_os_info()),
config.authenticator, config.installer)
else:
ua = config.user_agent
return ua
def register(config, account_storage, tos_cb=None):
@@ -84,7 +108,7 @@ def register(config, account_storage, tos_cb=None):
public_exponent=65537,
key_size=config.rsa_key_size,
backend=default_backend())))
acme = _acme_from_config_key(config, key)
acme = acme_from_config_key(config, key)
# TODO: add phone?
regr = acme.register(messages.NewRegistration.from_data(email=config.email))
@@ -98,6 +122,7 @@ def register(config, account_storage, tos_cb=None):
acc = account.Account(regr, key)
account.report_new_account(acc, config)
account_storage.save(acc)
return acc, acme
@@ -126,7 +151,7 @@ class Client(object):
# Initialize ACME if account is provided
if acme is None and self.account is not None:
acme = _acme_from_config_key(config, self.account.key)
acme = acme_from_config_key(config, self.account.key)
self.acme = acme
# TODO: Check if self.config.enroll_autorenew is None. If
@@ -211,7 +236,7 @@ class Client(object):
return self._obtain_certificate(domains, csr) + (key, csr)
def obtain_and_enroll_certificate(self, domains, plugins):
def obtain_and_enroll_certificate(self, domains):
"""Obtain and enroll certificate.
Get a new certificate for the specified domains using the specified
@@ -228,13 +253,6 @@ class Client(object):
"""
certr, chain, key, _ = self.obtain_certificate(domains)
# TODO: remove this dirty hack
self.config.namespace.authenticator = plugins.find_init(
self.dv_auth).name
if self.installer is not None:
self.config.namespace.installer = plugins.find_init(
self.installer).name
# XXX: We clearly need a more general and correct way of getting
# options into the configobj for the RenewableCert instance.
# This is a quick-and-dirty way to do it to allow integration
@@ -330,26 +348,12 @@ class Client(object):
self.installer.save("Deployed Let's Encrypt Certificate")
# sites may have been enabled / final cleanup
with error_handler.ErrorHandler(self._rollback_and_restart):
msg = ("We were unable to install your certificate, "
"however, we successfully restored your "
"server to its prior configuration.")
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
# sites may have been enabled / final cleanup
self.installer.restart()
def _rollback_and_restart(self):
"""Rollback the most recent checkpoint and restart the webserver"""
logger.critical("Rolling back to previous server configuration...")
try:
self.installer.rollback_checkpoints()
self.installer.restart()
except:
# TODO: suggest letshelp-letsencypt here
logger.critical("Failure to rollback config "
"changes and restart your server")
logger.critical("Please submit a bug report to "
"https://github.com/letsencrypt/letsencrypt")
raise
logger.critical("Rollback successful; your server has "
"been restarted with your old configuration")
def enhance_config(self, domains, config=None):
"""Enhance the configuration.
@@ -387,7 +391,8 @@ class Client(object):
"Upgrade-Insecure-Requests")
if redirect or hsts or uir:
self.installer.restart()
with error_handler.ErrorHandler(self._rollback_and_restart, msg):
self.installer.restart()
def apply_enhancement(self, domains, enhancement, options=None):
"""Applies an enhacement on all domains.
@@ -403,7 +408,9 @@ class Client(object):
:type str
"""
with error_handler.ErrorHandler(self.installer.recovery_routine):
msg = ("We were unable to set up a redirect for your server, "
"however, we successfully installed your certificate.")
with error_handler.ErrorHandler(self._recovery_routine_with_msg, msg):
for dom in domains:
try:
self.installer.enhance(dom, enhancement, options)
@@ -414,6 +421,37 @@ class Client(object):
self.installer.save("Add enhancement %s" % (enhancement))
def _recovery_routine_with_msg(self, success_msg):
"""Calls the installer's recovery routine and prints success_msg
:param str success_msg: message to show on successful recovery
"""
self.installer.recovery_routine()
reporter = zope.component.getUtility(interfaces.IReporter)
reporter.add_message(success_msg, reporter.HIGH_PRIORITY)
def _rollback_and_restart(self, success_msg):
"""Rollback the most recent checkpoint and restart the webserver
:param str success_msg: message to show on successful rollback
"""
logger.critical("Rolling back to previous server configuration...")
reporter = zope.component.getUtility(interfaces.IReporter)
try:
self.installer.rollback_checkpoints()
self.installer.restart()
except:
# TODO: suggest letshelp-letsencypt here
reporter.add_message(
"An error occured and we failed to restore your config and "
"restart your server. Please submit a bug report to "
"https://github.com/letsencrypt/letsencrypt",
reporter.HIGH_PRIORITY)
raise
reporter.add_message(success_msg, reporter.HIGH_PRIORITY)
def validate_key_csr(privkey, csr=None):
"""Validate Key and CSR files.

View File

@@ -1,6 +1,7 @@
"""Let's Encrypt user-supplied configuration."""
import os
import urlparse
import re
import zope.interface
@@ -37,10 +38,12 @@ class NamespaceConfig(object):
def __init__(self, namespace):
self.namespace = namespace
if self.http01_port == self.dvsni_port:
raise errors.Error(
"Trying to run http-01 and DVSNI "
"on the same port ({0})".format(self.dvsni_port))
self.namespace.config_dir = os.path.abspath(self.namespace.config_dir)
self.namespace.work_dir = os.path.abspath(self.namespace.work_dir)
self.namespace.logs_dir = os.path.abspath(self.namespace.logs_dir)
# Check command line parameters sanity, and error out in case of problem.
check_config_sanity(self)
def __getattr__(self, name):
return getattr(self.namespace, name)
@@ -111,3 +114,49 @@ class RenewerConfiguration(object):
def renewer_config_file(self): # pylint: disable=missing-docstring
return os.path.join(
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
def check_config_sanity(config):
"""Validate command line options and display error message if
requirements are not met.
:param config: IConfig instance holding user configuration
:type args: :class:`letsencrypt.interfaces.IConfig`
"""
# Port check
if config.http01_port == config.tls_sni_01_port:
raise errors.ConfigurationError(
"Trying to run http-01 and tls-sni-01 "
"on the same port ({0})".format(config.tls_sni_01_port))
# Domain checks
if config.namespace.domains is not None:
_check_config_domain_sanity(config.namespace.domains)
def _check_config_domain_sanity(domains):
"""Helper method for check_config_sanity which validates
domain flag values and errors out if the requirements are not met.
:param domains: List of domains
:type domains: `list` of `string`
:raises ConfigurationError: for invalid domains and cases where Let's
Encrypt currently will not issue certificates
"""
# Check if there's a wildcard domain
if any(d.startswith("*.") for d in domains):
raise errors.ConfigurationError(
"Wildcard domains are not supported")
# Punycode
if any("xn--" in d for d in domains):
raise errors.ConfigurationError(
"Punycode domains are not supported")
# FQDN checks from
# http://www.mkyong.com/regular-expressions/domain-name-regular-expression-example/
# Characters used, domain parts < 63 chars, tld > 1 < 7 chars
# first and last char is not "-"
fqdn = re.compile("^((?!-)[A-Za-z0-9-]{1,63}(?<!-)\\.)+[A-Za-z]{2,6}$")
if any(True for d in domains if not fqdn.match(d)):
raise errors.ConfigurationError("Requested domain is not a FQDN")

View File

@@ -23,7 +23,7 @@ CLI_DEFAULTS = dict(
work_dir="/var/lib/letsencrypt",
logs_dir="/var/log/letsencrypt",
no_verify_ssl=False,
dvsni_port=challenges.DVSNI.PORT,
tls_sni_01_port=challenges.TLSSNI01Response.PORT,
auth_cert_path="./cert.pem",
auth_chain_path="./chain.pem",
@@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict(
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
challenges.DVSNI, challenges.HTTP01])])
challenges.TLSSNI01, challenges.HTTP01])])
"""Mutually exclusive challenges."""

View File

@@ -1,4 +1,5 @@
"""Registers functions to be called if an exception or signal occurs."""
import functools
import logging
import os
import signal
@@ -40,11 +41,11 @@ class ErrorHandler(object):
to be called again by the next signal handler.
"""
def __init__(self, func=None):
def __init__(self, func=None, *args, **kwargs):
self.funcs = []
self.prev_handlers = {}
if func is not None:
self.register(func)
self.register(func, *args, **kwargs)
def __enter__(self):
self.set_signal_handlers()
@@ -57,9 +58,13 @@ class ErrorHandler(object):
self.call_registered()
self.reset_signal_handlers()
def register(self, func):
"""Registers func to be called if an error occurs."""
self.funcs.append(func)
def register(self, func, *args, **kwargs):
"""Sets func to be called with *args and **kwargs during cleanup
:param function func: function to be called in case of an error
"""
self.funcs.append(functools.partial(func, *args, **kwargs))
def call_registered(self):
"""Calls all registered functions"""

View File

@@ -57,8 +57,8 @@ class DvAuthError(AuthorizationError):
# Authenticator - Challenge specific errors
class DvsniError(DvAuthError):
"""Let's Encrypt DVSNI error."""
class TLSSNI01Error(DvAuthError):
"""Let's Encrypt TLSSNI01 error."""
# Plugin Errors
@@ -94,3 +94,7 @@ class StandaloneBindError(Error):
"Problem binding to port {0}: {1}".format(port, socket_error))
self.socket_error = socket_error
self.port = port
class ConfigurationError(Error):
"""Configuration sanity error."""

View File

@@ -219,8 +219,8 @@ class IConfig(zope.interface.Interface):
no_verify_ssl = zope.interface.Attribute(
"Disable SSL certificate verification.")
dvsni_port = zope.interface.Attribute(
"Port number to perform DVSNI challenge. "
tls_sni_01_port = zope.interface.Attribute(
"Port number to perform tls-sni-01 challenge. "
"Boulder in testing mode defaults to 5001.")
http01_port = zope.interface.Attribute(
@@ -298,7 +298,8 @@ class IInstaller(IPlugin):
Both title and temporary are needed because a save may be
intended to be permanent, but the save is not ready to be a full
checkpoint
checkpoint. If an exception is raised, it is assumed a new
checkpoint was not created.
:param str title: The title of the save. If a title is given, the
configuration will be saved as a new checkpoint and put in a

View File

@@ -3,6 +3,7 @@ import collections
import errno
import logging
import os
import platform
import re
import subprocess
import stat
@@ -202,6 +203,45 @@ def safely_remove(path):
raise
def get_os_info():
"""
Get Operating System type/distribution and major version
:returns: (os_name, os_version)
:rtype: `tuple` of `str`
"""
info = platform.system_alias(
platform.system(),
platform.release(),
platform.version()
)
os_type, os_ver, _ = info
os_type = os_type.lower()
if os_type.startswith('linux'):
info = platform.linux_distribution()
# On arch, platform.linux_distribution() is reportedly ('','',''),
# so handle it defensively
if info[0]:
os_type = info[0]
if info[1]:
os_ver = info[1]
elif os_type.startswith('darwin'):
os_ver = subprocess.Popen(
["sw_vers", "-productVersion"],
stdout=subprocess.PIPE
).communicate()[0]
os_ver = os_ver.partition(".")[0]
elif os_type.startswith('freebsd'):
# eg "9.3-RC3-p1"
os_ver = os_ver.partition("-")[0]
os_ver = os_ver.partition(".")[0]
elif platform.win32_ver()[1]:
os_ver = platform.win32_ver()[1]
else:
# Cases known to fall here: Cygwin python
os_ver = ''
return os_type, os_ver
# Just make sure we don't get pwned... Make sure that it also doesn't
# start with a period or have two consecutive periods <- this needs to
# be done in addition to the regex

View File

@@ -135,22 +135,22 @@ class Addr(object):
return self.__class__((self.tup[0], port))
class Dvsni(object):
"""Class that perform DVSNI challenges."""
class TLSSNI01(object):
"""Class that performs tls-sni-01 challenges."""
def __init__(self, configurator):
self.configurator = configurator
self.achalls = []
self.indices = []
self.challenge_conf = os.path.join(
configurator.config.config_dir, "le_dvsni_cert_challenge.conf")
configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf")
# self.completed = 0
def add_chall(self, achall, idx=None):
"""Add challenge to DVSNI object to perform at once.
"""Add challenge to TLSSNI01 object to perform at once.
:param achall: Annotated DVSNI challenge.
:type achall: :class:`letsencrypt.achallenges.DVSNI`
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
TLSSNI01 challenge.
:param int idx: index to challenge in a larger array
@@ -162,8 +162,8 @@ class Dvsni(object):
def get_cert_path(self, achall):
"""Returns standardized name for challenge certificate.
:param achall: Annotated DVSNI challenge.
:type achall: :class:`letsencrypt.achallenges.DVSNI`
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
tls-sni-01 challenge.
:returns: certificate file name
:rtype: str
@@ -177,7 +177,7 @@ class Dvsni(object):
return os.path.join(self.configurator.config.work_dir,
achall.chall.encode("token") + '.pem')
def _setup_challenge_cert(self, achall, s=None):
def _setup_challenge_cert(self, achall, cert_key=None):
"""Generate and write out challenge certificate."""
cert_path = self.get_cert_path(achall)
@@ -186,7 +186,8 @@ class Dvsni(object):
self.configurator.reverter.register_file_creation(True, key_path)
self.configurator.reverter.register_file_creation(True, cert_path)
response, cert, key = achall.gen_cert_and_response(s)
response, (cert, key) = achall.response_and_validation(
cert_key=cert_key)
cert_pem = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_PEM, cert)
key_pem = OpenSSL.crypto.dump_privatekey(

View File

@@ -115,24 +115,24 @@ class AddrTest(unittest.TestCase):
self.assertEqual(set_a, set_b)
class DvsniTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.common.DvsniTest."""
class TLSSNI01Test(unittest.TestCase):
"""Tests for letsencrypt.plugins.common.TLSSNI01."""
auth_key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
achalls = [
achallenges.DVSNI(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(token=b'dvsni1'), "pending"),
challenges.TLSSNI01(token=b'token1'), "pending"),
domain="encryption-example.demo", account_key=auth_key),
achallenges.DVSNI(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.chall_to_challb(
challenges.DVSNI(token=b'dvsni2'), "pending"),
challenges.TLSSNI01(token=b'token2'), "pending"),
domain="letsencrypt.demo", account_key=auth_key),
]
def setUp(self):
from letsencrypt.plugins.common import Dvsni
self.sni = Dvsni(configurator=mock.MagicMock())
from letsencrypt.plugins.common import TLSSNI01
self.sni = TLSSNI01(configurator=mock.MagicMock())
def test_add_chall(self):
self.sni.add_chall(self.achalls[0], 0)
@@ -146,11 +146,11 @@ class DvsniTest(unittest.TestCase):
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
mock_open, mock_safe_open = mock.mock_open(), mock.mock_open()
response = challenges.DVSNIResponse(validation=mock.Mock())
response = challenges.TLSSNI01Response()
achall = mock.MagicMock()
key = test_util.load_pyopenssl_private_key("rsa512_key.pem")
achall.gen_cert_and_response.return_value = (
response, test_util.load_cert("cert.pem"), key)
achall.response_and_validation.return_value = (
response, (test_util.load_cert("cert.pem"), key))
with mock.patch("letsencrypt.plugins.common.open",
mock_open, create=True):

View File

@@ -51,8 +51,8 @@ class PluginEntryPointTest(unittest.TestCase):
def test_description(self):
self.assertEqual(
"Automatically use a temporary webserver",
self.plugin_ep.description)
"Automatically use a temporary webserver",
self.plugin_ep.description)
def test_description_with_name(self):
self.plugin_ep.plugin_cls = mock.MagicMock(description="Desc")

View File

@@ -31,7 +31,7 @@ class Authenticator(common.Plugin):
run as a privileged process. Alternatively shows instructions on how
to use Python's built-in HTTP server.
.. todo:: Support for `~.challenges.DVSNI`.
.. todo:: Support for `~.challenges.TLSSNI01`.
"""
zope.interface.implements(interfaces.IAuthenticator)

View File

@@ -11,7 +11,6 @@ import six
import zope.interface
from acme import challenges
from acme import crypto_util as acme_crypto_util
from acme import standalone as acme_standalone
from letsencrypt import errors
@@ -28,9 +27,9 @@ class ServerManager(object):
Manager for `ACMEServer` and `ACMETLSServer` instances.
`certs` and `simple_http_resources` correspond to
`certs` and `http_01_resources` correspond to
`acme.crypto_util.SSLSocket.certs` and
`acme.crypto_util.SSLSocket.simple_http_resources` respectively. All
`acme.crypto_util.SSLSocket.http_01_resources` respectively. All
created servers share the same certificates and resources, so if
you're running both TLS and non-TLS instances, HTTP01 handlers
will serve the same URLs!
@@ -38,10 +37,10 @@ class ServerManager(object):
"""
_Instance = collections.namedtuple("_Instance", "server thread")
def __init__(self, certs, simple_http_resources):
def __init__(self, certs, http_01_resources):
self._instances = {}
self.certs = certs
self.simple_http_resources = simple_http_resources
self.http_01_resources = http_01_resources
def run(self, port, challenge_type):
"""Run ACME server on specified ``port``.
@@ -51,23 +50,23 @@ class ServerManager(object):
:param int port: Port to run the server on.
:param challenge_type: Subclass of `acme.challenges.Challenge`,
either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`.
either `acme.challenge.HTTP01` or `acme.challenges.TLSSNI01`.
:returns: Server instance.
:rtype: ACMEServerMixin
"""
assert challenge_type in (challenges.DVSNI, challenges.HTTP01)
assert challenge_type in (challenges.TLSSNI01, challenges.HTTP01)
if port in self._instances:
return self._instances[port].server
address = ("", port)
try:
if challenge_type is challenges.DVSNI:
server = acme_standalone.DVSNIServer(address, self.certs)
if challenge_type is challenges.TLSSNI01:
server = acme_standalone.TLSSNI01Server(address, self.certs)
else: # challenges.HTTP01
server = acme_standalone.HTTP01Server(
address, self.simple_http_resources)
address, self.http_01_resources)
except socket.error as error:
raise errors.StandaloneBindError(error, port)
@@ -109,7 +108,7 @@ class ServerManager(object):
in six.iteritems(self._instances))
SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01])
SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01])
def supported_challenges_validator(data):
@@ -138,7 +137,7 @@ class Authenticator(common.Plugin):
"""Standalone Authenticator.
This authenticator creates its own ephemeral TCP listener on the
necessary port in order to respond to incoming DVSNI and HTTP01
necessary port in order to respond to incoming tls-sni-01 and http-01
challenges from the certificate authority. Therefore, it does not
rely on any existing server program.
"""
@@ -150,12 +149,9 @@ class Authenticator(common.Plugin):
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
# one self-signed key for all DVSNI and HTTP01 certificates
# one self-signed key for all tls-sni-01 certificates
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048)
# TODO: generate only when the first HTTP01 challenge is solved
self.simple_http_cert = acme_crypto_util.gen_ss_cert(
self.key, domains=["temp server"])
self.served = collections.defaultdict(set)
@@ -164,9 +160,9 @@ class Authenticator(common.Plugin):
# GIL, the operations are safe, c.f.
# https://docs.python.org/2/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
self.certs = {}
self.simple_http_resources = set()
self.http_01_resources = set()
self.servers = ServerManager(self.certs, self.simple_http_resources)
self.servers = ServerManager(self.certs, self.http_01_resources)
@classmethod
def add_parser_arguments(cls, add):
@@ -186,15 +182,16 @@ class Authenticator(common.Plugin):
necessary_ports = set()
if challenges.HTTP01 in self.supported_challenges:
necessary_ports.add(self.config.http01_port)
if challenges.DVSNI in self.supported_challenges:
necessary_ports.add(self.config.dvsni_port)
if challenges.TLSSNI01 in self.supported_challenges:
necessary_ports.add(self.config.tls_sni_01_port)
return necessary_ports
def more_info(self): # pylint: disable=missing-docstring
return("This authenticator creates its own ephemeral TCP listener "
"on the necessary port in order to respond to incoming DVSNI "
"and HTTP01 challenges from the certificate authority. "
"Therefore, it does not rely on any existing server program.")
"on the necessary port in order to respond to incoming "
"tls-sni-01 and http-01 challenges from the certificate "
"authority. Therefore, it does not rely on any existing "
"server program.")
def prepare(self): # pylint: disable=missing-docstring
pass
@@ -240,17 +237,16 @@ class Authenticator(common.Plugin):
server = self.servers.run(
self.config.http01_port, challenges.HTTP01)
response, validation = achall.response_and_validation()
self.simple_http_resources.add(
self.http_01_resources.add(
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
chall=achall.chall, response=response,
validation=validation))
cert = self.simple_http_cert
domain = achall.domain
else: # DVSNI
server = self.servers.run(self.config.dvsni_port, challenges.DVSNI)
response, cert, _ = achall.gen_cert_and_response(self.key)
domain = response.z_domain
self.certs[domain] = (self.key, cert)
else: # tls-sni-01
server = self.servers.run(
self.config.tls_sni_01_port, challenges.TLSSNI01)
response, (cert, _) = achall.response_and_validation(
cert_key=self.key)
self.certs[response.z_domain] = (self.key, cert)
self.served[server].add(achall)
responses.append(response)

View File

@@ -24,13 +24,13 @@ class ServerManagerTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.standalone import ServerManager
self.certs = {}
self.simple_http_resources = {}
self.mgr = ServerManager(self.certs, self.simple_http_resources)
self.http_01_resources = {}
self.mgr = ServerManager(self.certs, self.http_01_resources)
def test_init(self):
self.assertTrue(self.mgr.certs is self.certs)
self.assertTrue(
self.mgr.simple_http_resources is self.simple_http_resources)
self.mgr.http_01_resources is self.http_01_resources)
def _test_run_stop(self, challenge_type):
server = self.mgr.run(port=0, challenge_type=challenge_type)
@@ -39,10 +39,10 @@ class ServerManagerTest(unittest.TestCase):
self.mgr.stop(port=port)
self.assertEqual(self.mgr.running(), {})
def test_run_stop_dvsni(self):
self._test_run_stop(challenges.DVSNI)
def test_run_stop_tls_sni_01(self):
self._test_run_stop(challenges.TLSSNI01)
def test_run_stop_simplehttp(self):
def test_run_stop_http_01(self):
self._test_run_stop(challenges.HTTP01)
def test_run_idempotent(self):
@@ -73,10 +73,10 @@ class SupportedChallengesValidatorTest(unittest.TestCase):
return supported_challenges_validator(data)
def test_correct(self):
self.assertEqual("dvsni", self._call("dvsni"))
self.assertEqual("tls-sni-01", self._call("tls-sni-01"))
self.assertEqual("http-01", self._call("http-01"))
self.assertEqual("dvsni,http-01", self._call("dvsni,http-01"))
self.assertEqual("http-01,dvsni", self._call("http-01,dvsni"))
self.assertEqual("tls-sni-01,http-01", self._call("tls-sni-01,http-01"))
self.assertEqual("http-01,tls-sni-01", self._call("http-01,tls-sni-01"))
def test_unrecognized(self):
assert "foo" not in challenges.Challenge.TYPES
@@ -92,24 +92,24 @@ class AuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.standalone import Authenticator
self.config = mock.MagicMock(
dvsni_port=1234, http01_port=4321,
standalone_supported_challenges="dvsni,http-01")
tls_sni_01_port=1234, http01_port=4321,
standalone_supported_challenges="tls-sni-01,http-01")
self.auth = Authenticator(self.config, name="standalone")
def test_supported_challenges(self):
self.assertEqual(self.auth.supported_challenges,
set([challenges.DVSNI, challenges.HTTP01]))
set([challenges.TLSSNI01, challenges.HTTP01]))
def test_more_info(self):
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
def test_get_chall_pref(self):
self.assertEqual(set(self.auth.get_chall_pref(domain=None)),
set([challenges.DVSNI, challenges.HTTP01]))
set([challenges.TLSSNI01, challenges.HTTP01]))
@mock.patch("letsencrypt.plugins.standalone.util")
def test_perform_alredy_listening(self, mock_util):
for chall, port in ((challenges.DVSNI.typ, 1234),
for chall, port in ((challenges.TLSSNI01.typ, 1234),
(challenges.HTTP01.typ, 4321)):
mock_util.already_listening.return_value = True
self.config.standalone_supported_challenges = chall
@@ -153,10 +153,10 @@ class AuthenticatorTest(unittest.TestCase):
def test_perform2(self):
domain = b'localhost'
key = jose.JWK.load(test_util.load_vector('rsa512_key.pem'))
simple_http = achallenges.KeyAuthorizationAnnotatedChallenge(
http_01 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain=domain, account_key=key)
dvsni = achallenges.DVSNI(
challb=acme_util.DVSNI_P, domain=domain, account_key=key)
tls_sni_01 = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.TLSSNI01_P, domain=domain, account_key=key)
self.auth.servers = mock.MagicMock()
@@ -164,24 +164,24 @@ class AuthenticatorTest(unittest.TestCase):
return "server{0}".format(port)
self.auth.servers.run.side_effect = _run
responses = self.auth.perform2([simple_http, dvsni])
responses = self.auth.perform2([http_01, tls_sni_01])
self.assertTrue(isinstance(responses, list))
self.assertEqual(2, len(responses))
self.assertTrue(isinstance(responses[0], challenges.HTTP01Response))
self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse))
self.assertTrue(isinstance(responses[1], challenges.TLSSNI01Response))
self.assertEqual(self.auth.servers.run.mock_calls, [
mock.call(4321, challenges.HTTP01),
mock.call(1234, challenges.DVSNI),
mock.call(1234, challenges.TLSSNI01),
])
self.assertEqual(self.auth.served, {
"server1234": set([dvsni]),
"server4321": set([simple_http]),
"server1234": set([tls_sni_01]),
"server4321": set([http_01]),
})
self.assertEqual(1, len(self.auth.simple_http_resources))
self.assertEqual(2, len(self.auth.certs))
self.assertEqual(list(self.auth.simple_http_resources), [
self.assertEqual(1, len(self.auth.http_01_resources))
self.assertEqual(1, len(self.auth.certs))
self.assertEqual(list(self.auth.http_01_resources), [
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
acme_util.HTTP01, responses[0], mock.ANY)])

View File

@@ -75,7 +75,7 @@ def renew(cert, old_version):
# XXX: this loses type data (for example, the fact that key_size
# was an int, not a str)
config.rsa_key_size = int(config.rsa_key_size)
config.dvsni_port = int(config.dvsni_port)
config.tls_sni_01_port = int(config.tls_sni_01_port)
config.namespace.http01_port = int(config.namespace.http01_port)
zope.component.provideUtility(config)
try:

View File

@@ -1,34 +0,0 @@
"""Tests for letsencrypt.achallenges."""
import unittest
import OpenSSL
from acme import challenges
from acme import jose
from letsencrypt.tests import acme_util
from letsencrypt.tests import test_util
class DVSNITest(unittest.TestCase):
"""Tests for letsencrypt.achallenges.DVSNI."""
def setUp(self):
self.challb = acme_util.chall_to_challb(acme_util.DVSNI, "pending")
key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
from letsencrypt.achallenges import DVSNI
self.achall = DVSNI(
challb=self.challb, domain="example.com", account_key=key)
def test_proxy(self):
self.assertEqual(self.challb.token, self.achall.token)
def test_gen_cert_and_response(self):
response, cert, key = self.achall.gen_cert_and_response()
self.assertTrue(isinstance(response, challenges.DVSNIResponse))
self.assertTrue(isinstance(cert, OpenSSL.crypto.X509))
self.assertTrue(isinstance(key, OpenSSL.crypto.PKey))
if __name__ == "__main__":
unittest.main() # pragma: no cover

View File

@@ -14,7 +14,7 @@ KEY = test_util.load_rsa_private_key('rsa512_key.pem')
# Challenges
HTTP01 = challenges.HTTP01(
token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
DVSNI = challenges.DVSNI(
TLSSNI01 = challenges.TLSSNI01(
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
DNS = challenges.DNS(token="17817c66b60ce2e4012dfad92657527a")
RECOVERY_CONTACT = challenges.RecoveryContact(
@@ -41,7 +41,7 @@ POP = challenges.ProofOfPossession(
)
)
CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP]
CHALLENGES = [HTTP01, TLSSNI01, DNS, RECOVERY_CONTACT, POP]
DV_CHALLENGES = [chall for chall in CHALLENGES
if isinstance(chall, challenges.DVChallenge)]
CONT_CHALLENGES = [chall for chall in CHALLENGES
@@ -79,13 +79,13 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
# Pending ChallengeBody objects
DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING)
TLSSNI01_P = chall_to_challb(TLSSNI01, messages.STATUS_PENDING)
HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)
DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING)
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING)
POP_P = chall_to_challb(POP, messages.STATUS_PENDING)
CHALLENGES_P = [HTTP01_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
CHALLENGES_P = [HTTP01_P, TLSSNI01_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
DV_CHALLENGES_P = [challb for challb in CHALLENGES_P
if isinstance(challb.chall, challenges.DVChallenge)]
CONT_CHALLENGES_P = [

View File

@@ -45,7 +45,7 @@ class ChallengeFactoryTest(unittest.TestCase):
self.assertEqual(
[achall.chall for achall in cont_c], [acme_util.RECOVERY_CONTACT])
self.assertEqual([achall.chall for achall in dv_c], [acme_util.DVSNI])
self.assertEqual([achall.chall for achall in dv_c], [acme_util.TLSSNI01])
def test_unrecognized(self):
self.handler.authzr["failure.com"] = acme_util.gen_authzr(
@@ -70,7 +70,7 @@ class GetAuthorizationsTest(unittest.TestCase):
self.mock_dv_auth = mock.MagicMock(name="ApacheConfigurator")
self.mock_cont_auth = mock.MagicMock(name="ContinuityAuthenticator")
self.mock_dv_auth.get_chall_pref.return_value = [challenges.DVSNI]
self.mock_dv_auth.get_chall_pref.return_value = [challenges.TLSSNI01]
self.mock_cont_auth.get_chall_pref.return_value = [
challenges.RecoveryContact]
@@ -90,7 +90,7 @@ class GetAuthorizationsTest(unittest.TestCase):
logging.disable(logging.NOTSET)
@mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges")
def test_name1_dvsni1(self, mock_poll):
def test_name1_tls_sni_01_1(self, mock_poll):
self.mock_net.request_domain_challenges.side_effect = functools.partial(
gen_dom_authzr, challs=acme_util.DV_CHALLENGES)
@@ -107,14 +107,14 @@ class GetAuthorizationsTest(unittest.TestCase):
self.assertEqual(self.mock_dv_auth.cleanup.call_count, 1)
self.assertEqual(self.mock_cont_auth.cleanup.call_count, 0)
# Test if list first element is DVSNI, use typ because it is an achall
# Test if list first element is TLSSNI01, use typ because it is an achall
self.assertEqual(
self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "dvsni")
self.mock_dv_auth.cleanup.call_args[0][0][0].typ, "tls-sni-01")
self.assertEqual(len(authzr), 1)
@mock.patch("letsencrypt.auth_handler.AuthHandler._poll_challenges")
def test_name3_dvsni3_rectok_3(self, mock_poll):
def test_name3_tls_sni_01_3_rectok_3(self, mock_poll):
self.mock_net.request_domain_challenges.side_effect = functools.partial(
gen_dom_authzr, challs=acme_util.CHALLENGES)
@@ -309,9 +309,9 @@ class GenChallengePathTest(unittest.TestCase):
return gen_challenge_path(challbs, preferences, combinations)
def test_common_case(self):
"""Given DVSNI and HTTP01 with appropriate combos."""
challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P)
prefs = [challenges.DVSNI]
"""Given TLSSNI01 and HTTP01 with appropriate combos."""
challbs = (acme_util.TLSSNI01_P, acme_util.HTTP01_P)
prefs = [challenges.TLSSNI01]
combos = ((0,), (1,))
# Smart then trivial dumb path test
@@ -324,9 +324,9 @@ class GenChallengePathTest(unittest.TestCase):
def test_common_case_with_continuity(self):
challbs = (acme_util.POP_P,
acme_util.RECOVERY_CONTACT_P,
acme_util.DVSNI_P,
acme_util.TLSSNI01_P,
acme_util.HTTP01_P)
prefs = [challenges.ProofOfPossession, challenges.DVSNI]
prefs = [challenges.ProofOfPossession, challenges.TLSSNI01]
combos = acme_util.gen_combos(challbs)
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
@@ -336,14 +336,14 @@ class GenChallengePathTest(unittest.TestCase):
def test_full_cont_server(self):
challbs = (acme_util.RECOVERY_CONTACT_P,
acme_util.POP_P,
acme_util.DVSNI_P,
acme_util.TLSSNI01_P,
acme_util.HTTP01_P,
acme_util.DNS_P)
# Typical webserver client that can do everything except DNS
# Attempted to make the order realistic
prefs = [challenges.ProofOfPossession,
challenges.HTTP01,
challenges.DVSNI,
challenges.TLSSNI01,
challenges.RecoveryContact]
combos = acme_util.gen_combos(challbs)
self.assertEqual(self._call(challbs, prefs, combos), (1, 3))
@@ -352,8 +352,8 @@ class GenChallengePathTest(unittest.TestCase):
self.assertTrue(self._call(challbs, prefs, None))
def test_not_supported(self):
challbs = (acme_util.POP_P, acme_util.DVSNI_P)
prefs = [challenges.DVSNI]
challbs = (acme_util.POP_P, acme_util.TLSSNI01_P)
prefs = [challenges.TLSSNI01]
combos = ((0, 1),)
self.assertRaises(
@@ -411,7 +411,7 @@ class IsPreferredTest(unittest.TestCase):
def _call(cls, chall, satisfied):
from letsencrypt.auth_handler import is_preferred
return is_preferred(chall, satisfied, exclusive_groups=frozenset([
frozenset([challenges.DVSNI, challenges.HTTP01]),
frozenset([challenges.TLSSNI01, challenges.HTTP01]),
frozenset([challenges.DNS, challenges.HTTP01]),
]))
@@ -421,11 +421,11 @@ class IsPreferredTest(unittest.TestCase):
def test_mutually_exclusvie(self):
self.assertFalse(
self._call(
acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P])))
acme_util.TLSSNI01_P, frozenset([acme_util.HTTP01_P])))
def test_mutually_exclusive_same_type(self):
self.assertTrue(
self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P])))
self._call(acme_util.TLSSNI01_P, frozenset([acme_util.TLSSNI01_P])))
class ReportFailedChallsTest(unittest.TestCase):
@@ -446,15 +446,15 @@ class ReportFailedChallsTest(unittest.TestCase):
domain="example.com",
account_key="key")
kwargs["chall"] = acme_util.DVSNI
self.dvsni_same = achallenges.DVSNI(
kwargs["chall"] = acme_util.TLSSNI01
self.tls_sni_same = achallenges.KeyAuthorizationAnnotatedChallenge(
# 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(
self.tls_sni_diff = achallenges.KeyAuthorizationAnnotatedChallenge(
# pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="foo.bar",
@@ -464,7 +464,7 @@ class ReportFailedChallsTest(unittest.TestCase):
def test_same_error_and_domain(self, mock_zope):
from letsencrypt import auth_handler
auth_handler._report_failed_challs([self.http01, self.dvsni_same])
auth_handler._report_failed_challs([self.http01, self.tls_sni_same])
call_list = mock_zope().add_message.call_args_list
self.assertTrue(len(call_list) == 1)
self.assertTrue("Domains: example.com\n" in call_list[0][0][0])
@@ -473,7 +473,7 @@ class ReportFailedChallsTest(unittest.TestCase):
def test_different_errors_and_domains(self, mock_zope):
from letsencrypt import auth_handler
auth_handler._report_failed_challs([self.http01, self.dvsni_diff])
auth_handler._report_failed_challs([self.http01, self.tls_sni_diff])
self.assertTrue(mock_zope().add_message.call_count == 2)

View File

@@ -1,4 +1,5 @@
"""Tests for letsencrypt.cli."""
import argparse
import itertools
import os
import shutil
@@ -9,9 +10,14 @@ import unittest
import mock
from acme import jose
from letsencrypt import account
from letsencrypt import cli
from letsencrypt import configuration
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.plugins import disco
@@ -19,10 +25,12 @@ from letsencrypt.tests import renewer_test
from letsencrypt.tests import test_util
CERT = test_util.vector_path('cert.pem')
CSR = test_util.vector_path('csr.der')
KEY = test_util.vector_path('rsa256_key.pem')
class CLITest(unittest.TestCase):
class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
"""Tests for different commands."""
def setUp(self):
@@ -30,33 +38,36 @@ class CLITest(unittest.TestCase):
self.config_dir = os.path.join(self.tmp_dir, 'config')
self.work_dir = os.path.join(self.tmp_dir, 'work')
self.logs_dir = os.path.join(self.tmp_dir, 'logs')
self.standard_args = ['--text', '--config-dir', self.config_dir,
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
'--agree-dev-preview']
def tearDown(self):
shutil.rmtree(self.tmp_dir)
def _call(self, args):
from letsencrypt import cli
args = ['--text', '--config-dir', self.config_dir,
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
'--agree-dev-preview'] + args
"Run the cli with output streams and actual client mocked out"
with mock.patch('letsencrypt.cli.client') as client:
ret, stdout, stderr = self._call_no_clientmock(args)
return ret, stdout, stderr, client
def _call_no_clientmock(self, args):
"Run the client with output streams mocked out"
args = self.standard_args + args
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
ret = cli.main(args)
return ret, stdout, stderr, client
ret = cli.main(args[:]) # NOTE: parser can alter its args!
return ret, stdout, stderr
def _call_stdout(self, args):
"""
Variant of _call that preserves stdout so that it can be mocked by the
caller.
"""
from letsencrypt import cli
args = ['--text', '--config-dir', self.config_dir,
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
'--agree-dev-preview'] + args
args = self.standard_args + args
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
ret = cli.main(args)
ret = cli.main(args[:]) # NOTE: parser can alter its args!
return ret, None, stderr, client
def test_no_flags(self):
@@ -112,16 +123,61 @@ class CLITest(unittest.TestCase):
self.assertTrue("--key-path" not in out)
out = self._help_output(['-h'])
from letsencrypt import cli
self.assertTrue(cli.usage_strings(plugins)[0] in out)
@mock.patch('letsencrypt.cli.client.acme_client.Client')
@mock.patch('letsencrypt.cli._determine_account')
@mock.patch('letsencrypt.cli.client.Client.obtain_and_enroll_certificate')
@mock.patch('letsencrypt.cli._auth_from_domains')
def test_user_agent(self, _afd, _obt, det, _client):
# Normally the client is totally mocked out, but here we need more
# arguments to automate it...
args = ["--standalone", "certonly", "-m", "none@none.com",
"-d", "example.com", '--agree-tos'] + self.standard_args
det.return_value = mock.MagicMock(), None
with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net:
self._call_no_clientmock(args)
os_ver = " ".join(le_util.get_os_info())
ua = acme_net.call_args[1]["user_agent"]
self.assertTrue(os_ver in ua)
import platform
plat = platform.platform()
if "linux" in plat.lower():
self.assertTrue(platform.linux_distribution()[0] in ua)
with mock.patch('letsencrypt.cli.client.acme_client.ClientNetwork') as acme_net:
ua = "bandersnatch"
args += ["--user-agent", ua]
self._call_no_clientmock(args)
acme_net.assert_called_once_with(mock.ANY, verify_ssl=True, user_agent=ua)
def test_install_abspath(self):
cert = 'cert'
key = 'key'
chain = 'chain'
fullchain = 'fullchain'
with MockedVerb('install') as mock_install:
self._call(['install', '--cert-path', cert, '--key-path', 'key',
'--chain-path', 'chain',
'--fullchain-path', 'fullchain'])
args = mock_install.call_args[0][0]
self.assertEqual(args.cert_path, os.path.abspath(cert))
self.assertEqual(args.key_path, os.path.abspath(key))
self.assertEqual(args.chain_path, os.path.abspath(chain))
self.assertEqual(args.fullchain_path, os.path.abspath(fullchain))
@mock.patch('letsencrypt.cli.record_chosen_plugins')
@mock.patch('letsencrypt.cli.display_ops')
def test_installer_selection(self, mock_display_ops):
self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert',
def test_installer_selection(self, mock_display_ops, _rec):
self._call(['install', '--domains', 'foo.bar', '--cert-path', 'cert',
'--key-path', 'key', '--chain-path', 'chain'])
self.assertEqual(mock_display_ops.pick_installer.call_count, 1)
def test_configurator_selection(self):
@mock.patch('letsencrypt.le_util.exe_exists')
def test_configurator_selection(self, mock_exe_exists):
mock_exe_exists.return_value = True
real_plugins = disco.PluginsRegistry.find_all()
args = ['--agree-dev-preview', '--apache',
'--authenticator', 'standalone']
@@ -168,6 +224,65 @@ class CLITest(unittest.TestCase):
for r in xrange(len(flags)))):
self._call(['plugins'] + list(args))
@mock.patch('letsencrypt.cli.plugins_disco')
def test_plugins_no_args(self, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins'])
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
stdout.write.called_once_with(str(filtered))
@mock.patch('letsencrypt.cli.plugins_disco')
def test_plugins_init(self, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins', '--init'])
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
self.assertEqual(filtered.init.call_count, 1)
filtered.verify.assert_called_once_with(ifaces)
verified = filtered.verify()
stdout.write.called_once_with(str(verified))
@mock.patch('letsencrypt.cli.plugins_disco')
def test_plugins_prepare(self, mock_disco):
ifaces = []
plugins = mock_disco.PluginsRegistry.find_all()
_, stdout, _, _ = self._call(['plugins', '--init', '--prepare'])
plugins.visible.assert_called_once_with()
plugins.visible().ifaces.assert_called_once_with(ifaces)
filtered = plugins.visible().ifaces()
self.assertEqual(filtered.init.call_count, 1)
filtered.verify.assert_called_once_with(ifaces)
verified = filtered.verify()
verified.prepare.assert_called_once_with()
verified.available.assert_called_once_with()
available = verified.available()
stdout.write.called_once_with(str(available))
def test_certonly_abspath(self):
cert = 'cert'
key = 'key'
chain = 'chain'
fullchain = 'fullchain'
with MockedVerb('certonly') as mock_obtaincert:
self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
'--chain-path', 'chain',
'--fullchain-path', 'fullchain'])
args = mock_obtaincert.call_args[0][0]
self.assertEqual(args.cert_path, os.path.abspath(cert))
self.assertEqual(args.key_path, os.path.abspath(key))
self.assertEqual(args.chain_path, os.path.abspath(chain))
self.assertEqual(args.fullchain_path, os.path.abspath(fullchain))
def test_certonly_bad_args(self):
ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
@@ -175,6 +290,44 @@ class CLITest(unittest.TestCase):
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
def test_check_config_sanity_domain(self):
# Punycode
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'this.is.xn--ls8h.tld'])
# FQDN
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'comma,gotwrong.tld'])
# FQDN 2
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', 'illegal.character=.tld'])
# Wildcard
self.assertRaises(errors.ConfigurationError,
self._call,
['-d', '*.wildcard.tld'])
def test_parse_domains(self):
plugins = disco.PluginsRegistry.find_all()
short_args = ['-d', 'example.com']
namespace = cli.prepare_and_parse_args(plugins, short_args)
self.assertEqual(namespace.domains, ['example.com'])
short_args = ['-d', 'example.com,another.net,third.org,example.com']
namespace = cli.prepare_and_parse_args(plugins, short_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net',
'third.org'])
long_args = ['--domains', 'example.com']
namespace = cli.prepare_and_parse_args(plugins, long_args)
self.assertEqual(namespace.domains, ['example.com'])
long_args = ['--domains', 'example.com,another.net,example.com']
namespace = cli.prepare_and_parse_args(plugins, long_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
@mock.patch('letsencrypt.crypto_util.notAfter')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
@@ -235,7 +388,8 @@ class CLITest(unittest.TestCase):
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
@mock.patch('letsencrypt.cli._init_le_client')
def test_certonly_csr(self, mock_init, mock_get_utility,
@mock.patch('letsencrypt.cli.record_chosen_plugins')
def test_certonly_csr(self, _rec, mock_init, mock_get_utility,
mock_pick_installer, mock_notAfter):
cert_path = '/etc/letsencrypt/live/blahcert.pem'
date = '1970-01-01'
@@ -260,11 +414,31 @@ class CLITest(unittest.TestCase):
self.assertTrue(
date in mock_get_utility().add_message.call_args[0][0])
@mock.patch('letsencrypt.cli.client.acme_client')
def test_revoke_with_key(self, mock_acme_client):
server = 'foo.bar'
self._call_no_clientmock(['--cert-path', CERT, '--key-path', KEY,
'--server', server, 'revoke'])
with open(KEY) as f:
mock_acme_client.Client.assert_called_once_with(
server, key=jose.JWK.load(f.read()), net=mock.ANY)
with open(CERT) as f:
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
mock_revoke = mock_acme_client.Client().revoke
mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
@mock.patch('letsencrypt.cli._determine_account')
def test_revoke_without_key(self, mock_determine_account):
mock_determine_account.return_value = (mock.MagicMock(), None)
_, _, _, client = self._call(['--cert-path', CERT, 'revoke'])
with open(CERT) as f:
cert = crypto_util.pyopenssl_load_certificate(f.read())[0]
mock_revoke = client.acme_from_config_key().revoke
mock_revoke.assert_called_once_with(jose.ComparableX509(cert))
@mock.patch('letsencrypt.cli.sys')
def test_handle_exception(self, mock_sys):
# pylint: disable=protected-access
from letsencrypt import cli
mock_open = mock.mock_open()
with mock.patch('letsencrypt.cli.open', mock_open, create=True):
exception = Exception('detail')
@@ -296,6 +470,19 @@ class CLITest(unittest.TestCase):
mock_sys.exit.assert_called_with(''.join(
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
def test_read_file(self):
rel_test_path = os.path.relpath(os.path.join(self.tmp_dir, 'foo'))
self.assertRaises(
argparse.ArgumentTypeError, cli.read_file, rel_test_path)
test_contents = 'bar\n'
with open(rel_test_path, 'w') as f:
f.write(test_contents)
path, contents = cli.read_file(rel_test_path)
self.assertEqual(path, os.path.abspath(path))
self.assertEqual(contents, test_contents)
class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.cli._determine_account."""
@@ -415,8 +602,6 @@ class MockedVerb(object):
"""
def __init__(self, verb_name):
from letsencrypt import cli
self.verb_dict = cli.HelpfulArgumentParser.VERBS
self.verb_func = None
self.verb_name = verb_name

View File

@@ -70,8 +70,8 @@ class ClientTest(unittest.TestCase):
dv_auth=None, installer=None)
def test_init_acme_verify_ssl(self):
self.acme_client.assert_called_once_with(
directory=mock.ANY, key=mock.ANY, verify_ssl=True)
net = self.acme_client.call_args[1]["net"]
self.assertTrue(net.verify_ssl)
def _mock_obtain_certificate(self):
self.client.auth_handler = mock.MagicMock()
@@ -148,7 +148,7 @@ class ClientTest(unittest.TestCase):
shutil.rmtree(tmp_path)
def test_deploy_certificate(self):
def test_deploy_certificate_success(self):
self.assertRaises(errors.Error, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain", "fullchain")
@@ -166,17 +166,38 @@ class ClientTest(unittest.TestCase):
self.assertEqual(installer.save.call_count, 2)
installer.restart.assert_called_once_with()
def test_deploy_certificate_restart_failure_with_recovery(self):
def test_deploy_certificate_failure(self):
installer = mock.MagicMock()
self.client.installer = installer
installer.deploy_cert.side_effect = errors.PluginError
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain", "fullchain")
installer.recovery_routine.assert_called_once_with()
def test_deploy_certificate_save_failure(self):
installer = mock.MagicMock()
self.client.installer = installer
installer.save.side_effect = errors.PluginError
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain", "fullchain")
installer.recovery_routine.assert_called_once_with()
@mock.patch("letsencrypt.client.zope.component.getUtility")
def test_deploy_certificate_restart_failure(self, mock_get_utility):
installer = mock.MagicMock()
installer.restart.side_effect = [errors.PluginError, None]
self.client.installer = installer
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain", "fullchain")
self.assertEqual(mock_get_utility().add_message.call_count, 1)
installer.rollback_checkpoints.assert_called_once_with()
self.assertEqual(installer.restart.call_count, 2)
def test_deploy_certificate_restart_failure_without_recovery(self):
@mock.patch("letsencrypt.client.zope.component.getUtility")
def test_deploy_certificate_restart_failure2(self, mock_get_utility):
installer = mock.MagicMock()
installer.restart.side_effect = errors.PluginError
installer.rollback_checkpoints.side_effect = errors.ReverterError
@@ -184,6 +205,7 @@ class ClientTest(unittest.TestCase):
self.assertRaises(errors.PluginError, self.client.deploy_certificate,
["foo.bar"], "key", "cert", "chain", "fullchain")
self.assertEqual(mock_get_utility().add_message.call_count, 1)
installer.rollback_checkpoints.assert_called_once_with()
self.assertEqual(installer.restart.call_count, 1)
@@ -201,10 +223,68 @@ class ClientTest(unittest.TestCase):
self.assertEqual(installer.save.call_count, 1)
installer.restart.assert_called_once_with()
def test_enhance_config_no_installer(self):
self.assertRaises(errors.Error,
self.client.enhance_config, ["foo.bar"])
@mock.patch("letsencrypt.client.zope.component.getUtility")
@mock.patch("letsencrypt.client.enhancements")
def test_enhance_config_enhance_failure(self, mock_enhancements,
mock_get_utility):
mock_enhancements.ask.return_value = True
installer = mock.MagicMock()
self.client.installer = installer
installer.enhance.side_effect = errors.PluginError
self.assertRaises(errors.PluginError,
self.client.enhance_config, ["foo.bar"], True)
installer.recovery_routine.assert_called_once_with()
self.assertEqual(mock_get_utility().add_message.call_count, 1)
@mock.patch("letsencrypt.client.zope.component.getUtility")
@mock.patch("letsencrypt.client.enhancements")
def test_enhance_config_save_failure(self, mock_enhancements,
mock_get_utility):
mock_enhancements.ask.return_value = True
installer = mock.MagicMock()
self.client.installer = installer
installer.save.side_effect = errors.PluginError
self.assertRaises(errors.PluginError,
self.client.enhance_config, ["foo.bar"], True)
installer.recovery_routine.assert_called_once_with()
self.assertEqual(mock_get_utility().add_message.call_count, 1)
@mock.patch("letsencrypt.client.zope.component.getUtility")
@mock.patch("letsencrypt.client.enhancements")
def test_enhance_config_restart_failure(self, mock_enhancements,
mock_get_utility):
mock_enhancements.ask.return_value = True
installer = mock.MagicMock()
self.client.installer = installer
installer.restart.side_effect = [errors.PluginError, None]
self.assertRaises(errors.PluginError,
self.client.enhance_config, ["foo.bar"], True)
self.assertEqual(mock_get_utility().add_message.call_count, 1)
installer.rollback_checkpoints.assert_called_once_with()
self.assertEqual(installer.restart.call_count, 2)
@mock.patch("letsencrypt.client.zope.component.getUtility")
@mock.patch("letsencrypt.client.enhancements")
def test_enhance_config_restart_failure2(self, mock_enhancements,
mock_get_utility):
mock_enhancements.ask.return_value = True
installer = mock.MagicMock()
self.client.installer = installer
installer.restart.side_effect = errors.PluginError
installer.rollback_checkpoints.side_effect = errors.ReverterError
self.assertRaises(errors.PluginError,
self.client.enhance_config, ["foo.bar"], True)
self.assertEqual(mock_get_utility().add_message.call_count, 1)
installer.rollback_checkpoints.assert_called_once_with()
self.assertEqual(installer.restart.call_count, 1)
class RollbackTest(unittest.TestCase):

View File

@@ -14,12 +14,12 @@ class NamespaceConfigTest(unittest.TestCase):
self.namespace = mock.MagicMock(
config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar',
server='https://acme-server.org:443/new',
dvsni_port=1234, http01_port=4321)
tls_sni_01_port=1234, http01_port=4321)
from letsencrypt.configuration import NamespaceConfig
self.config = NamespaceConfig(self.namespace)
def test_init_same_ports(self):
self.namespace.dvsni_port = 4321
self.namespace.tls_sni_01_port = 4321
from letsencrypt.configuration import NamespaceConfig
self.assertRaises(errors.Error, NamespaceConfig, self.namespace)
@@ -59,6 +59,38 @@ class NamespaceConfigTest(unittest.TestCase):
self.namespace.http01_port = None
self.assertEqual(80, self.config.http01_port)
def test_absolute_paths(self):
from letsencrypt.configuration import NamespaceConfig
config_base = "foo"
work_base = "bar"
logs_base = "baz"
mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir',
'logs_dir', 'http01_port',
'tls_sni_01_port',
'domains', 'server'])
mock_namespace.config_dir = config_base
mock_namespace.work_dir = work_base
mock_namespace.logs_dir = logs_base
config = NamespaceConfig(mock_namespace)
self.assertTrue(os.path.isabs(config.config_dir))
self.assertEqual(config.config_dir,
os.path.join(os.getcwd(), config_base))
self.assertTrue(os.path.isabs(config.work_dir))
self.assertEqual(config.work_dir,
os.path.join(os.getcwd(), work_base))
self.assertTrue(os.path.isabs(config.logs_dir))
self.assertEqual(config.logs_dir,
os.path.join(os.getcwd(), logs_base))
self.assertTrue(os.path.isabs(config.accounts_dir))
self.assertTrue(os.path.isabs(config.backup_dir))
self.assertTrue(os.path.isabs(config.csr_dir))
self.assertTrue(os.path.isabs(config.in_progress_dir))
self.assertTrue(os.path.isabs(config.key_dir))
self.assertTrue(os.path.isabs(config.temp_checkpoint_dir))
class RenewerConfigurationTest(unittest.TestCase):
"""Test for letsencrypt.configuration.RenewerConfiguration."""
@@ -81,6 +113,28 @@ class RenewerConfigurationTest(unittest.TestCase):
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
def test_absolute_paths(self):
from letsencrypt.configuration import NamespaceConfig
from letsencrypt.configuration import RenewerConfiguration
config_base = "foo"
work_base = "bar"
logs_base = "baz"
mock_namespace = mock.MagicMock(spec=['config_dir', 'work_dir',
'logs_dir', 'http01_port',
'tls_sni_01_port',
'domains', 'server'])
mock_namespace.config_dir = config_base
mock_namespace.work_dir = work_base
mock_namespace.logs_dir = logs_base
config = RenewerConfiguration(NamespaceConfig(mock_namespace))
self.assertTrue(os.path.isabs(config.archive_dir))
self.assertTrue(os.path.isabs(config.live_dir))
self.assertTrue(os.path.isabs(config.renewal_configs_dir))
self.assertTrue(os.path.isabs(config.renewer_config_file))
if __name__ == '__main__':
unittest.main() # pragma: no cover

View File

@@ -34,7 +34,7 @@ class PerformTest(unittest.TestCase):
def test_unexpected(self):
self.assertRaises(
errors.ContAuthError, self.auth.perform, [
achallenges.DVSNI(
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=None, domain="0", account_key="invalid_key")])
def test_chall_pref(self):
@@ -53,7 +53,7 @@ class CleanupTest(unittest.TestCase):
mock.MagicMock(server="demo_server.org"), None)
def test_unexpected(self):
unexpected = achallenges.DVSNI(
unexpected = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=None, domain="0", account_key="dummy_key")
self.assertRaises(errors.ContAuthError, self.auth.cleanup, [unexpected])

View File

@@ -13,7 +13,11 @@ class ErrorHandlerTest(unittest.TestCase):
from letsencrypt import error_handler
self.init_func = mock.MagicMock()
self.handler = error_handler.ErrorHandler(self.init_func)
self.init_args = set((42,))
self.init_kwargs = {'foo': 'bar'}
self.handler = error_handler.ErrorHandler(self.init_func,
*self.init_args,
**self.init_kwargs)
# pylint: disable=protected-access
self.signals = error_handler._SIGNALS
@@ -23,7 +27,8 @@ class ErrorHandlerTest(unittest.TestCase):
raise ValueError
except ValueError:
pass
self.init_func.assert_called_once_with()
self.init_func.assert_called_once_with(*self.init_args,
**self.init_kwargs)
@mock.patch('letsencrypt.error_handler.os')
@mock.patch('letsencrypt.error_handler.signal')
@@ -37,7 +42,8 @@ class ErrorHandlerTest(unittest.TestCase):
signum = self.signals[0]
signal_handler(signum, None)
self.init_func.assert_called_once_with()
self.init_func.assert_called_once_with(*self.init_args,
**self.init_kwargs)
mock_os.kill.assert_called_once_with(mock_os.getpid(), signum)
self.handler.reset_signal_handlers()
@@ -48,7 +54,8 @@ class ErrorHandlerTest(unittest.TestCase):
bad_func = mock.MagicMock(side_effect=[ValueError])
self.handler.register(bad_func)
self.handler.call_registered()
self.init_func.assert_called_once_with()
self.init_func.assert_called_once_with(*self.init_args,
**self.init_kwargs)
bad_func.assert_called_once_with()
def test_sysexit_ignored(self):

View File

@@ -688,9 +688,13 @@ class RenewableCertTests(BaseRenewableCertTest):
self.test_rc.configfile["renewalparams"]["rsa_key_size"] = "2048"
self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com"
self.test_rc.configfile["renewalparams"]["authenticator"] = "fake"
self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430"
self.test_rc.configfile["renewalparams"]["tls_sni_01_port"] = "4430"
self.test_rc.configfile["renewalparams"]["http01_port"] = "1234"
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
self.test_rc.configfile["renewalparams"]["domains"] = ["example.com"]
self.test_rc.configfile["renewalparams"]["config_dir"] = "config"
self.test_rc.configfile["renewalparams"]["work_dir"] = "work"
self.test_rc.configfile["renewalparams"]["logs_dir"] = "logs"
mock_auth = mock.MagicMock()
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
# Fails because "fake" != "apache"

View File

@@ -225,25 +225,25 @@ htmlhelp_basename = 'letshelp-letsencryptdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
(master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation',
u'Let\'s Encrypt Project', 'manual'),
(master_doc, 'letshelp-letsencrypt.tex', u'letshelp-letsencrypt Documentation',
u'Let\'s Encrypt Project', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
@@ -286,9 +286,9 @@ man_pages = [
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
(master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation',
author, 'letshelp-letsencrypt', 'One line description of project.',
'Miscellaneous'),
(master_doc, 'letshelp-letsencrypt', u'letshelp-letsencrypt Documentation',
author, 'letshelp-letsencrypt', 'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.

View File

@@ -27,7 +27,7 @@ common() {
"$@"
}
common --domains le1.wtf --standalone-supported-challenges dvsni auth
common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth
common --domains le2.wtf --standalone-supported-challenges http-01 run
common -a manual -d le.wtf auth
@@ -40,7 +40,7 @@ common auth --csr "$CSR_PATH" \
openssl x509 -in "${root}/csr/0000_cert.pem" -text
openssl x509 -in "${root}/csr/0000_chain.pem" -text
common --domain le3.wtf install \
common --domains le3.wtf install \
--cert-path "${root}/csr/cert.pem" \
--key-path "${root}/csr/key.pem"

View File

@@ -15,7 +15,7 @@ letsencrypt_test () {
letsencrypt \
--server "${SERVER:-http://localhost:4000/directory}" \
--no-verify-ssl \
--dvsni-port 5001 \
--tls-sni-01-port 5001 \
--http-01-port 5002 \
--manual-test-mode \
$store_flags \