mirror of
https://github.com/certbot/certbot.git
synced 2026-08-01 08:08:00 +02:00
Merge remote-tracking branch 'origin/master' into split-renew
This commit is contained in:
@@ -8,6 +8,7 @@ dist*/
|
|||||||
/.tox/
|
/.tox/
|
||||||
/releases/
|
/releases/
|
||||||
letsencrypt.log
|
letsencrypt.log
|
||||||
|
letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64
|
||||||
|
|
||||||
# coverage
|
# coverage
|
||||||
.coverage
|
.coverage
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ IRC Channel: #letsencrypt on `Freenode`_
|
|||||||
|
|
||||||
Community: https://community.letsencrypt.org
|
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
|
Mailing list: `client-dev`_ (to subscribe without a Google account, send an
|
||||||
email to client-dev+subscribe@letsencrypt.org)
|
email to client-dev+subscribe@letsencrypt.org)
|
||||||
|
|
||||||
|
|||||||
+4
-113
@@ -13,7 +13,6 @@ from acme import errors
|
|||||||
from acme import crypto_util
|
from acme import crypto_util
|
||||||
from acme import fields
|
from acme import fields
|
||||||
from acme import jose
|
from acme import jose
|
||||||
from acme import other
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -36,14 +35,6 @@ class Challenge(jose.TypedJSONObjectWithFields):
|
|||||||
return UnrecognizedChallenge.from_json(jobj)
|
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):
|
class ChallengeResponse(jose.TypedJSONObjectWithFields):
|
||||||
# _fields_to_partial_json | pylint: disable=abstract-method
|
# _fields_to_partial_json | pylint: disable=abstract-method
|
||||||
"""ACME challenge response."""
|
"""ACME challenge response."""
|
||||||
@@ -78,8 +69,8 @@ class UnrecognizedChallenge(Challenge):
|
|||||||
return cls(jobj)
|
return cls(jobj)
|
||||||
|
|
||||||
|
|
||||||
class _TokenDVChallenge(DVChallenge):
|
class _TokenChallenge(Challenge):
|
||||||
"""DV Challenge with token.
|
"""Challenge with token.
|
||||||
|
|
||||||
:ivar bytes token:
|
:ivar bytes token:
|
||||||
|
|
||||||
@@ -149,7 +140,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class KeyAuthorizationChallenge(_TokenDVChallenge):
|
class KeyAuthorizationChallenge(_TokenChallenge):
|
||||||
# pylint: disable=abstract-class-little-used,too-many-ancestors
|
# pylint: disable=abstract-class-little-used,too-many-ancestors
|
||||||
"""Challenge based on Key Authorization.
|
"""Challenge based on Key Authorization.
|
||||||
|
|
||||||
@@ -460,108 +451,8 @@ class TLSSNI01(KeyAuthorizationChallenge):
|
|||||||
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
|
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
|
@Challenge.register # pylint: disable=too-many-ancestors
|
||||||
class DNS(_TokenDVChallenge):
|
class DNS(_TokenChallenge):
|
||||||
"""ACME "dns" challenge."""
|
"""ACME "dns" challenge."""
|
||||||
typ = "dns"
|
typ = "dns"
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err
|
|||||||
|
|
||||||
from acme import errors
|
from acme import errors
|
||||||
from acme import jose
|
from acme import jose
|
||||||
from acme import other
|
|
||||||
from acme import test_util
|
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)
|
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):
|
class DNSTest(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|||||||
@@ -271,10 +271,8 @@ class AuthorizationTest(unittest.TestCase):
|
|||||||
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
||||||
chall=challenges.DNS(
|
chall=challenges.DNS(
|
||||||
token=b'DGyRejmCefe7v4NfDGDKfA')),
|
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 Authorization
|
||||||
from acme.messages import Identifier
|
from acme.messages import Identifier
|
||||||
@@ -300,8 +298,8 @@ class AuthorizationTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_resolved_combinations(self):
|
def test_resolved_combinations(self):
|
||||||
self.assertEqual(self.authz.resolved_combinations, (
|
self.assertEqual(self.authz.resolved_combinations, (
|
||||||
(self.challbs[0], self.challbs[2]),
|
(self.challbs[0],),
|
||||||
(self.challbs[1], self.challbs[2]),
|
(self.challbs[1],),
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
+12
-8
@@ -71,11 +71,12 @@ Plugin Auth Inst Notes
|
|||||||
=========== ==== ==== ===============================================================
|
=========== ==== ==== ===============================================================
|
||||||
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
|
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
|
||||||
Debian-based distributions with ``libaugeas0`` 1.0+.
|
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
|
webroot_ Y N Obtains a cert by writing to the webroot directory of an
|
||||||
already running webserver.
|
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
|
manual_ Y N Helps you obtain a cert by giving you instructions to perform
|
||||||
domain validation yourself.
|
domain validation yourself.
|
||||||
nginx_ Y Y Very experimental and not included in letsencrypt-auto_.
|
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
|
Plugin Auth Inst Notes
|
||||||
=========== ==== ==== ===============================================================
|
=========== ==== ==== ===============================================================
|
||||||
plesk_ Y Y Integration with the Plesk web hosting tool
|
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
|
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
|
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
|
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
|
Future plugins for IMAP servers, SMTP servers, IRC servers, etc, are likely to
|
||||||
be installers but not authenticators.
|
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
|
certificate for multiple domains, each domain will use the most recently
|
||||||
specified ``--webroot-path``. So, for instance,
|
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
|
would obtain a single certificate for all of those names, using the
|
||||||
``/var/www/example`` webroot directory for the first two, and
|
``/var/www/example`` webroot directory for the first two, and
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||||||
paths = self.aug.match(
|
paths = self.aug.match(
|
||||||
("/files%s//*[label()=~regexp('%s')]" %
|
("/files%s//*[label()=~regexp('%s')]" %
|
||||||
(vhost_path, parser.case_i("VirtualHost"))))
|
(vhost_path, parser.case_i("VirtualHost"))))
|
||||||
|
paths = [path for path in paths if os.path.basename(path) == "VirtualHost"]
|
||||||
for path in paths:
|
for path in paths:
|
||||||
new_vhost = self._create_vhost(path)
|
new_vhost = self._create_vhost(path)
|
||||||
realpath = os.path.realpath(new_vhost.filep)
|
realpath = os.path.realpath(new_vhost.filep)
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ def _vhost_menu(domain, vhosts):
|
|||||||
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
|
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
|
||||||
"We were unable to find a vhost with a ServerName "
|
"We were unable to find a vhost with a ServerName "
|
||||||
"or Address of {0}.{1}Which virtual host would you "
|
"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")
|
choices, help_label="More Info", ok_label="Select")
|
||||||
except errors.MissingCommandlineFlag as e:
|
except errors.MissingCommandlineFlag as e:
|
||||||
msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}"
|
msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}"
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
XQAAAAT//////////wApLArrUzOk5bRHUk0UvMS4xjyZkm3U3qhnKvMbEan7rVeK6yBlbwGeeWFn
|
|
||||||
Sw4XT1raGAMNq7cwyJvT7ql93Df7TpuRnxNSbPx7q52GojYyb5Oj1IQ2Y22Mvq41Q4K3kCZcVv+1
|
|
||||||
YVKW3OazUn+wCnaoGhDdMFmH0EKbEPSGibba6HJqUoFosaDE2hRZmjqYR/VwwPCtW820L0Qz9PZ7
|
|
||||||
DEAZ5VdMmj1+u+bYjDEcZD5+DyWKoLWci8tBXcPGiSvPDdZax/IWmR0GGUOd13gC7uX/HM2dHgbM
|
|
||||||
Izh7Y3PPNEzM8Fu2wdXLoMCaYrQcrPAdKhsnyMCDbjxCVbD9LkS17xCq4LUMkcz/fMu3/CRSMMZ7
|
|
||||||
gnn//jNQAA==
|
|
||||||
+14
-8
@@ -1,4 +1,4 @@
|
|||||||
"""Let's Encrypt command CLI argument processing."""
|
"""Let's Encrypt command line argument & config processing."""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import argparse
|
import argparse
|
||||||
import glob
|
import glob
|
||||||
@@ -292,7 +292,6 @@ def argparse_type(variable):
|
|||||||
return action.type
|
return action.type
|
||||||
return str
|
return str
|
||||||
|
|
||||||
|
|
||||||
def read_file(filename, mode="rb"):
|
def read_file(filename, mode="rb"):
|
||||||
"""Returns the given file's contents.
|
"""Returns the given file's contents.
|
||||||
|
|
||||||
@@ -355,14 +354,17 @@ class HelpfulArgumentParser(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args, plugins, detect_defaults=False):
|
def __init__(self, args, plugins, detect_defaults=False):
|
||||||
|
|
||||||
from letsencrypt import main
|
from letsencrypt import main
|
||||||
self.VERBS = main.VERBS
|
self.VERBS = {"auth": main.obtain_cert, "certonly": main.obtain_cert,
|
||||||
# List of topics for which additional help can be provided
|
"config_changes": main.config_changes, "run": main.run,
|
||||||
HELP_TOPICS = ["all", "security",
|
"install": main.install, "plugins": main.plugins_cmd,
|
||||||
"paths", "automation", "testing"] + list(six.iterkeys(self.VERBS))
|
"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]
|
self.help_topics = HELP_TOPICS + plugin_names + [None]
|
||||||
usage, short_usage = usage_strings(plugins)
|
usage, short_usage = usage_strings(plugins)
|
||||||
self.parser = configargparse.ArgParser(
|
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("revoke", description="Options for revocation of certs")
|
||||||
helpful.add_group("rollback", description="Options for reverting config changes")
|
helpful.add_group("rollback", description="Options for reverting config changes")
|
||||||
helpful.add_group("plugins", description="Plugin options")
|
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(
|
helpful.add(
|
||||||
None, "--user-agent", default=None,
|
None, "--user-agent", default=None,
|
||||||
help="Set a custom user agent string for the client. User agent strings allow "
|
help="Set a custom user agent string for the client. User agent strings allow "
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ def rollback(default_installer, checkpoints, config, plugins):
|
|||||||
installer.restart()
|
installer.restart()
|
||||||
|
|
||||||
|
|
||||||
def view_config_changes(config):
|
def view_config_changes(config, num=None):
|
||||||
"""View checkpoints and associated configuration changes.
|
"""View checkpoints and associated configuration changes.
|
||||||
|
|
||||||
.. note:: This assumes that the installation is using a Reverter object.
|
.. 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 = reverter.Reverter(config)
|
||||||
rev.recovery_routine()
|
rev.recovery_routine()
|
||||||
rev.view_config_changes()
|
rev.view_config_changes(num)
|
||||||
|
|
||||||
|
|
||||||
def _save_chain(chain_pem, chain_path):
|
def _save_chain(chain_pem, chain_path):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import re
|
import re
|
||||||
|
import six
|
||||||
import socket
|
import socket
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
@@ -310,10 +311,13 @@ def enforce_domain_sanity(domain):
|
|||||||
# Unicode
|
# Unicode
|
||||||
try:
|
try:
|
||||||
domain = domain.encode('ascii').lower()
|
domain = domain.encode('ascii').lower()
|
||||||
except UnicodeDecodeError:
|
except UnicodeError:
|
||||||
raise errors.ConfigurationError(
|
error_fmt = (u"Internationalized domain names "
|
||||||
"Internationalized domain names are not presently supported: {0}"
|
"are not presently supported: {0}")
|
||||||
.format(domain))
|
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
|
# Remove trailing dot
|
||||||
domain = domain[:-1] if domain.endswith('.') else domain
|
domain = domain[:-1] if domain.endswith('.') else domain
|
||||||
|
|||||||
+9
-16
@@ -2,10 +2,17 @@
|
|||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
import atexit
|
import atexit
|
||||||
import functools
|
import functools
|
||||||
|
import logging.handlers
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import OpenSSL
|
||||||
import zope.component
|
import zope.component
|
||||||
|
|
||||||
|
from acme import jose
|
||||||
|
|
||||||
import letsencrypt
|
import letsencrypt
|
||||||
|
|
||||||
from letsencrypt import account
|
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.display import util as display_util, ops as display_ops
|
||||||
from letsencrypt.plugins import disco as plugins_disco
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -156,7 +158,7 @@ def _handle_subset_cert_request(config, domains, cert):
|
|||||||
br=os.linesep)
|
br=os.linesep)
|
||||||
if config.expand or config.renew_by_default or zope.component.getUtility(
|
if config.expand or config.renew_by_default or zope.component.getUtility(
|
||||||
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
|
interfaces.IDisplay).yesno(question, "Expand", "Cancel",
|
||||||
cli_flag="--expand (or in some cases, --duplicate)"):
|
cli_flag="--expand"):
|
||||||
return "renew", cert
|
return "renew", cert
|
||||||
else:
|
else:
|
||||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||||
@@ -454,7 +456,7 @@ def config_changes(config, unused_plugins):
|
|||||||
View checkpoints and associated configuration changes.
|
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
|
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)
|
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__":
|
if __name__ == "__main__":
|
||||||
err_string = main()
|
err_string = main()
|
||||||
if err_string:
|
if err_string:
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ class Reverter(object):
|
|||||||
"Unable to load checkpoint during rollback")
|
"Unable to load checkpoint during rollback")
|
||||||
rollback -= 1
|
rollback -= 1
|
||||||
|
|
||||||
def view_config_changes(self, for_logging=False):
|
def view_config_changes(self, for_logging=False, num=None):
|
||||||
"""Displays all saved checkpoints.
|
"""Displays all saved checkpoints.
|
||||||
|
|
||||||
All checkpoints are printed by
|
All checkpoints are printed by
|
||||||
@@ -107,7 +107,8 @@ class Reverter(object):
|
|||||||
"""
|
"""
|
||||||
backups = os.listdir(self.config.backup_dir)
|
backups = os.listdir(self.config.backup_dir)
|
||||||
backups.sort(reverse=True)
|
backups.sort(reverse=True)
|
||||||
|
if num:
|
||||||
|
backups = backups[:num]
|
||||||
if not backups:
|
if not backups:
|
||||||
logger.info("The Let's Encrypt client has not saved any backups "
|
logger.info("The Let's Encrypt client has not saved any backups "
|
||||||
"of your configuration")
|
"of your configuration")
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
return ret, None, stderr, client
|
return ret, None, stderr, client
|
||||||
|
|
||||||
def test_no_flags(self):
|
def test_no_flags(self):
|
||||||
with MockedVerb("run") as mock_run:
|
with mock.patch('letsencrypt.main.run') as mock_run:
|
||||||
self._call([])
|
self._call([])
|
||||||
self.assertEqual(1, mock_run.call_count)
|
self.assertEqual(1, mock_run.call_count)
|
||||||
|
|
||||||
@@ -190,7 +190,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
chain = 'chain'
|
chain = 'chain'
|
||||||
fullchain = 'fullchain'
|
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',
|
self._call(['install', '--cert-path', cert, '--key-path', 'key',
|
||||||
'--chain-path', 'chain',
|
'--chain-path', 'chain',
|
||||||
'--fullchain-path', 'fullchain'])
|
'--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]
|
unused_config, auth, unused_installer = mock_init.call_args[0]
|
||||||
self.assertTrue(isinstance(auth, manual.Authenticator))
|
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._call(["auth", "--standalone"])
|
||||||
self.assertEqual(1, mock_certonly.call_count)
|
self.assertEqual(1, mock_certonly.call_count)
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
chain = 'chain'
|
chain = 'chain'
|
||||||
fullchain = 'fullchain'
|
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',
|
self._call(['certonly', '--cert-path', cert, '--key-path', 'key',
|
||||||
'--chain-path', 'chain',
|
'--chain-path', 'chain',
|
||||||
'--fullchain-path', 'fullchain'])
|
'--fullchain-path', 'fullchain'])
|
||||||
@@ -900,7 +900,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
|
|||||||
self.assertEqual(contents, test_contents)
|
self.assertEqual(contents, test_contents)
|
||||||
|
|
||||||
def test_agree_dev_preview_config(self):
|
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._call(['-c', test_util.vector_path('cli.ini')])
|
||||||
self.assertTrue(mocked_run.called)
|
self.assertTrue(mocked_run.called)
|
||||||
|
|
||||||
@@ -1010,34 +1010,5 @@ class DuplicativeCertsTest(storage_test.BaseRenewableCertTest):
|
|||||||
self.assertEqual(result, (None, None))
|
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__':
|
if __name__ == '__main__':
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import letsencrypt.errors as errors
|
|||||||
|
|
||||||
from letsencrypt.display import util as display_util
|
from letsencrypt.display import util as display_util
|
||||||
|
|
||||||
|
|
||||||
CHOICES = [("First", "Description1"), ("Second", "Description2")]
|
CHOICES = [("First", "Description1"), ("Second", "Description2")]
|
||||||
TAGS = ["tag1", "tag2", "tag3"]
|
TAGS = ["tag1", "tag2", "tag3"]
|
||||||
TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]
|
TAGS_CHOICES = [("1", "tag1"), ("2", "tag2"), ("3", "tag3")]
|
||||||
|
|||||||
@@ -323,5 +323,21 @@ class AddDeprecatedArgumentTest(unittest.TestCase):
|
|||||||
self.assertTrue("--old-option" not in stdout.getvalue())
|
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__":
|
if __name__ == "__main__":
|
||||||
unittest.main() # pragma: no cover
|
unittest.main() # pragma: no cover
|
||||||
|
|||||||
Reference in New Issue
Block a user