Merge remote-tracking branch 'origin/master' into split-renew

This commit is contained in:
Peter Eckersley 2016-03-17 16:19:56 -07:00
commit b4ed78a31b
19 changed files with 86 additions and 588 deletions

1
.gitignore vendored
View file

@ -8,6 +8,7 @@ dist*/
/.tox/
/releases/
letsencrypt.log
letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64
# coverage
.coverage

View file

@ -90,6 +90,11 @@ IRC Channel: #letsencrypt on `Freenode`_
Community: https://community.letsencrypt.org
ACME spec: http://ietf-wg-acme.github.io/acme/
ACME working area in github: https://github.com/ietf-wg-acme/acme
Mailing list: `client-dev`_ (to subscribe without a Google account, send an
email to client-dev+subscribe@letsencrypt.org)

View file

@ -13,7 +13,6 @@ from acme import errors
from acme import crypto_util
from acme import fields
from acme import jose
from acme import other
logger = logging.getLogger(__name__)
@ -36,14 +35,6 @@ class Challenge(jose.TypedJSONObjectWithFields):
return UnrecognizedChallenge.from_json(jobj)
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
"""Client validation challenges."""
class DVChallenge(Challenge): # pylint: disable=abstract-method
"""Domain validation challenges."""
class ChallengeResponse(jose.TypedJSONObjectWithFields):
# _fields_to_partial_json | pylint: disable=abstract-method
"""ACME challenge response."""
@ -78,8 +69,8 @@ class UnrecognizedChallenge(Challenge):
return cls(jobj)
class _TokenDVChallenge(DVChallenge):
"""DV Challenge with token.
class _TokenChallenge(Challenge):
"""Challenge with token.
:ivar bytes token:
@ -149,7 +140,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
return True
class KeyAuthorizationChallenge(_TokenDVChallenge):
class KeyAuthorizationChallenge(_TokenChallenge):
# pylint: disable=abstract-class-little-used,too-many-ancestors
"""Challenge based on Key Authorization.
@ -460,108 +451,8 @@ class TLSSNI01(KeyAuthorizationChallenge):
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
@Challenge.register
class RecoveryContact(ContinuityChallenge):
"""ACME "recoveryContact" challenge.
:ivar unicode activation_url:
:ivar unicode success_url:
:ivar unicode contact:
"""
typ = "recoveryContact"
activation_url = jose.Field("activationURL", omitempty=True)
success_url = jose.Field("successURL", omitempty=True)
contact = jose.Field("contact", omitempty=True)
@ChallengeResponse.register
class RecoveryContactResponse(ChallengeResponse):
"""ACME "recoveryContact" challenge response.
:ivar unicode token:
"""
typ = "recoveryContact"
token = jose.Field("token", omitempty=True)
@Challenge.register
class ProofOfPossession(ContinuityChallenge):
"""ACME "proofOfPossession" challenge.
:ivar .JWAAlgorithm alg:
:ivar bytes nonce: Random data, **not** base64-encoded.
:ivar hints: Various clues for the client (:class:`Hints`).
"""
typ = "proofOfPossession"
NONCE_SIZE = 16
class Hints(jose.JSONObjectWithFields):
"""Hints for "proofOfPossession" challenge.
:ivar JWK jwk: JSON Web Key
:ivar tuple cert_fingerprints: `tuple` of `unicode`
:ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509`
certificates.
:ivar tuple subject_key_identifiers: `tuple` of `unicode`
:ivar tuple issuers: `tuple` of `unicode`
:ivar tuple authorized_for: `tuple` of `unicode`
"""
jwk = jose.Field("jwk", decoder=jose.JWK.from_json)
cert_fingerprints = jose.Field(
"certFingerprints", omitempty=True, default=())
certs = jose.Field("certs", omitempty=True, default=())
subject_key_identifiers = jose.Field(
"subjectKeyIdentifiers", omitempty=True, default=())
serial_numbers = jose.Field("serialNumbers", omitempty=True, default=())
issuers = jose.Field("issuers", omitempty=True, default=())
authorized_for = jose.Field("authorizedFor", omitempty=True, default=())
@certs.encoder
def certs(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(jose.encode_cert(cert) for cert in value)
@certs.decoder
def certs(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(jose.decode_cert(cert) for cert in value)
alg = jose.Field("alg", decoder=jose.JWASignature.from_json)
nonce = jose.Field(
"nonce", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=NONCE_SIZE))
hints = jose.Field("hints", decoder=Hints.from_json)
@ChallengeResponse.register
class ProofOfPossessionResponse(ChallengeResponse):
"""ACME "proofOfPossession" challenge response.
:ivar bytes nonce: Random data, **not** base64-encoded.
:ivar acme.other.Signature signature: Sugnature of this message.
"""
typ = "proofOfPossession"
NONCE_SIZE = ProofOfPossession.NONCE_SIZE
nonce = jose.Field(
"nonce", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=NONCE_SIZE))
signature = jose.Field("signature", decoder=other.Signature.from_json)
def verify(self):
"""Verify the challenge."""
# self.signature is not Field | pylint: disable=no-member
return self.signature.verify(self.nonce)
@Challenge.register # pylint: disable=too-many-ancestors
class DNS(_TokenDVChallenge):
class DNS(_TokenChallenge):
"""ACME "dns" challenge."""
typ = "dns"

View file

@ -9,7 +9,6 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err
from acme import errors
from acme import jose
from acme import other
from acme import test_util
@ -324,233 +323,6 @@ class TLSSNI01Test(unittest.TestCase):
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
class RecoveryContactTest(unittest.TestCase):
def setUp(self):
from acme.challenges import RecoveryContact
self.msg = RecoveryContact(
activation_url='https://example.ca/sendrecovery/a5bd99383fb0',
success_url='https://example.ca/confirmrecovery/bb1b9928932',
contact='c********n@example.com')
self.jmsg = {
'type': 'recoveryContact',
'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0',
'successURL': 'https://example.ca/confirmrecovery/bb1b9928932',
'contact': 'c********n@example.com',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import RecoveryContact
self.assertEqual(self.msg, RecoveryContact.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import RecoveryContact
hash(RecoveryContact.from_json(self.jmsg))
def test_json_without_optionals(self):
del self.jmsg['activationURL']
del self.jmsg['successURL']
del self.jmsg['contact']
from acme.challenges import RecoveryContact
msg = RecoveryContact.from_json(self.jmsg)
self.assertTrue(msg.activation_url is None)
self.assertTrue(msg.success_url is None)
self.assertTrue(msg.contact is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class RecoveryContactResponseTest(unittest.TestCase):
def setUp(self):
from acme.challenges import RecoveryContactResponse
self.msg = RecoveryContactResponse(token='23029d88d9e123e')
self.jmsg = {
'resource': 'challenge',
'type': 'recoveryContact',
'token': '23029d88d9e123e',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import RecoveryContactResponse
self.assertEqual(
self.msg, RecoveryContactResponse.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import RecoveryContactResponse
hash(RecoveryContactResponse.from_json(self.jmsg))
def test_json_without_optionals(self):
del self.jmsg['token']
from acme.challenges import RecoveryContactResponse
msg = RecoveryContactResponse.from_json(self.jmsg)
self.assertTrue(msg.token is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class ProofOfPossessionHintsTest(unittest.TestCase):
def setUp(self):
jwk = KEY.public_key()
issuers = (
'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA',
'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure',
)
cert_fingerprints = (
'93416768eb85e33adc4277f4c9acd63e7418fcfe',
'16d95b7b63f1972b980b14c20291f3c0d1855d95',
'48b46570d9fc6358108af43ad1649484def0debf',
)
subject_key_identifiers = ('d0083162dcc4c8a23ecb8aecbd86120e56fd24e5')
authorized_for = ('www.example.com', 'example.net')
serial_numbers = (34234239832, 23993939911, 17)
from acme.challenges import ProofOfPossession
self.msg = ProofOfPossession.Hints(
jwk=jwk, issuers=issuers, cert_fingerprints=cert_fingerprints,
certs=(CERT,), subject_key_identifiers=subject_key_identifiers,
authorized_for=authorized_for, serial_numbers=serial_numbers)
self.jmsg_to = {
'jwk': jwk,
'certFingerprints': cert_fingerprints,
'certs': (jose.encode_b64jose(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped)),),
'subjectKeyIdentifiers': subject_key_identifiers,
'serialNumbers': serial_numbers,
'issuers': issuers,
'authorizedFor': authorized_for,
}
self.jmsg_from = self.jmsg_to.copy()
self.jmsg_from.update({'jwk': jwk.to_json()})
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 ProofOfPossession
self.assertEqual(
self.msg, ProofOfPossession.Hints.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import ProofOfPossession
hash(ProofOfPossession.Hints.from_json(self.jmsg_from))
def test_json_without_optionals(self):
for optional in ['certFingerprints', 'certs', 'subjectKeyIdentifiers',
'serialNumbers', 'issuers', 'authorizedFor']:
del self.jmsg_from[optional]
del self.jmsg_to[optional]
from acme.challenges import ProofOfPossession
msg = ProofOfPossession.Hints.from_json(self.jmsg_from)
self.assertEqual(msg.cert_fingerprints, ())
self.assertEqual(msg.certs, ())
self.assertEqual(msg.subject_key_identifiers, ())
self.assertEqual(msg.serial_numbers, ())
self.assertEqual(msg.issuers, ())
self.assertEqual(msg.authorized_for, ())
self.assertEqual(self.jmsg_to, msg.to_partial_json())
class ProofOfPossessionTest(unittest.TestCase):
def setUp(self):
from acme.challenges import ProofOfPossession
hints = ProofOfPossession.Hints(
jwk=KEY.public_key(), cert_fingerprints=(),
certs=(), serial_numbers=(), subject_key_identifiers=(),
issuers=(), authorized_for=())
self.msg = ProofOfPossession(
alg=jose.RS256, hints=hints,
nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ')
self.jmsg_to = {
'type': 'proofOfPossession',
'alg': jose.RS256,
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'hints': hints,
}
self.jmsg_from = {
'type': 'proofOfPossession',
'alg': jose.RS256.to_json(),
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'hints': hints.to_json(),
}
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 ProofOfPossession
self.assertEqual(
self.msg, ProofOfPossession.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import ProofOfPossession
hash(ProofOfPossession.from_json(self.jmsg_from))
class ProofOfPossessionResponseTest(unittest.TestCase):
def setUp(self):
# acme-spec uses a confusing example in which both signature
# nonce and challenge nonce are the same, don't make the same
# mistake here...
signature = other.Signature(
alg=jose.RS256, jwk=KEY.public_key(),
sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83'
b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap'
b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde'
b'\x99\x08\xf0\x0e{',
nonce=b'\x99\xc7Q\xb3f2\xbc\xdci\xfe\xd6\x98k\xc67\xdf',
)
from acme.challenges import ProofOfPossessionResponse
self.msg = ProofOfPossessionResponse(
nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ',
signature=signature)
self.jmsg_to = {
'resource': 'challenge',
'type': 'proofOfPossession',
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'signature': signature,
}
self.jmsg_from = {
'resource': 'challenge',
'type': 'proofOfPossession',
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'signature': signature.to_json(),
}
def test_verify(self):
self.assertTrue(self.msg.verify())
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 ProofOfPossessionResponse
self.assertEqual(
self.msg, ProofOfPossessionResponse.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import ProofOfPossessionResponse
hash(ProofOfPossessionResponse.from_json(self.jmsg_from))
class DNSTest(unittest.TestCase):
def setUp(self):

View file

@ -271,10 +271,8 @@ class AuthorizationTest(unittest.TestCase):
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS(
token=b'DGyRejmCefe7v4NfDGDKfA')),
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
chall=challenges.RecoveryContact()),
)
combinations = ((0, 2), (1, 2))
combinations = ((0,), (1,))
from acme.messages import Authorization
from acme.messages import Identifier
@ -300,8 +298,8 @@ class AuthorizationTest(unittest.TestCase):
def test_resolved_combinations(self):
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0], self.challbs[2]),
(self.challbs[1], self.challbs[2]),
(self.challbs[0],),
(self.challbs[1],),
))

View file

@ -1,67 +0,0 @@
"""Other ACME objects."""
import functools
import logging
import os
from acme import jose
logger = logging.getLogger(__name__)
class Signature(jose.JSONObjectWithFields):
"""ACME signature.
:ivar .JWASignature alg: Signature algorithm.
:ivar bytes sig: Signature.
:ivar bytes nonce: Nonce.
:ivar .JWK jwk: JWK.
"""
NONCE_SIZE = 16
"""Minimum size of nonce in bytes."""
alg = jose.Field('alg', decoder=jose.JWASignature.from_json)
sig = jose.Field('sig', encoder=jose.encode_b64jose,
decoder=jose.decode_b64jose)
nonce = jose.Field(
'nonce', encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=NONCE_SIZE, minimum=True))
jwk = jose.Field('jwk', decoder=jose.JWK.from_json)
@classmethod
def from_msg(cls, msg, key, nonce=None, nonce_size=None, alg=jose.RS256):
"""Create signature with nonce prepended to the message.
:param bytes msg: Message to be signed.
:param key: Key used for signing.
:type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
(optionally wrapped in `.ComparableRSAKey`).
:param bytes nonce: Nonce to be used. If None, nonce of
``nonce_size`` will be randomly generated.
:param int nonce_size: Size of the automatically generated nonce.
Defaults to :const:`NONCE_SIZE`.
:param .JWASignature alg:
"""
nonce_size = cls.NONCE_SIZE if nonce_size is None else nonce_size
nonce = os.urandom(nonce_size) if nonce is None else nonce
msg_with_nonce = nonce + msg
sig = alg.sign(key, nonce + msg)
logger.debug('%r signed as %r', msg_with_nonce, sig)
return cls(alg=alg, sig=sig, nonce=nonce,
jwk=alg.kty(key=key.public_key()))
def verify(self, msg):
"""Verify the signature.
:param bytes msg: Message that was used in signing.
"""
# self.alg is not Field, but JWA | pylint: disable=no-member
return self.alg.verify(self.jwk.key, self.nonce + msg, self.sig)

View file

@ -1,94 +0,0 @@
"""Tests for acme.sig."""
import unittest
from acme import jose
from acme import test_util
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class SignatureTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
"""Tests for acme.sig.Signature."""
def setUp(self):
self.msg = b'message'
self.sig = (b'IC\xd8*\xe7\x14\x9e\x19S\xb7\xcf\xec3\x12\xe2\x8a\x03'
b'\x98u\xff\xf0\x94\xe2\xd7<\x8f\xa8\xed\xa4KN\xc3\xaa'
b'\xb9X\xc3w\xaa\xc0_\xd0\x05$y>l#\x10<\x96\xd2\xcdr\xa3'
b'\x1b\xa1\xf5!f\xef\xc64\xb6\x13')
self.nonce = b'\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.alg = jose.RS256
self.jwk = jose.JWKRSA(key=KEY.public_key())
b64sig = ('SUPYKucUnhlTt8_sMxLiigOYdf_wlOLXPI-o7aRLTsOquVjDd6r'
'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew')
b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'
self.jsig_to = {
'nonce': b64nonce,
'alg': self.alg,
'jwk': self.jwk,
'sig': b64sig,
}
self.jsig_from = {
'nonce': b64nonce,
'alg': self.alg.to_partial_json(),
'jwk': self.jwk.to_partial_json(),
'sig': b64sig,
}
from acme.other import Signature
self.signature = Signature(
alg=self.alg, sig=self.sig, nonce=self.nonce, jwk=self.jwk)
def test_attributes(self):
self.assertEqual(self.signature.nonce, self.nonce)
self.assertEqual(self.signature.alg, self.alg)
self.assertEqual(self.signature.sig, self.sig)
self.assertEqual(self.signature.jwk, self.jwk)
def test_verify_good_succeeds(self):
self.assertTrue(self.signature.verify(self.msg))
def test_verify_bad_fails(self):
self.assertFalse(self.signature.verify(self.msg + b'x'))
@classmethod
def _from_msg(cls, *args, **kwargs):
from acme.other import Signature
return Signature.from_msg(*args, **kwargs)
def test_create_from_msg(self):
signature = self._from_msg(self.msg, KEY, self.nonce)
self.assertEqual(self.signature, signature)
def test_create_from_msg_random_nonce(self):
signature = self._from_msg(self.msg, KEY)
self.assertEqual(signature.alg, self.alg)
self.assertEqual(signature.jwk, self.jwk)
self.assertTrue(signature.verify(self.msg))
def test_to_partial_json(self):
self.assertEqual(self.signature.to_partial_json(), self.jsig_to)
def test_from_json(self):
from acme.other import Signature
self.assertEqual(
self.signature, Signature.from_json(self.jsig_from))
def test_from_json_non_schema_errors(self):
from acme.other import Signature
jwk = self.jwk.to_partial_json()
self.assertRaises(
jose.DeserializationError, Signature.from_json, {
'alg': 'RS256', 'sig': 'x', 'nonce': '', 'jwk': jwk})
self.assertRaises(
jose.DeserializationError, Signature.from_json, {
'alg': 'RS256', 'sig': '', 'nonce': 'x', 'jwk': jwk})
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -71,11 +71,12 @@ Plugin Auth Inst Notes
=========== ==== ==== ===============================================================
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
Debian-based distributions with ``libaugeas0`` 1.0+.
standalone_ Y N Uses a "standalone" webserver to obtain a cert. This is useful
on systems with no webserver, or when direct integration with
the local webserver is not supported or not desired.
webroot_ Y N Obtains a cert by writing to the webroot directory of an
already running webserver.
standalone_ Y N Uses a "standalone" webserver to obtain a cert. Requires
port 80 or 443 to be available. This is useful on systems
with no webserver, or when direct integration with the local
webserver is not supported or not desired.
manual_ Y N Helps you obtain a cert by giving you instructions to perform
domain validation yourself.
nginx_ Y Y Very experimental and not included in letsencrypt-auto_.
@ -87,15 +88,16 @@ There are also a number of third-party plugins for the client, provided by other
Plugin Auth Inst Notes
=========== ==== ==== ===============================================================
plesk_ Y Y Integration with the Plesk web hosting tool
https://github.com/plesk/letsencrypt-plesk
haproxy_ Y Y Integration with the HAProxy load balancer
https://code.greenhost.net/open/letsencrypt-haproxy
s3front_ Y Y Integration with Amazon CloudFront distribution of S3 buckets
https://github.com/dlapiduz/letsencrypt-s3front
gandi_ Y Y Integration with Gandi's hosting products and API
https://github.com/Gandi/letsencrypt-gandi
=========== ==== ==== ===============================================================
.. _plesk: https://github.com/plesk/letsencrypt-plesk
.. _haproxy: https://code.greenhost.net/open/letsencrypt-haproxy
.. _s3front: https://github.com/dlapiduz/letsencrypt-s3front
.. _gandi: https://github.com/Gandi/letsencrypt-gandi
Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to
be installers but not authenticators.
@ -126,7 +128,9 @@ potentially be a separate directory for each domain. When requested a
certificate for multiple domains, each domain will use the most recently
specified ``--webroot-path``. So, for instance,
``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net``
::
letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/other -d other.example.net -d another.other.example.net
would obtain a single certificate for all of those names, using the
``/var/www/example`` webroot directory for the first two, and

View file

@ -545,6 +545,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
paths = self.aug.match(
("/files%s//*[label()=~regexp('%s')]" %
(vhost_path, parser.case_i("VirtualHost"))))
paths = [path for path in paths if os.path.basename(path) == "VirtualHost"]
for path in paths:
new_vhost = self._create_vhost(path)
realpath = os.path.realpath(new_vhost.filep)

View file

@ -83,7 +83,8 @@ def _vhost_menu(domain, vhosts):
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
"We were unable to find a vhost with a ServerName "
"or Address of {0}.{1}Which virtual host would you "
"like to choose?".format(domain, os.linesep),
"like to choose?\n(note: conf files with multiple "
"vhosts are not yet supported)".format(domain, os.linesep),
choices, help_label="More Info", ok_label="Select")
except errors.MissingCommandlineFlag as e:
msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}"

View file

@ -1,6 +0,0 @@
XQAAAAT//////////wApLArrUzOk5bRHUk0UvMS4xjyZkm3U3qhnKvMbEan7rVeK6yBlbwGeeWFn
Sw4XT1raGAMNq7cwyJvT7ql93Df7TpuRnxNSbPx7q52GojYyb5Oj1IQ2Y22Mvq41Q4K3kCZcVv+1
YVKW3OazUn+wCnaoGhDdMFmH0EKbEPSGibba6HJqUoFosaDE2hRZmjqYR/VwwPCtW820L0Qz9PZ7
DEAZ5VdMmj1+u+bYjDEcZD5+DyWKoLWci8tBXcPGiSvPDdZax/IWmR0GGUOd13gC7uX/HM2dHgbM
Izh7Y3PPNEzM8Fu2wdXLoMCaYrQcrPAdKhsnyMCDbjxCVbD9LkS17xCq4LUMkcz/fMu3/CRSMMZ7
gnn//jNQAA==

View file

@ -1,4 +1,4 @@
"""Let's Encrypt command CLI argument processing."""
"""Let's Encrypt command line argument & config processing."""
from __future__ import print_function
import argparse
import glob
@ -292,7 +292,6 @@ def argparse_type(variable):
return action.type
return str
def read_file(filename, mode="rb"):
"""Returns the given file's contents.
@ -355,14 +354,17 @@ class HelpfulArgumentParser(object):
"""
def __init__(self, args, plugins, detect_defaults=False):
from letsencrypt import main
self.VERBS = main.VERBS
# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security",
"paths", "automation", "testing"] + list(six.iterkeys(self.VERBS))
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
"config_changes": main.config_changes, "run": main.run,
"install": main.install, "plugins": main.plugins_cmd,
"renew": renew, "revoke": main.revoke,
"rollback": main.rollback, "everything": main.run}
plugin_names = list(six.iterkeys(plugins))
# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"] + list(self.VERBS)
plugin_names = list(plugins)
self.help_topics = HELP_TOPICS + plugin_names + [None]
usage, short_usage = usage_strings(plugins)
self.parser = configargparse.ArgParser(
@ -844,6 +846,10 @@ 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_group("config_changes",
description="Options for showing a history of config changes")
helpful.add("config_changes", "--num", type=int,
help="How many past revisions you want to be displayed")
helpful.add(
None, "--user-agent", default=None,
help="Set a custom user agent string for the client. User agent strings allow "

View file

@ -536,7 +536,7 @@ def rollback(default_installer, checkpoints, config, plugins):
installer.restart()
def view_config_changes(config):
def view_config_changes(config, num=None):
"""View checkpoints and associated configuration changes.
.. note:: This assumes that the installation is using a Reverter object.
@ -547,7 +547,7 @@ def view_config_changes(config):
"""
rev = reverter.Reverter(config)
rev.recovery_routine()
rev.view_config_changes()
rev.view_config_changes(num)
def _save_chain(chain_pem, chain_path):

View file

@ -6,6 +6,7 @@ import logging
import os
import platform
import re
import six
import socket
import stat
import subprocess
@ -310,10 +311,13 @@ def enforce_domain_sanity(domain):
# Unicode
try:
domain = domain.encode('ascii').lower()
except UnicodeDecodeError:
raise errors.ConfigurationError(
"Internationalized domain names are not presently supported: {0}"
.format(domain))
except UnicodeError:
error_fmt = (u"Internationalized domain names "
"are not presently supported: {0}")
if isinstance(domain, six.text_type):
raise errors.ConfigurationError(error_fmt.format(domain))
else:
raise errors.ConfigurationError(str(error_fmt).format(domain))
# Remove trailing dot
domain = domain[:-1] if domain.endswith('.') else domain

View file

@ -2,10 +2,17 @@
from __future__ import print_function
import atexit
import functools
import logging.handlers
import os
import sys
import time
import traceback
import OpenSSL
import zope.component
from acme import jose
import letsencrypt
from letsencrypt import account
@ -26,11 +33,6 @@ from letsencrypt import storage
from letsencrypt.display import util as display_util, ops as display_ops
from letsencrypt.plugins import disco as plugins_disco
import traceback
import logging.handlers
import time
from acme import jose
import OpenSSL
logger = logging.getLogger(__name__)
@ -156,7 +158,7 @@ def _handle_subset_cert_request(config, domains, cert):
br=os.linesep)
if config.expand or config.renew_by_default or zope.component.getUtility(
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
cli_flag="--expand (or in some cases, --duplicate)"):
cli_flag="--expand"):
return "renew", cert
else:
reporter_util = zope.component.getUtility(interfaces.IReporter)
@ -454,7 +456,7 @@ def config_changes(config, unused_plugins):
View checkpoints and associated configuration changes.
"""
client.view_config_changes(config)
client.view_config_changes(config, num=config.num)
def revoke(config, unused_plugins): # TODO: coop with renewal config
@ -700,15 +702,6 @@ def main(cli_args=sys.argv[1:]):
return config.func(config, plugins)
# Maps verbs/subcommands to the functions that implement them
# In principle this should live in cli.HelpfulArgumentParser, but
# due to issues with import cycles and testing, it lives here
VERBS = {"auth": obtain_cert, "certonly": obtain_cert,
"config_changes": config_changes, "everything": run,
"install": install, "plugins": plugins_cmd, "renew": renew.renew,
"revoke": revoke, "rollback": rollback, "run": run}
if __name__ == "__main__":
err_string = main()
if err_string:

View file

@ -94,7 +94,7 @@ class Reverter(object):
"Unable to load checkpoint during rollback")
rollback -= 1
def view_config_changes(self, for_logging=False):
def view_config_changes(self, for_logging=False, num=None):
"""Displays all saved checkpoints.
All checkpoints are printed by
@ -107,7 +107,8 @@ class Reverter(object):
"""
backups = os.listdir(self.config.backup_dir)
backups.sort(reverse=True)
if num:
backups = backups[:num]
if not backups:
logger.info("The Let's Encrypt client has not saved any backups "
"of your configuration")

View file

@ -79,7 +79,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
return ret, None, stderr, client
def test_no_flags(self):
with MockedVerb("run") as mock_run:
with mock.patch('letsencrypt.main.run') as mock_run:
self._call([])
self.assertEqual(1, mock_run.call_count)
@ -190,7 +190,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
chain = 'chain'
fullchain = 'fullchain'
with MockedVerb('install') as mock_install:
with mock.patch('letsencrypt.main.install') as mock_install:
self._call(['install', '--cert-path', cert, '--key-path', 'key',
'--chain-path', 'chain',
'--fullchain-path', 'fullchain'])
@ -248,7 +248,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
unused_config, auth, unused_installer = mock_init.call_args[0]
self.assertTrue(isinstance(auth, manual.Authenticator))
with MockedVerb("certonly") as mock_certonly:
with mock.patch('letsencrypt.main.obtain_cert') as mock_certonly:
self._call(["auth", "--standalone"])
self.assertEqual(1, mock_certonly.call_count)
@ -321,7 +321,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
chain = 'chain'
fullchain = 'fullchain'
with MockedVerb('certonly') as mock_obtaincert:
with mock.patch('letsencrypt.main.obtain_cert') as mock_obtaincert:
self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
'--chain-path', 'chain',
'--fullchain-path', 'fullchain'])
@ -900,7 +900,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(contents, test_contents)
def test_agree_dev_preview_config(self):
with MockedVerb('run') as mocked_run:
with mock.patch('letsencrypt.main.run') as mocked_run:
self._call(['-c', test_util.vector_path('cli.ini')])
self.assertTrue(mocked_run.called)
@ -1010,34 +1010,5 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
self.assertEqual(result, (None, None))
class MockedVerb(object):
"""Simple class that can be used for mocking out verbs/subcommands.
Storing a dictionary of verbs and the functions that implement them
in letsencrypt.cli makes mocking much more complicated. This class
can be used as a simple context manager for mocking out verbs in CLI
tests. For example:
with MockedVerb("run") as mock_run:
self._call([])
self.assertEqual(1, mock_run.call_count)
"""
def __init__(self, verb_name):
self.verb_dict = main.VERBS
self.verb_func = None
self.verb_name = verb_name
def __enter__(self):
self.verb_func = self.verb_dict[self.verb_name]
mocked_func = mock.MagicMock()
self.verb_dict[self.verb_name] = mocked_func
return mocked_func
def __exit__(self, unused_type, unused_value, unused_trace):
self.verb_dict[self.verb_name] = self.verb_func
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -8,6 +8,7 @@ import letsencrypt.errors as errors
from letsencrypt.display import util as display_util
CHOICES = [("First", "Description1"), ("Second", "Description2")]
TAGS = ["tag1", "tag2", "tag3"]
TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]

View file

@ -323,5 +323,21 @@ class AddDeprecatedArgumentTest(unittest.TestCase):
self.assertTrue("--old-option" not in stdout.getvalue())
class EnforceDomainSanityTest(unittest.TestCase):
"""Test enforce_domain_sanity."""
def _call(self, domain):
from letsencrypt.le_util import enforce_domain_sanity
return enforce_domain_sanity(domain)
def test_nonascii_str(self):
self.assertRaises(errors.ConfigurationError, self._call,
u"eichh\u00f6rnchen.example.com".encode("utf-8"))
def test_nonascii_unicode(self):
self.assertRaises(errors.ConfigurationError, self._call,
u"eichh\u00f6rnchen.example.com")
if __name__ == "__main__":
unittest.main() # pragma: no cover