Merge remote-tracking branch 'letsencrypt/master'

This commit is contained in:
Nav
2015-11-03 23:18:55 +02:00
66 changed files with 1088 additions and 778 deletions
+7
View File
@@ -0,0 +1,7 @@
* text=auto eol=lf
# special files
*.bat text eol=crlf
*.jpeg binary
*.jpg binary
*.png binary
+3 -2
View File
@@ -38,8 +38,9 @@ load-plugins=linter_plugin
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name
# abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1)
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes
# abstract-class-not-used cannot be disabled locally (at least in
# pylint 1.4.1), same for abstract-class-little-used
[REPORTS]
-1
View File
@@ -39,7 +39,6 @@ addons:
mariadb: "10.0"
apt:
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
- lsb-release
- python
- python-dev
- python-virtualenv
Vendored
+3 -6
View File
@@ -4,14 +4,11 @@
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
VAGRANTFILE_API_VERSION = "2"
# Setup instructions from docs/using.rst
# Setup instructions from docs/contributing.rst
$ubuntu_setup_script = <<SETUP_SCRIPT
cd /vagrant
sudo ./bootstrap/ubuntu.sh
if [ ! -d "venv" ]; then
virtualenv --no-site-packages -p python2 venv
./venv/bin/pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx
fi
./bootstrap/install-deps.sh
./bootstrap/dev/venv.sh
SETUP_SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
+176 -168
View File
@@ -1,9 +1,11 @@
"""ACME Identifier Validation Challenges."""
import abc
import functools
import hashlib
import logging
import socket
from cryptography.hazmat.primitives import hashes
import OpenSSL
import requests
@@ -76,26 +78,21 @@ class UnrecognizedChallenge(Challenge):
return cls(jobj)
@Challenge.register
class SimpleHTTP(DVChallenge):
"""ACME "simpleHttp" challenge.
class _TokenDVChallenge(DVChallenge):
"""DV Challenge with token.
:ivar unicode token:
:ivar bytes token:
"""
typ = "simpleHttp"
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
URI_ROOT_PATH = ".well-known/acme-challenge"
"""URI root path for the server provisioned resource."""
# TODO: acme-spec doesn't specify token as base64-encoded value
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
# XXX: rename to ~token_good_for_url
@property
def good_token(self): # XXX: @token.decoder
"""Is `token` good?
@@ -109,124 +106,130 @@ class SimpleHTTP(DVChallenge):
# URI_ROOT_PATH!
return b'..' not in self.token and b'/' not in self.token
@property
def path(self):
"""Path (starting with '/') for provisioned resource."""
return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')
class KeyAuthorizationChallengeResponse(ChallengeResponse):
"""Response to Challenges based on Key Authorization.
@ChallengeResponse.register
class SimpleHTTPResponse(ChallengeResponse):
"""ACME "simpleHttp" challenge response.
:ivar bool tls:
:param unicode key_authorization:
"""
typ = "simpleHttp"
tls = jose.Field("tls", default=True, omitempty=True)
key_authorization = jose.Field("keyAuthorization")
thumbprint_hash_function = hashes.SHA256
URI_ROOT_PATH = SimpleHTTP.URI_ROOT_PATH
_URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}"
def verify(self, chall, account_public_key):
"""Verify the key authorization.
CONTENT_TYPE = "application/jose+json"
PORT = 80
TLS_PORT = 443
@property
def scheme(self):
"""URL scheme for the provisioned resource."""
return "https" if self.tls else "http"
@property
def port(self):
"""Port that the ACME client should be listening for validation."""
return self.TLS_PORT if self.tls else self.PORT
def uri(self, domain, chall):
"""Create an URI to the provisioned resource.
Forms an URI to the HTTPS server provisioned resource
(containing :attr:`~SimpleHTTP.token`).
:param unicode domain: Domain name being verified.
:param challenges.SimpleHTTP chall:
"""
return self._URI_TEMPLATE.format(
scheme=self.scheme, domain=domain, token=chall.encode("token"))
def gen_resource(self, chall):
"""Generate provisioned resource.
:param challenges.SimpleHTTP chall:
:rtype: SimpleHTTPProvisionedResource
"""
return SimpleHTTPProvisionedResource(token=chall.token, tls=self.tls)
def gen_validation(self, chall, account_key, alg=jose.RS256, **kwargs):
"""Generate validation.
:param challenges.SimpleHTTP chall:
:param .JWK account_key: Private account key.
:param .JWA alg:
:returns: `.SimpleHTTPProvisionedResource` signed in `.JWS`
:rtype: .JWS
"""
return jose.JWS.sign(
payload=self.gen_resource(chall).json_dumps(
sort_keys=True).encode('utf-8'),
key=account_key, alg=alg, **kwargs)
def check_validation(self, validation, chall, account_public_key):
"""Check validation.
:param .JWS validation:
:param challenges.SimpleHTTP chall:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:param KeyAuthorization chall: Challenge that corresponds to
this response.
:param JWK account_public_key:
:return: ``True`` iff verification of the key authorization was
successful.
:rtype: bool
"""
if not validation.verify(key=account_public_key):
parts = self.key_authorization.split('.') # pylint: disable=no-member
if len(parts) != 2:
logger.debug("Key authorization (%r) is not well formed",
self.key_authorization)
return False
try:
resource = SimpleHTTPProvisionedResource.json_loads(
validation.payload.decode('utf-8'))
except jose.DeserializationError as error:
logger.debug(error)
if parts[0] != chall.encode("token"):
logger.debug("Mismatching token in key authorization: "
"%r instead of %r", parts[0], chall.encode("token"))
return False
return resource.token == chall.token and resource.tls == self.tls
thumbprint = jose.b64encode(account_public_key.thumbprint(
hash_function=self.thumbprint_hash_function)).decode()
if parts[1] != thumbprint:
logger.debug("Mismatching thumbprint in key authorization: "
"%r instead of %r", parts[0], thumbprint)
return False
return True
class KeyAuthorizationChallenge(_TokenDVChallenge):
# pylint: disable=abstract-class-little-used,too-many-ancestors
"""Challenge based on Key Authorization.
:param response_cls: Subclass of `KeyAuthorizationChallengeResponse`
that will be used to generate `response`.
"""
__metaclass__ = abc.ABCMeta
response_cls = NotImplemented
thumbprint_hash_function = (
KeyAuthorizationChallengeResponse.thumbprint_hash_function)
def key_authorization(self, account_key):
"""Generate Key Authorization.
:param JWK account_key:
:rtype unicode:
"""
return self.encode("token") + "." + jose.b64encode(
account_key.thumbprint(
hash_function=self.thumbprint_hash_function)).decode()
def response(self, account_key):
"""Generate response to the challenge.
:param JWK account_key:
:returns: Response (initialized `response_cls`) to the challenge.
:rtype: KeyAuthorizationChallengeResponse
"""
return self.response_cls(
key_authorization=self.key_authorization(account_key))
@abc.abstractmethod
def validation(self, account_key):
"""Generate validation for the challenge.
Subclasses must implement this method, but they are likely to
return completely different data structures, depending on what's
necessary to complete the challenge. Interepretation of that
return value must be known to the caller.
:param JWK account_key:
:returns: Challenge-specific validation.
"""
raise NotImplementedError() # pragma: no cover
def response_and_validation(self, account_key):
"""Generate response and validation.
Convenience function that return results of `response` and
`validation`.
:param JWK account_key:
:rtype: tuple
"""
return (self.response(account_key), self.validation(account_key))
@ChallengeResponse.register
class HTTP01Response(KeyAuthorizationChallengeResponse):
"""ACME http-01 challenge response."""
typ = "http-01"
PORT = 80
def simple_verify(self, chall, domain, account_public_key, port=None):
"""Simple verify.
According to the ACME specification, "the ACME server MUST
ignore the certificate provided by the HTTPS server", so
``requests.get`` is called with ``verify=False``.
:param challenges.SimpleHTTP chall: Corresponding challenge.
:param unicode domain: Domain name being verified.
:param account_public_key: Public key for the key pair
being authorized. If ``None`` key verification is not
performed!
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:param JWK account_public_key:
:param int port: Port used in the validation.
:returns: ``True`` iff validation is successful, ``False``
@@ -234,48 +237,89 @@ class SimpleHTTPResponse(ChallengeResponse):
:rtype: bool
"""
if not self.verify(chall, account_public_key):
logger.debug("Verification of key authorization in response failed")
return False
# TODO: ACME specification defines URI template that doesn't
# allow to use a custom port... Make sure port is not in the
# request URI, if it's standard.
if port is not None and port != self.port:
logger.warn(
if port is not None and port != self.PORT:
logger.warning(
"Using non-standard port for SimpleHTTP verification: %s", port)
domain += ":{0}".format(port)
uri = self.uri(domain, chall)
uri = chall.uri(domain)
logger.debug("Verifying %s at %s...", chall.typ, uri)
try:
http_response = requests.get(uri, verify=False)
http_response = requests.get(uri)
except requests.exceptions.RequestException as error:
logger.error("Unable to reach %s: %s", uri, error)
return False
logger.debug("Received %s: %s. Headers: %s", http_response,
http_response.text, http_response.headers)
if self.CONTENT_TYPE != http_response.headers.get(
"Content-Type", self.CONTENT_TYPE):
found_ct = http_response.headers.get(
"Content-Type", chall.CONTENT_TYPE)
if found_ct != chall.CONTENT_TYPE:
logger.debug("Wrong Content-Type: found %r, expected %r",
found_ct, chall.CONTENT_TYPE)
return False
try:
validation = jose.JWS.json_loads(http_response.text)
except jose.DeserializationError as error:
logger.debug(error)
if self.key_authorization != http_response.text:
logger.debug("Key authorization from response (%r) doesn't match "
"HTTP response (%r)", self.key_authorization,
http_response.text)
return False
return self.check_validation(validation, chall, account_public_key)
return True
class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields):
"""SimpleHTTP provisioned resource."""
typ = fields.Fixed("type", SimpleHTTP.typ)
token = SimpleHTTP._fields["token"]
# If the "tls" field is not included in the response, then
# validation object MUST have its "tls" field set to "true".
tls = jose.Field("tls", omitempty=False)
@Challenge.register # pylint: disable=too-many-ancestors
class HTTP01(KeyAuthorizationChallenge):
"""ACME http-01 challenge."""
response_cls = HTTP01Response
typ = response_cls.typ
CONTENT_TYPE = "text/plain"
"""Only valid value for Content-Type if the header is included."""
URI_ROOT_PATH = ".well-known/acme-challenge"
"""URI root path for the server provisioned resource."""
@property
def path(self):
"""Path (starting with '/') for provisioned resource.
:rtype: string
"""
return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')
def uri(self, domain):
"""Create an URI to the provisioned resource.
Forms an URI to the HTTPS server provisioned resource
(containing :attr:`~SimpleHTTP.token`).
:param unicode domain: Domain name being verified.
:rtype: string
"""
return "http://" + domain + self.path
def validation(self, account_key):
"""Generate validation.
:param JWK account_key:
:rtype: unicode
"""
return self.key_authorization(account_key)
@Challenge.register
class DVSNI(DVChallenge):
@Challenge.register # pylint: disable=too-many-ancestors
class DVSNI(_TokenDVChallenge):
"""ACME "dvsni" challenge.
:ivar bytes token: Random data, **not** base64-encoded.
@@ -286,13 +330,6 @@ class DVSNI(DVChallenge):
PORT = 443
"""Port to perform DVSNI challenge."""
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
def gen_response(self, account_key, alg=jose.RS256, **kwargs):
"""Generate response.
@@ -406,17 +443,12 @@ class DVSNIResponse(ChallengeResponse):
:param .challenges.DVSNI chall: Corresponding challenge.
:param str domain: Domain name being validated.
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:param JWK account_public_key:
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
provided (``None``) certificate will be retrieved using
`probe_cert`.
:returns: ``True`` iff client's control of the domain has been
verified, ``False`` otherwise.
:rtype: bool
@@ -491,7 +523,7 @@ class ProofOfPossession(ContinuityChallenge):
class Hints(jose.JSONObjectWithFields):
"""Hints for "proofOfPossession" challenge.
:ivar jwk: JSON Web Key (:class:`acme.jose.JWK`)
:ivar JWK jwk: JSON Web Key
:ivar tuple cert_fingerprints: `tuple` of `unicode`
:ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509`
certificates.
@@ -548,25 +580,14 @@ class ProofOfPossessionResponse(ChallengeResponse):
return self.signature.verify(self.nonce)
@Challenge.register
class DNS(DVChallenge):
"""ACME "dns" challenge.
:ivar unicode token:
"""
@Challenge.register # pylint: disable=too-many-ancestors
class DNS(_TokenDVChallenge):
"""ACME "dns" challenge."""
typ = "dns"
LABEL = "_acme-challenge"
"""Label clients prepend to the domain name being validated."""
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
"""Minimum size of the :attr:`token` in bytes."""
token = jose.Field(
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
def gen_validation(self, account_key, alg=jose.RS256, **kwargs):
"""Generate validation.
@@ -585,14 +606,7 @@ class DNS(DVChallenge):
"""Check validation.
:param JWS validation:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:param JWK account_public_key:
:rtype: bool
"""
@@ -641,13 +655,7 @@ class DNSResponse(ChallengeResponse):
"""Check validation.
:param challenges.DNS chall:
:type account_public_key:
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
or
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
wrapped in `.ComparableKey`
:param JWK account_public_key:
:rtype: bool
+126 -153
View File
@@ -14,7 +14,7 @@ from acme import test_util
CERT = test_util.load_cert('cert.pem')
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
class ChallengeTest(unittest.TestCase):
@@ -43,171 +43,149 @@ class UnrecognizedChallengeTest(unittest.TestCase):
self.chall, UnrecognizedChallenge.from_json(self.jobj))
class SimpleHTTPTest(unittest.TestCase):
class KeyAuthorizationChallengeResponseTest(unittest.TestCase):
def setUp(self):
from acme.challenges import SimpleHTTP
self.msg = SimpleHTTP(
token=jose.decode_b64jose(
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
def _encode(name):
assert name == "token"
return "foo"
self.chall = mock.Mock()
self.chall.encode.side_effect = _encode
def test_verify_ok(self):
from acme.challenges import KeyAuthorizationChallengeResponse
response = KeyAuthorizationChallengeResponse(
key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
self.assertTrue(response.verify(self.chall, KEY.public_key()))
def test_verify_wrong_token(self):
from acme.challenges import KeyAuthorizationChallengeResponse
response = KeyAuthorizationChallengeResponse(
key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
self.assertFalse(response.verify(self.chall, KEY.public_key()))
def test_verify_wrong_thumbprint(self):
from acme.challenges import KeyAuthorizationChallengeResponse
response = KeyAuthorizationChallengeResponse(
key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv')
self.assertFalse(response.verify(self.chall, KEY.public_key()))
def test_verify_wrong_form(self):
from acme.challenges import KeyAuthorizationChallengeResponse
response = KeyAuthorizationChallengeResponse(
key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
self.assertFalse(response.verify(self.chall, KEY.public_key()))
class HTTP01ResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import HTTP01Response
self.msg = HTTP01Response(key_authorization=u'foo')
self.jmsg = {
'type': 'simpleHttp',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
'resource': 'challenge',
'type': 'http-01',
'keyAuthorization': u'foo',
}
from acme.challenges import HTTP01
self.chall = HTTP01(token=(b'x' * 16))
self.response = self.chall.response(KEY)
self.good_headers = {'Content-Type': HTTP01.CONTENT_TYPE}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import SimpleHTTP
self.assertEqual(self.msg, SimpleHTTP.from_json(self.jmsg))
from acme.challenges import HTTP01Response
self.assertEqual(
self.msg, HTTP01Response.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import SimpleHTTP
hash(SimpleHTTP.from_json(self.jmsg))
from acme.challenges import HTTP01Response
hash(HTTP01Response.from_json(self.jmsg))
def test_good_token(self):
self.assertTrue(self.msg.good_token)
self.assertFalse(
self.msg.update(token=b'..').good_token)
class SimpleHTTPResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
from acme.challenges import SimpleHTTPResponse
self.msg_http = SimpleHTTPResponse(tls=False)
self.msg_https = SimpleHTTPResponse(tls=True)
self.jmsg_http = {
'resource': 'challenge',
'type': 'simpleHttp',
'tls': False,
}
self.jmsg_https = {
'resource': 'challenge',
'type': 'simpleHttp',
'tls': True,
}
from acme.challenges import SimpleHTTP
self.chall = SimpleHTTP(token=(b"x" * 16))
self.resp_http = SimpleHTTPResponse(tls=False)
self.resp_https = SimpleHTTPResponse(tls=True)
self.good_headers = {'Content-Type': SimpleHTTPResponse.CONTENT_TYPE}
def test_to_partial_json(self):
self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json())
self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json())
def test_from_json(self):
from acme.challenges import SimpleHTTPResponse
self.assertEqual(
self.msg_http, SimpleHTTPResponse.from_json(self.jmsg_http))
self.assertEqual(
self.msg_https, SimpleHTTPResponse.from_json(self.jmsg_https))
def test_from_json_hashable(self):
from acme.challenges import SimpleHTTPResponse
hash(SimpleHTTPResponse.from_json(self.jmsg_http))
hash(SimpleHTTPResponse.from_json(self.jmsg_https))
def test_scheme(self):
self.assertEqual('http', self.msg_http.scheme)
self.assertEqual('https', self.msg_https.scheme)
def test_port(self):
self.assertEqual(80, self.msg_http.port)
self.assertEqual(443, self.msg_https.port)
def test_uri(self):
self.assertEqual(
'http://example.com/.well-known/acme-challenge/'
'eHh4eHh4eHh4eHh4eHh4eA', self.msg_http.uri(
'example.com', self.chall))
self.assertEqual(
'https://example.com/.well-known/acme-challenge/'
'eHh4eHh4eHh4eHh4eHh4eA', self.msg_https.uri(
'example.com', self.chall))
def test_gen_check_validation(self):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
self.assertTrue(self.resp_http.check_validation(
validation=self.resp_http.gen_validation(self.chall, account_key),
chall=self.chall, account_public_key=account_key.public_key()))
def test_gen_check_validation_wrong_key(self):
key1 = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))
self.assertFalse(self.resp_http.check_validation(
validation=self.resp_http.gen_validation(self.chall, key1),
chall=self.chall, account_public_key=key2.public_key()))
def test_check_validation_wrong_payload(self):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
validations = tuple(
jose.JWS.sign(payload=payload, alg=jose.RS256, key=account_key)
for payload in (b'', b'{}', self.chall.json_dumps().encode('utf-8'),
self.resp_http.json_dumps().encode('utf-8'))
)
for validation in validations:
self.assertFalse(self.resp_http.check_validation(
validation=validation, chall=self.chall,
account_public_key=account_key.public_key()))
def test_check_validation_wrong_fields(self):
resource = self.resp_http.gen_resource(self.chall)
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
validations = tuple(
jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'),
alg=jose.RS256, key=account_key)
for bad_resource in (resource.update(tls=True),
resource.update(token=(b'x' * 20)))
)
for validation in validations:
self.assertFalse(self.resp_http.check_validation(
validation=validation, chall=self.chall,
account_public_key=account_key.public_key()))
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.requests.get")
def test_simple_verify_good_validation(self, mock_get):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
for resp in self.resp_http, self.resp_https:
mock_get.reset_mock()
validation = resp.gen_validation(self.chall, account_key)
mock_get.return_value = mock.MagicMock(
text=validation.json_dumps(), headers=self.good_headers)
self.assertTrue(resp.simple_verify(self.chall, "local", None))
mock_get.assert_called_once_with(resp.uri(
"local", self.chall), verify=False)
validation = self.chall.validation(KEY)
mock_get.return_value = mock.MagicMock(
text=validation, headers=self.good_headers)
self.assertTrue(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
mock_get.assert_called_once_with(self.chall.uri("local"))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_validation(self, mock_get):
mock_get.return_value = mock.MagicMock(
text="!", headers=self.good_headers)
self.assertFalse(self.resp_http.simple_verify(
self.chall, "local", None))
self.assertFalse(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_content_type(self, mock_get):
mock_get().text = self.chall.token
self.assertFalse(self.resp_http.simple_verify(
self.chall, "local", None))
self.assertFalse(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_connection_error(self, mock_get):
mock_get.side_effect = requests.exceptions.RequestException
self.assertFalse(self.resp_http.simple_verify(
self.chall, "local", None))
self.assertFalse(self.response.simple_verify(
self.chall, "local", KEY.public_key()))
@mock.patch("acme.challenges.requests.get")
def test_simple_verify_port(self, mock_get):
self.resp_http.simple_verify(
self.chall, domain="local", account_public_key=None, port=4430)
self.assertEqual("local:4430", urllib_parse.urlparse(
self.response.simple_verify(
self.chall, domain="local",
account_public_key=KEY.public_key(), port=8080)
self.assertEqual("local:8080", urllib_parse.urlparse(
mock_get.mock_calls[0][1][0]).netloc)
class HTTP01Test(unittest.TestCase):
def setUp(self):
from acme.challenges import HTTP01
self.msg = HTTP01(
token=jose.decode_b64jose(
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
self.jmsg = {
'type': 'http-01',
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
}
def test_path(self):
self.assertEqual(self.msg.path, '/.well-known/acme-challenge/'
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')
def test_uri(self):
self.assertEqual(
'http://example.com/.well-known/acme-challenge/'
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
self.msg.uri('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 HTTP01
self.assertEqual(self.msg, HTTP01.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import HTTP01
hash(HTTP01.from_json(self.jmsg))
def test_good_token(self):
self.assertTrue(self.msg.good_token)
self.assertFalse(
self.msg.update(token=b'..').good_token)
class DVSNITest(unittest.TestCase):
def setUp(self):
@@ -237,18 +215,15 @@ class DVSNITest(unittest.TestCase):
jose.DeserializationError, DVSNI.from_json, self.jmsg)
def test_gen_response(self):
key = jose.JWKRSA(key=KEY)
from acme.challenges import DVSNI
self.assertEqual(self.msg, DVSNI.json_loads(
self.msg.gen_response(key).validation.payload.decode()))
self.msg.gen_response(KEY).validation.payload.decode()))
class DVSNIResponseTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
def setUp(self):
self.key = jose.JWKRSA(key=KEY)
from acme.challenges import DVSNI
self.chall = DVSNI(
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
@@ -256,7 +231,7 @@ class DVSNIResponseTest(unittest.TestCase):
from acme.challenges import DVSNIResponse
self.validation = jose.JWS.sign(
payload=self.chall.json_dumps(sort_keys=True).encode(),
key=self.key, alg=jose.RS256)
key=KEY, alg=jose.RS256)
self.msg = DVSNIResponse(validation=self.validation)
self.jmsg_to = {
'resource': 'challenge',
@@ -340,30 +315,30 @@ class DVSNIResponseTest(unittest.TestCase):
def test_simple_verify_wrong_payload(self):
for payload in b'', b'{}':
msg = self.msg.update(validation=jose.JWS.sign(
payload=payload, key=self.key, alg=jose.RS256))
payload=payload, key=KEY, alg=jose.RS256))
self.assertFalse(msg.simple_verify(
self.chall, self.domain, self.key.public_key()))
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=self.key, alg=jose.RS256))
key=KEY, alg=jose.RS256))
self.assertFalse(msg.simple_verify(
self.chall, self.domain, self.key.public_key()))
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, self.key.public_key(),
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert)
def test_simple_verify_false_on_probe_error(self):
chall = mock.Mock()
chall.probe_cert.side_effect = errors.Error
@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, self.key.public_key()))
self.chall, self.domain, KEY.public_key()))
class RecoveryContactTest(unittest.TestCase):
@@ -442,7 +417,7 @@ class RecoveryContactResponseTest(unittest.TestCase):
class ProofOfPossessionHintsTest(unittest.TestCase):
def setUp(self):
jwk = jose.JWKRSA(key=KEY.public_key())
jwk = KEY.public_key()
issuers = (
'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA',
'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure',
@@ -511,7 +486,7 @@ class ProofOfPossessionTest(unittest.TestCase):
def setUp(self):
from acme.challenges import ProofOfPossession
hints = ProofOfPossession.Hints(
jwk=jose.JWKRSA(key=KEY.public_key()), cert_fingerprints=(),
jwk=KEY.public_key(), cert_fingerprints=(),
certs=(), serial_numbers=(), subject_key_identifiers=(),
issuers=(), authorized_for=())
self.msg = ProofOfPossession(
@@ -551,7 +526,7 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
# nonce and challenge nonce are the same, don't make the same
# mistake here...
signature = other.Signature(
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.public_key()),
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'
@@ -659,14 +634,12 @@ class DNSTest(unittest.TestCase):
class DNSResponseTest(unittest.TestCase):
def setUp(self):
self.key = jose.JWKRSA(key=KEY)
from acme.challenges import DNS
self.chall = DNS(token=jose.b64decode(
b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"))
self.validation = jose.JWS.sign(
payload=self.chall.json_dumps(sort_keys=True).encode(),
key=self.key, alg=jose.RS256)
key=KEY, alg=jose.RS256)
from acme.challenges import DNSResponse
self.msg = DNSResponse(validation=self.validation)
@@ -694,7 +667,7 @@ class DNSResponseTest(unittest.TestCase):
def test_check_validation(self):
self.assertTrue(
self.msg.check_validation(self.chall, self.key.public_key()))
self.msg.check_validation(self.chall, KEY.public_key()))
if __name__ == '__main__':
+2
View File
@@ -47,6 +47,8 @@ class JWK(json_util.TypedJSONObjectWithFields):
https://tools.ietf.org/html/rfc7638
:returns bytes:
"""
digest = hashes.Hash(hash_function(), backend=default_backend())
digest.update(json.dumps(
+3 -5
View File
@@ -104,7 +104,7 @@ class Header(json_util.JSONObjectWithFields):
.. todo:: Supports only "jwk" header parameter lookup.
:returns: (Public) key found in the header.
:rtype: :class:`acme.jose.jwk.JWK`
:rtype: .JWK
:raises acme.jose.errors.Error: if key could not be found
@@ -194,8 +194,7 @@ class Signature(json_util.JSONObjectWithFields):
def verify(self, payload, key=None):
"""Verify.
:param key: Key used for verification.
:type key: :class:`acme.jose.jwk.JWK`
:param JWK key: Key used for verification.
"""
key = self.combined.find_key() if key is None else key
@@ -208,8 +207,7 @@ class Signature(json_util.JSONObjectWithFields):
protect=frozenset(), **kwargs):
"""Sign.
:param key: Key for signature.
:type key: :class:`acme.jose.jwk.JWK`
:param JWK key: Key for signature.
"""
assert isinstance(key, alg.kty)
+1 -1
View File
@@ -278,7 +278,7 @@ class AuthorizationTest(unittest.TestCase):
self.challbs = (
ChallengeBody(
uri='http://challb1', status=STATUS_VALID,
chall=challenges.SimpleHTTP(token=b'IlirfxKKXAsHtmzK29Pj8A')),
chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')),
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS(
token=b'DGyRejmCefe7v4NfDGDKfA')),
+13 -41
View File
@@ -4,7 +4,6 @@ import collections
import functools
import logging
import os
import socket
import sys
import six
@@ -50,62 +49,35 @@ class ACMEServerMixin: # pylint: disable=old-style-class
server_version = "ACME client standalone challenge solver"
allow_reuse_address = True
def __init__(self):
self._stopped = False
def serve_forever2(self):
"""Serve forever, until other thread calls `shutdown2`."""
logger.debug("Starting server at %s:%d...",
*self.socket.getsockname()[:2])
while not self._stopped:
self.handle_request()
def shutdown2(self):
"""Shutdown server loop from `serve_forever2`."""
self._stopped = True
# dummy request to terminate last server_forever2.handle_request()
sock = socket.socket()
try:
sock.connect(self.socket.getsockname())
except socket.error:
pass # thread is probably already finished
finally:
sock.close()
self.server_close()
class DVSNIServer(TLSServer, ACMEServerMixin):
"""DVSNI Server."""
def __init__(self, server_address, certs):
ACMEServerMixin.__init__(self)
TLSServer.__init__(
self, server_address, socketserver.BaseRequestHandler, certs=certs)
class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin):
"""SimpleHTTP Server."""
class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin):
"""HTTP01 Server."""
def __init__(self, server_address, resources):
ACMEServerMixin.__init__(self)
BaseHTTPServer.HTTPServer.__init__(
self, server_address, SimpleHTTPRequestHandler.partial_init(
self, server_address, HTTP01RequestHandler.partial_init(
simple_http_resources=resources))
class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""SimpleHTTP challenge handler.
class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
"""HTTP01 challenge handler.
Adheres to the stdlib's `socketserver.BaseRequestHandler` interface.
:ivar set simple_http_resources: A set of `SimpleHTTPResource`
:ivar set simple_http_resources: A set of `HTTP01Resource`
objects. TODO: better name?
"""
SimpleHTTPResource = collections.namedtuple(
"SimpleHTTPResource", "chall response validation")
HTTP01Resource = collections.namedtuple(
"HTTP01Resource", "chall response validation")
def __init__(self, *args, **kwargs):
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
@@ -114,7 +86,7 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self): # pylint: disable=invalid-name,missing-docstring
if self.path == "/":
self.handle_index()
elif self.path.startswith("/" + challenges.SimpleHTTP.URI_ROOT_PATH):
elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH):
self.handle_simple_http_resource()
else:
self.handle_404()
@@ -134,15 +106,15 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
self.wfile.write(b"404")
def handle_simple_http_resource(self):
"""Handle SimpleHTTP provisioned resources."""
"""Handle HTTP01 provisioned resources."""
for resource in self.simple_http_resources:
if resource.chall.path == self.path:
logger.debug("Serving SimpleHTTP with token %r",
logger.debug("Serving HTTP01 with token %r",
resource.chall.encode("token"))
self.send_response(http_client.OK)
self.send_header("Content-type", resource.response.CONTENT_TYPE)
self.send_header("Content-type", resource.chall.CONTENT_TYPE)
self.end_headers()
self.wfile.write(resource.validation.json_dumps().encode())
self.wfile.write(resource.validation.encode())
return
else: # pylint: disable=useless-else-on-loop
logger.debug("No resources to serve")
+20 -74
View File
@@ -1,7 +1,6 @@
"""Tests for acme.standalone."""
import os
import shutil
import socket
import threading
import tempfile
import time
@@ -29,54 +28,6 @@ class TLSServerTest(unittest.TestCase):
server.server_close() # pylint: disable=no-member
class ACMEServerMixinTest(unittest.TestCase):
"""Tests for acme.standalone.ACMEServerMixin."""
def setUp(self):
from acme.standalone import ACMEServerMixin
class _MockHandler(socketserver.BaseRequestHandler):
# pylint: disable=missing-docstring,no-member,no-init
def handle(self):
self.request.sendall(b"DONE")
class _MockServer(socketserver.TCPServer, ACMEServerMixin):
def __init__(self, *args, **kwargs):
socketserver.TCPServer.__init__(self, *args, **kwargs)
ACMEServerMixin.__init__(self)
self.server = _MockServer(("", 0), _MockHandler)
def _busy_wait(self): # pragma: no cover
# This function is used to avoid race conditions in tests, but
# not all of the functionality is always used, hence "no
# cover"
while True:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# pylint: disable=no-member
sock.connect(self.server.socket.getsockname())
except socket.error:
pass
else:
sock.recv(4) # wait until handle_request is actually called
break
finally:
sock.close()
time.sleep(1)
def test_serve_shutdown(self):
thread = threading.Thread(target=self.server.serve_forever2)
thread.start()
self._busy_wait()
self.server.shutdown2()
def test_shutdown2_not_running(self):
self.server.shutdown2()
self.server.shutdown2()
class DVSNIServerTest(unittest.TestCase):
"""Test for acme.standalone.DVSNIServer."""
@@ -89,42 +40,38 @@ class DVSNIServerTest(unittest.TestCase):
from acme.standalone import DVSNIServer
self.server = DVSNIServer(("", 0), certs=self.certs)
# pylint: disable=no-member
self.thread = threading.Thread(target=self.server.handle_request)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
def tearDown(self):
self.server.shutdown2()
self.server.shutdown() # pylint: disable=no-member
self.thread.join()
def test_init(self):
# pylint: disable=protected-access
self.assertFalse(self.server._stopped)
def test_dvsni(self):
def test_it(self):
host, port = self.server.socket.getsockname()[:2]
cert = crypto_util.probe_sni(b'localhost', host=host, port=port)
cert = crypto_util.probe_sni(b'localhost', host=host, port=port, timeout=1)
self.assertEqual(jose.ComparableX509(cert),
jose.ComparableX509(self.certs[b'localhost'][1]))
class SimpleHTTPServerTest(unittest.TestCase):
"""Tests for acme.standalone.SimpleHTTPServer."""
class HTTP01ServerTest(unittest.TestCase):
"""Tests for acme.standalone.HTTP01Server."""
def setUp(self):
self.account_key = jose.JWK.load(
test_util.load_vector('rsa1024_key.pem'))
self.resources = set()
from acme.standalone import SimpleHTTPServer
self.server = SimpleHTTPServer(('', 0), resources=self.resources)
from acme.standalone import HTTP01Server
self.server = HTTP01Server(('', 0), resources=self.resources)
# pylint: disable=no-member
self.port = self.server.socket.getsockname()[1]
self.thread = threading.Thread(target=self.server.handle_request)
self.thread = threading.Thread(target=self.server.serve_forever)
self.thread.start()
def tearDown(self):
self.server.shutdown2()
self.server.shutdown() # pylint: disable=no-member
self.thread.join()
def test_index(self):
@@ -139,25 +86,24 @@ class SimpleHTTPServerTest(unittest.TestCase):
'http://localhost:{0}/foo'.format(self.port), verify=False)
self.assertEqual(response.status_code, http_client.NOT_FOUND)
def _test_simple_http(self, add):
chall = challenges.SimpleHTTP(token=(b'x' * 16))
response = challenges.SimpleHTTPResponse(tls=False)
def _test_http01(self, add):
chall = challenges.HTTP01(token=(b'x' * 16))
response, validation = chall.response_and_validation(self.account_key)
from acme.standalone import SimpleHTTPRequestHandler
resource = SimpleHTTPRequestHandler.SimpleHTTPResource(
chall=chall, response=response, validation=response.gen_validation(
chall, self.account_key))
from acme.standalone import HTTP01RequestHandler
resource = HTTP01RequestHandler.HTTP01Resource(
chall=chall, response=response, validation=validation)
if add:
self.resources.add(resource)
return resource.response.simple_verify(
resource.chall, 'localhost', self.account_key.public_key(),
port=self.port)
def test_simple_http_found(self):
self.assertTrue(self._test_simple_http(add=True))
def test_http01_found(self):
self.assertTrue(self._test_http01(add=True))
def test_simple_http_not_found(self):
self.assertFalse(self._test_simple_http(add=False))
def test_http01_not_found(self):
self.assertFalse(self._test_http01(add=False))
class TestSimpleDVSNIServer(unittest.TestCase):
+3 -3
View File
@@ -4,12 +4,12 @@ to use appropriate extension for vector filenames: .pem for PEM and
The following command has been used to generate test keys:
for x in 256 512 1024; do openssl genrsa -out rsa${k}_key.pem $k; done
for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
and for the CSR:
openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
and for the certificate:
openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+27
View File
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA8HwZMHeImB/iM8/n8CTCR4KeYQB2gLGO3v8xLms+PWH3Zbxc
dVtEn25Y34scIh+iOuEXBcSBalBddLHKBGVN3nCfmpupoLm52xgRG44q9OWODpg4
FSi4afqVw2agMx0RHi0v3GVcdpqB83UW42kK1ESZHUuq7mxLg8u3IMYZFm6Amsf+
YQjBbDNn8NczJOFhsExP2EdM5ykgM1Om8aqTqqPMgPub68/r4Sym+BjLnvRq5Qtz
h/jCfOBIIpAwg3lj7l8OyE3kkD3ALtuiuminNUqLHEkUaLq/Xiv8V8mvnrhG7h3Q
+L1Xc707P0dz5YM5XxTMhmUE1cae/lQ0KbNrpwIDAQABAoIBAAiDXCDrGlrIRimv
YnaN1pLRfOnSKl/D6VrbjdIm2b0yip9/W4aMBJHgRiUjt4s9s3CCJ1585lftIGHR
KWWecHM/aWb/u7GE4Z9v6qsfDUY+GhlKKjIVjvGxfTu9lk446TI4R0l2DR/luFP2
ASlrvoZlJ0ZyN0rZapLv0zvFx32Tukd+3rcMmXfHl7aRGMZG1YTKNmBJ4d9iJ6cP
HG3fgSzLQMPLNO/20MzbXdREG5FNQtwaMuFnIcVbtMCvc/71lQQEfANMLCUweEed
YWGOjgDeh+731nJsopel+2TSTgnf5VhcFrgChZZdqeKvP+HbXjTE2VkWo7BrzoM7
xICYBwECgYEA/ZF/JOjZfwIMUdikv8vldJzRMdFryl4NJWnh4NeThNOEpUcpxnyK
wyMnnQaGJa51u9EEnzl0sZ2h2ODjD6KFpz6fkWaVRq5SWalVPAoKZGaoPZV3IUOI
8Tm0xkXho+A/FUUEcxCLME+3V9EdPfHaVRJOrbfDyxvNhsj4w9F0aAkCgYEA8sp7
XTrolOknJGv4Qt1w6gcm5+cMtLaRfi8ZHPHujl2x9eWE8/s2818az7jc0Xr/G4HQ
NeU+3Es4BblEckSHmhUZhx26cZgkLSIIDofEtaEc6u8CyWfxsWvn3l4T3kMdeSLC
9UoLk59AH2tkMIh8vzV8LSisLJa341lMdgryQi8CgYAlJKr7PSCe+i3Tz2hSsAts
iYwbQBIKErzaPihYRzvUuSc1DreP26535y5mUg5UdrnISVXj/Qaa/fw3SLn6EFSD
qyi0o9I6CE8H00YpBU+AZYk/fCV3Oe1VaJ6SbKog1zhmZTXBpSq+aO7ybi9aY5MX
4xajW8fSeMAifk3yYTwsAQKBgErcEcOCOVpItU/uloKPYpRWFjHktK83p46fmP+q
vOJak1d9KExOBfhuN4caucNBSE1D7l3fzE0CSEjDgg41gRYKMW/Ow8DopybfWlqY
lBdokNEDVvmgug35dmnC2h9q1DiYdkJJTV57+Lp3U1H/k28lX59Q7h1lb1eDHic7
YszzAoGBAOx05dhOiYbzAJSTQu3oBHFn4mTYIqCcDO6cQrEJwPKAq7mAhT0yOk9N
CrqRV/1aes665829cyTwcAZl6nqbzHv5XjX5+g6vmooCb4oCkq49rumHjoQdrX8D
RR5b+Spkc1jo4rctCcExzSkgo+K5N3oBVYznecje7O7Z0/qiJE/8
-----END RSA PRIVATE KEY-----
+6
View File
@@ -17,6 +17,12 @@ Contents:
:members:
Example client:
.. include:: ../examples/example_client.py
:code: python
Indices and tables
==================
@@ -15,7 +15,7 @@ from acme import jose
logging.basicConfig(level=logging.DEBUG)
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
DIRECTORY_URL = 'https://acme-staging.api.letsencrypt.org/directory'
BITS = 2048 # minimum for Boulder
DOMAIN = 'example1.com' # example.com is ignored by Boulder
@@ -24,7 +24,7 @@ key = jose.JWKRSA(key=rsa.generate_private_key(
public_exponent=65537,
key_size=BITS,
backend=default_backend()))
acme = client.Client(NEW_REG_URL, key)
acme = client.Client(DIRECTORY_URL, key)
regr = acme.register()
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
+1
View File
@@ -58,6 +58,7 @@ setup(
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
+20 -22
View File
@@ -1,29 +1,27 @@
#!/bin/sh
# Tested with:
# - Manjaro 15.09 (x86_64)
# - ArchLinux (x86_64)
# Both "gcc-multilib" and "gcc" packages provide gcc. If user already has
# "gcc-multilib" installed, let's stick to their choice
if pacman -Qc gcc-multilib &>/dev/null
then
GCC_PACKAGE="gcc-multilib";
else
GCC_PACKAGE="gcc";
fi
#
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
# only "virtualenv2" binary, not "virtualenv" necessary in
# ./bootstrap/dev/_common_venv.sh
pacman -S --needed \
git \
python2 \
python-virtualenv \
"$GCC_PACKAGE" \
dialog \
augeas \
openssl \
libffi \
ca-certificates \
pkg-config \
deps="
git
python2
python-virtualenv
gcc
dialog
augeas
openssl
libffi
ca-certificates
pkg-config
"
missing=$(pacman -T $deps)
if [ "$missing" ]; then
pacman -S --needed $missing
fi
+1 -1
View File
@@ -33,7 +33,7 @@ if apt-cache show python-virtualenv > /dev/null ; then
fi
apt-get install -y --no-install-recommends \
git-core \
git \
python \
python-dev \
$virtualenv \
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
PACKAGES="dev-vcs/git
dev-lang/python:2.7
dev-python/virtualenv
dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
case "$PACKAGE_MANAGER" in
(paludis)
cave resolve --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
pmerge --noreplace $PACKAGES
;;
(portage|*)
emerge --noreplace $PACKAGES
;;
esac
-1
View File
@@ -21,7 +21,6 @@ $tool install -y \
python \
python-devel \
python-virtualenv \
python-devel \
gcc \
dialog \
augeas-libs \
+1
View File
@@ -0,0 +1 @@
_gentoo_common.sh
+3
View File
@@ -23,6 +23,9 @@ elif [ -f /etc/arch-release ] ; then
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO $BOOTSTRAP/_rpm_common.sh
elif [ -f /etc/gentoo-release ] ; then
echo "Bootstrapping dependencies for Gentoo-based OSes..."
$SUDO $BOOTSTRAP/_gentoo_common.sh
elif uname | grep -iq FreeBSD ; then
echo "Bootstrapping dependencies for FreeBSD..."
$SUDO $BOOTSTRAP/freebsd.sh
+3 -3
View File
@@ -25,9 +25,9 @@ pip install -U letsencrypt letsencrypt-apache # letsencrypt-nginx
echo
echo "Congratulations, Let's Encrypt has been successfully installed/updated!"
echo
echo -n "Your prompt should now be prepended with ($VENV_NAME). Next "
echo -n "time, if the prompt is different, 'source' this script again "
echo -n "before running 'letsencrypt'."
printf "%s" "Your prompt should now be prepended with ($VENV_NAME). Next "
printf "time, if the prompt is different, 'source' this script again "
printf "before running 'letsencrypt'."
echo
echo
echo "You can now run 'letsencrypt --help'."
+211
View File
@@ -0,0 +1,211 @@
============
Ciphersuites
============
.. contents:: Table of Contents
:local:
.. _ciphersuites:
Introduction
============
Autoupdates
-----------
Within certain limits, TLS server software can choose what kind of
cryptography to use when a client connects. These choices can affect
security, compatibility, and performance in complex ways. Most of
these options are independent of a particular certificate. The Let's
Encrypt client tries to provide defaults that we think are most useful
to our users.
As described below, the Let's Encrypt client will default to modifying
server software's cryptographic settings to keep these up-to-date with
what we think are appropriate defaults when new versions of the Let's
Encrypt client are installed (for example, by an operating system package
manager).
When this feature is implemented, this document will be updated
to describe how to disable these automatic changes.
Cryptographic choices
---------------------
Software that uses cryptography must inevitably make choices about what
kind of cryptography to use and how. These choices entail assumptions
about how well particular cryptographic mechanisms resist attack, and what
trade-offs are available and appropriate. The choices are constrained
by compatibility issues (in order to interoperate with other software,
an implementation must agree to use cryptographic mechanisms that the
other side also supports) and protocol issues (cryptographic mechanisms
must be specified in protocols and there must be a way to agree to use
them in a particular context).
The best choices for a particular application may change over time in
response to new research, new standardization events, changes in computer
hardware, and changes in the prevalence of legacy software. Much important
research on cryptanalysis and cryptographic vulnerabilities is unpublished
because many researchers have been working in the interest of improving
some entities' communications security while weakening, or failing to
improve, others' security. But important information that improves our
understanding of the state of the art is published regularly.
When enabling TLS support in a compatible web server (which is a separate
step from obtaining a certificate), Let's Encrypt has the ability to
update that web server's TLS configuration. Again, this is *different
from the cryptographic particulars of the certificate itself*; the
certificate as of the initial release will be RSA-signed using one of
Let's Encrypt's 2048-bit RSA keys, and will describe the subscriber's
RSA public key ("subject public key") of at least 2048 bits, which is
used for key establishment.
Note that the subscriber's RSA public key can be used in a wide variety
of key establishment methods, most of which do not use RSA directly
for key exchange, but only for authenticating the server! For example,
in DHE and ECDHE key exchanges, the subject public key is just used to
sign other parameters for authentication. You do not have to "use RSA"
for other purposes just because you're using an RSA key for authentication.
The certificate doesn't specify other cryptographic or ciphersuite
particulars; for example, it doesn't say whether or not parties should
use a particular symmetric algorithm like 3DES, or what cipher modes
they should use. All of these details are negotiated between client
and server independent of the content of the ciphersuite. The
Let's Encrypt project hopes to provide useful defaults that reflect
good security choices with respect to the publicly-known state of the
art. However, the Let's Encrypt certificate authority does *not*
dictate end-users' security policy, and any site is welcome to change
its preferences in accordance with its own policy or its administrators'
preferences, and use different cryptographic mechanisms or parameters,
or a different priority order, than the defaults provided by the Let's
Encrypt client.
If you don't use the Let's Encrypt client to configure your server
directly, because the client doesn't integrate with your server software
or because you chose not to use this integration, then the cryptographic
defaults haven't been modified, and the cryptography chosen by the server
will still be whatever the default for your software was. For example,
if you obtain a certificate using *standalone* mode and then manually
install it in an IMAP or LDAP server, your cryptographic settings will
not be modified by the client in any way.
Sources of defaults
-------------------
Initially, the Let's Encrypt client will configure users' servers to
use the cryptographic defaults recommended by the Mozilla project.
These settings are well-reasoned recommendations that carefully
consider client software compatibility. They are described at
https://wiki.mozilla.org/Security/Server_Side_TLS
and the version implemented by the Let's Encrypt client will be the
version that was most current as of the release date of each client
version. Mozilla offers three seperate sets of cryptographic options,
which trade off security and compatibility differently. These are
referred to as as the "Modern", "Intermediate", and "Old" configurations
(in order from most secure to least secure, and least-backwards compatible
to most-backwards compatible). The client will follow the Mozilla defaults
for the *Intermediate* configuration by default, at least with regards to
ciphersuites and TLS versions. Mozilla's web site describes which client
software will be compatible with each configuration. You can also use
the Qualys SSL Labs site, which the Let's Encrypt software will suggest
when installing a certificate, to test your server and see whether it
will be compatible with particular software versions.
It will be possible to ask the Let's Encrypt client to instead apply
(and track) Modern or Old configurations.
The Let's Encrypt project expects to follow the Mozilla recommendations
in the future as those recommendations are updated. (For example, some
users have proposed prioritizing a new ciphersuite known as ``0xcc13``
which uses the ChaCha and Poly1305 algorithms, and which is already
implemented by the Chrome browser. Mozilla has delayed recommending
``0xcc13`` over compatibility and standardization concerns, but is likely
to recommend it in the future once these concerns have been addressed. At
that point, the Let's Encrypt client would likely follow the Mozilla
recommendations and favor the use of this ciphersuite as well.)
The Let's Encrypt project may deviate from the Mozilla recommendations
in the future if good cause is shown and we believe our users'
priorities would be well-served by doing so. In general, please address
relevant proposals for changing priorities to the Mozilla security
team first, before asking the Let's Encrypt project to change the
client's priorities. The Mozilla security team is likely to have more
resources and expertise to bring to bear on evaluating reasons why its
recommendations should be updated.
The Let's Encrpyt project will entertain proposals to create a *very*
small number of alternative configurations (apart from Modern,
Intermediate, and Old) that there's reason to believe would be widely
used by sysadmins; this would usually be a preferable course to modifying
an existing configuration. For example, if many sysadmins want their
servers configured to track a different expert recommendation, Let's
Encrypt could add an option to do so.
Resources for recommendations
-----------------------------
In the course of considering how to handle this issue, we received
recommendations with sources of expert guidance on ciphersuites and other
cryptographic parameters. We're grateful to everyone who contributed
suggestions. The recommendations we received are available at
https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance
Let's Encrypt client users are welcome to review these authorities to
better inform their own cryptographic parameter choices. We also
welcome suggestions of other resources to add to this list. Please keep
in mind that different recommendations may reflect different priorities
or evaluations of trade-offs, especially related to compatibility!
Changing your settings
----------------------
This will probably look something like
..code-block: shell
letsencrypt --cipher-recommendations mozilla-secure
letsencrypt --cipher-recommendations mozilla-intermediate
letsencrypt --cipher-recommendations mozilla-old
to track Mozilla's *Secure*, *Intermediate*, or *Old* recommendations,
and
..code-block: shell
letsencrypt --update-ciphers on
to enable updating ciphers with each new Let's Encrypt client release,
or
..code-block: shell
letsencrypt --update-ciphers off
to disable automatic configuration updates. These features have not yet
been implemented and this syntax may change then they are implemented.
TODO
----
The status of this feature is tracked as part of issue #1123 in our
bug tracker.
https://github.com/letsencrypt/letsencrypt/issues/1123
Prior to implementation of #1123, the client does not actually modify
ciphersuites (this is intended to be implemented as a "configuration
enhancement", but the only configuration enhancement implemented
so far is redirecting HTTP requests to HTTPS in web servers, the
"redirect" enhancement). The changes here would probably be either a new
"ciphersuite" enhancement in each plugin that provides an installer,
or a family of enhancements, one per selectable ciphersuite configuration.
+1 -1
View File
@@ -315,5 +315,5 @@ texinfo_documents = [
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org', None),
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
}
+1 -2
View File
@@ -152,7 +152,7 @@ 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`,
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific
`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific
challenges (subclasses of `~.ContinuityChallenge`,
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
`~.challenges.ProofOfPossession`). Continuity challenges are
@@ -312,7 +312,6 @@ synced to ``/vagrant``, so you can get started with:
vagrant ssh
cd /vagrant
./venv/bin/pip install -r requirements.txt .[dev,docs,testing]
sudo ./venv/bin/letsencrypt
Support for other Linux distributions coming soon.
+7 -4
View File
@@ -73,7 +73,7 @@ to, `install Docker`_, then issue the following command:
.. code-block:: shell
sudo docker run -it --rm -p 443:443 --name letsencrypt \
sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \
-v "/etc/letsencrypt:/etc/letsencrypt" \
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
quay.io/letsencrypt/letsencrypt:latest auth
@@ -127,15 +127,15 @@ Officially supported plugins:
Plugin A I Notes and status
========== = = ================================================================
standalone Y N Very stable. Uses port 80 (force by
``--standalone-supported-challenges simpleHttp``) or 443
(force by ``standalone-supported-challenges dvsni``).
``--standalone-supported-challenges http-01``) or 443
(force by ``--standalone-supported-challenges dvsni``).
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
to the disk (``--webroot-path`` should be pointed to your
``public_html``). Currently, when multiple domains are specified
(`-d`), they must all use the same web root path.
manual Y N Hidden from standard UI, use with ``--a manual``. Requires to
manual Y N Hidden from standard UI, use with ``-a manual``. Requires to
copy and paste commands into a new terminal session. Allows to
run client on machine different than target webserver, e.g. your
laptop.
@@ -163,6 +163,9 @@ sure that UI doesn't prompt for any details you can add the command to
``crontab`` (make it less than every 90 days to avoid problems, say
every month).
Please note that the CA will send notification emails to the address
you provide if you do not renew certificates that are about to expire.
Let's Encrypt is working hard on automating the renewal process. Until
the tool is ready, we are sorry for the inconvenience!
+2 -2
View File
@@ -313,6 +313,6 @@ texinfo_documents = [
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
}
@@ -20,7 +20,7 @@ class ApacheDvsni(common.Dvsni):
larger array. ApacheDvsni is capable of solving many challenges
at once which causes an indexing issue within ApacheConfigurator
who must return all responses in order. Imagine ApacheConfigurator
maintaining state about where all of the SimpleHTTP Challenges,
maintaining state about where all of the http-01 Challenges,
Dvsni Challenges belong in the response array. This is an optional
utility.
@@ -122,7 +122,7 @@ class ApacheParser(object):
"""
try:
proc = subprocess.Popen(
[ctl, "-D", "DUMP_RUN_CFG"],
[ctl, "-t", "-D", "DUMP_RUN_CFG"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
+1
View File
@@ -41,6 +41,7 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
+8 -5
View File
@@ -47,6 +47,9 @@ then
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO $BOOTSTRAP/_rpm_common.sh
elif [ -f /etc/gentoo-release ] ; then
echo "Bootstrapping dependencies for Gentoo-based OSes..."
$SUDO $BOOTSTRAP/_gentoo_common.sh
elif uname | grep -iq FreeBSD ; then
echo "Bootstrapping dependencies for FreeBSD..."
$SUDO $BOOTSTRAP/freebsd.sh
@@ -70,7 +73,7 @@ then
fi
fi
echo -n "Updating letsencrypt and virtual environment dependencies..."
printf "Updating letsencrypt and virtual environment dependencies..."
if [ "$VERBOSE" = 1 ] ; then
echo
$VENV_BIN/pip install -U setuptools
@@ -83,15 +86,15 @@ if [ "$VERBOSE" = 1 ] ; then
fi
else
$VENV_BIN/pip install -U setuptools > /dev/null
echo -n .
printf .
$VENV_BIN/pip install -U pip > /dev/null
echo -n .
printf .
# nginx is buggy / disabled for now...
$VENV_BIN/pip install -U letsencrypt > /dev/null
echo -n .
printf .
$VENV_BIN/pip install -U letsencrypt-apache > /dev/null
if $VENV_BIN/pip freeze | grep -q letsencrypt-nginx ; then
echo -n .
printf .
$VENV_BIN/pip install -U letsencrypt-nginx > /dev/null
fi
echo
+4 -4
View File
@@ -307,8 +307,8 @@ texinfo_documents = [
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org', None),
'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.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),
}
+1
View File
@@ -38,6 +38,7 @@ setup(
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
+2 -2
View File
@@ -306,6 +306,6 @@ texinfo_documents = [
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
}
+1 -1
View File
@@ -26,7 +26,7 @@ class NginxDvsni(common.Dvsni):
larger array. NginxDvsni is capable of solving many challenges
at once which causes an indexing issue within NginxConfigurator
who must return all responses in order. Imagine NginxConfigurator
maintaining state about where all of the SimpleHTTP Challenges,
maintaining state about where all of the http-01 Challenges,
Dvsni Challenges belong in the response array. This is an optional
utility.
+1
View File
@@ -41,6 +41,7 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
+9 -25
View File
@@ -45,6 +45,15 @@ class AnnotatedChallenge(jose.ImmutableMap):
return getattr(self.challb, name)
class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge):
"""Client annotated `KeyAuthorizationChallenge` challenge."""
__slots__ = ('challb', 'domain', 'account_key')
def response_and_validation(self):
"""Generate response and validation."""
return self.challb.chall.response_and_validation(self.account_key)
class DVSNI(AnnotatedChallenge):
"""Client annotated "dvsni" ACME challenge.
@@ -76,31 +85,6 @@ class DVSNI(AnnotatedChallenge):
return response, cert, key
class SimpleHTTP(AnnotatedChallenge):
"""Client annotated "simpleHttp" ACME challenge."""
__slots__ = ('challb', 'domain', 'account_key')
acme_type = challenges.SimpleHTTP
def gen_response_and_validation(self, tls):
"""Generates a SimpleHTTP response and validation.
:param bool tls: True if TLS should be used
:returns: ``(response, validation)`` tuple, where ``response`` is
an instance of `acme.challenges.SimpleHTTPResponse` and
``validation`` is an instance of
`acme.challenges.SimpleHTTPProvisionedResource`.
:rtype: tuple
"""
response = challenges.SimpleHTTPResponse(tls=tls)
validation = response.gen_validation(
self.challb.chall, self.account_key)
logger.debug("Simple HTTP validation payload: %s", validation.payload)
return response, validation
class DNS(AnnotatedChallenge):
"""Client annotated "dns" ACME challenge."""
__slots__ = ('challb', 'domain')
+11 -12
View File
@@ -347,9 +347,6 @@ def challb_to_achall(challb, account_key, domain):
if isinstance(chall, challenges.DVSNI):
return achallenges.DVSNI(
challb=challb, domain=domain, account_key=account_key)
elif isinstance(chall, challenges.SimpleHTTP):
return achallenges.SimpleHTTP(
challb=challb, domain=domain, account_key=account_key)
elif isinstance(chall, challenges.DNS):
return achallenges.DNS(challb=challb, domain=domain)
elif isinstance(chall, challenges.RecoveryContact):
@@ -358,7 +355,9 @@ 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)
@@ -486,29 +485,29 @@ def is_preferred(offered_challb, satisfied,
_ERROR_HELP_COMMON = (
"To fix these errors, please make sure that your domain name was entered "
"correctly and the DNS A record(s) for that domain contains the "
"correctly and the DNS A record(s) for that domain contain(s) the "
"right IP address.")
_ERROR_HELP = {
"connection":
_ERROR_HELP_COMMON + " Additionally, please check that your computer "
"has publicly routable IP address and no firewalls are preventing the "
"server from communicating with the client.",
"has a publicly routable IP address and that no firewalls are preventing "
"the server from communicating with the client.",
"dnssec":
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
"your domain, please ensure the signature is valid.",
"your domain, please ensure that the signature is valid.",
"malformed":
"To fix these errors, please make sure that you did not provide any "
"invalid information to the client and try running Let's Encrypt "
"invalid information to the client, and try running Let's Encrypt "
"again.",
"serverInternal":
"Unfortunately, an error on the ACME server prevented you from completing "
"authorization. Please try again later.",
"tls":
_ERROR_HELP_COMMON + " Additionally, please check that you have an up "
"to date TLS configuration that allows the server to communicate with "
"the Let's Encrypt client.",
_ERROR_HELP_COMMON + " Additionally, please check that you have an "
"up-to-date TLS configuration that allows the server to communicate "
"with the Let's Encrypt client.",
"unauthorized": _ERROR_HELP_COMMON,
"unknownHost": _ERROR_HELP_COMMON,
}
+69 -34
View File
@@ -56,7 +56,7 @@ default, it will attempt to use a webserver both for obtaining and installing
the cert. Major SUBCOMMANDS are:
(default) run Obtain & install a cert in your current webserver
auth Authenticate & obtain cert, but do not install it
certonly Obtain cert, but do not install it (aka "auth")
install Install a previously obtained cert in a server
revoke Revoke a previously obtained certificate
rollback Rollback server configuration changes made during install
@@ -64,27 +64,41 @@ the cert. Major SUBCOMMANDS are:
"""
# This is the short help for letsencrypt --help, where we disable argparse
# altogether
USAGE = SHORT_USAGE + """Choice of server for authentication/installation:
USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
--apache Use the Apache plugin for authentication & installation
--nginx Use the Nginx plugin for authentication & installation
%s
--standalone Run a standalone webserver for authentication
OR:
--authenticator standalone --installer nginx
%s
OR use different servers to obtain (authenticate) the cert and then install it:
--authenticator standalone --installer apache
More detailed help:
-h, --help [topic] print this message, or detailed help on a topic;
the available topics are:
all, apache, automation, manual, nginx, paths, security, testing, or any of
the subcommands
all, automation, paths, security, testing, or any of the subcommands or
plugins (certonly, install, nginx, apache, standalone, etc)
"""
def usage_strings(plugins):
"""Make usage strings late so that plugins can be initialised late"""
if "nginx" in plugins:
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
else:
nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
if "apache" in plugins:
apache_doc = "--apache Use the Apache plugin for authentication & installation"
else:
apache_doc = "(the apache plugin is not installed)"
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
def _find_domains(args, installer):
if args.domains is None:
domains = display_ops.choose_names(installer)
@@ -358,11 +372,11 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
if os.path.exists("/etc/debian_version"):
# Debian... installers are at least possible
msg = ('No installers seem to be present and working on your system; '
'fix that or try running letsencrypt with the "auth" command')
'fix that or try running letsencrypt with the "certonly" command')
else:
# XXX update this logic as we make progress on #788 and nginx support
msg = ('No installers are available on your OS yet; try running '
'"letsencrypt-auto auth" to get a cert you can install manually')
'"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)
@@ -377,7 +391,7 @@ def choose_configurator_plugins(args, config, plugins, verb):
# Which plugins do we need?
need_inst = need_auth = (verb == "run")
if verb == "auth":
if verb == "certonly":
need_auth = True
if verb == "install":
need_inst = True
@@ -447,7 +461,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
display_ops.success_renewal(domains)
def auth(args, config, plugins):
def obtaincert(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""
if args.domains is not None and args.csr is not None:
@@ -457,7 +471,7 @@ def auth(args, config, plugins):
try:
# installers are used in auth mode to determine domain names
installer, authenticator = choose_configurator_plugins(args, config, plugins, "auth")
installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly")
except PluginSelectionError, e:
return e.message
@@ -481,7 +495,8 @@ def install(args, config, plugins):
# XXX: Update for renewer/RenewableCert
try:
installer, _ = choose_configurator_plugins(args, config, plugins, "auth")
installer, _ = choose_configurator_plugins(args, config,
plugins, "install")
except PluginSelectionError, e:
return e.message
@@ -609,7 +624,8 @@ class HelpfulArgumentParser(object):
"""
# Maps verbs/subcommands to the functions that implement them
VERBS = {"auth": auth, "config_changes": config_changes,
VERBS = {"auth": obtaincert, "certonly": obtaincert,
"config_changes": config_changes, "everything": run,
"install": install, "plugins": plugins_cmd,
"revoke": revoke, "rollback": rollback, "run": run}
@@ -620,8 +636,9 @@ class HelpfulArgumentParser(object):
def __init__(self, args, plugins):
plugin_names = [name for name, _p in plugins.iteritems()]
self.help_topics = self.HELP_TOPICS + plugin_names + [None]
usage, short_usage = usage_strings(plugins)
self.parser = configargparse.ArgParser(
usage=SHORT_USAGE,
usage=short_usage,
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
args_for_setting_config_path=["-c", "--config"],
default_config_files=flag_default("config_files"))
@@ -635,12 +652,12 @@ class HelpfulArgumentParser(object):
help1 = self.prescan_for_flag("-h", self.help_topics)
help2 = self.prescan_for_flag("--help", self.help_topics)
assert max(True, "a") == "a", "Gravity changed direction"
help_arg = max(help1, help2)
if help_arg is True:
self.help_arg = max(help1, help2)
if self.help_arg is True:
# just --help with no topic; avoid argparse altogether
print USAGE
print usage
sys.exit(0)
self.visible_topics = self.determine_help_topics(help_arg)
self.visible_topics = self.determine_help_topics(self.help_arg)
#print self.visible_topics
self.groups = {} # elements are added by .add_group()
@@ -669,7 +686,12 @@ class HelpfulArgumentParser(object):
for i, token in enumerate(self.args):
if token in self.VERBS:
self.verb = token
verb = token
if verb == "auth":
verb = "certonly"
if verb == "everything":
verb = "run"
self.verb = verb
self.args.pop(i)
return
@@ -754,6 +776,10 @@ class HelpfulArgumentParser(object):
"""
# topics maps each topic to whether it should be documented by
# argparse on the command line
if chosen_topic == "auth":
chosen_topic = "certonly"
if chosen_topic == "everything":
chosen_topic = "run"
if chosen_topic == "all":
return dict([(t, True) for t in self.help_topics])
elif not chosen_topic:
@@ -828,8 +854,8 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
"testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"),
help=config_help("dvsni_port"))
helpful.add("testing", "--simple-http-port", type=int,
help=config_help("simple_http_port"))
helpful.add("testing", "--http-01-port", dest="http01_port", type=int,
help=config_help("http01_port"))
helpful.add_group(
"security", description="Security parameters & server settings")
@@ -849,24 +875,24 @@ def prepare_and_parse_args(plugins, args):
help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/")
_create_subparsers(helpful)
_paths_parser(helpful)
# _plugins_parsing should be the last thing to act upon the main
# parser (--help should display plugin-specific options last)
_plugins_parsing(helpful, plugins)
_create_subparsers(helpful)
return helpful.parse_args()
def _create_subparsers(helpful):
helpful.add_group("auth", description="Options for modifying how a cert is obtained")
helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
helpful.add_group("install", description="Options for modifying how a cert is deployed")
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("auth",
helpful.add("certonly",
"--csr", type=read_file,
help="Path to a Certificate Signing Request (CSR) in DER"
" format; note that the .csr file *must* contain a Subject"
@@ -890,24 +916,33 @@ def _create_subparsers(helpful):
def _paths_parser(helpful):
add = helpful.add
verb = helpful.verb
if verb == "help":
verb = helpful.help_arg
helpful.add_group(
"paths", description="Arguments changing execution paths & servers")
cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked."
if verb == "auth":
add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph)
cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
section = "paths"
if verb in ("install", "revoke", "certonly"):
section = verb
if verb == "certonly":
add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph)
elif verb == "revoke":
add("paths", "--cert-path", type=read_file, required=True, help=cph)
add(section, "--cert-path", type=read_file, required=True, help=cph)
else:
add("paths", "--cert-path", help=cph, required=(verb == "install"))
add(section, "--cert-path", help=cph, required=(verb == "install"))
section = "paths"
if verb in ("install", "revoke"):
section = verb
print helpful.help_arg, helpful.help_arg == "install"
# revoke --key-path reads a file, install --key-path takes a string
add("paths", "--key-path", type=((verb == "revoke" and read_file) or str),
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)")
default_cp = None
if verb == "auth":
if verb == "certonly":
default_cp = flag_default("auth_chain_path")
add("paths", "--fullchain-path", default=default_cp,
help="Accompanying path to a full certificate chain (cert plus chain).")
+1
View File
@@ -326,6 +326,7 @@ class Client(object):
key_path=os.path.abspath(privkey_path),
chain_path=chain_path,
fullchain_path=fullchain_path)
self.installer.save() # needed by the Apache plugin
self.installer.save("Deployed Let's Encrypt Certificate")
# sites may have been enabled / final cleanup
+6 -6
View File
@@ -37,9 +37,9 @@ class NamespaceConfig(object):
def __init__(self, namespace):
self.namespace = namespace
if self.simple_http_port == self.dvsni_port:
if self.http01_port == self.dvsni_port:
raise errors.Error(
"Trying to run SimpleHTTP and DVSNI "
"Trying to run http-01 and DVSNI "
"on the same port ({0})".format(self.dvsni_port))
def __getattr__(self, name):
@@ -78,11 +78,11 @@ class NamespaceConfig(object):
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
@property
def simple_http_port(self): # pylint: disable=missing-docstring
if self.namespace.simple_http_port is not None:
return self.namespace.simple_http_port
def http01_port(self): # pylint: disable=missing-docstring
if self.namespace.http01_port is not None:
return self.namespace.http01_port
else:
return challenges.SimpleHTTPResponse.PORT
return challenges.HTTP01Response.PORT
class RenewerConfiguration(object):
+1 -1
View File
@@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict(
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
challenges.DVSNI, challenges.SimpleHTTP])])
challenges.DVSNI, challenges.HTTP01])])
"""Mutually exclusive challenges."""
+1 -1
View File
@@ -223,7 +223,7 @@ class IConfig(zope.interface.Interface):
"Port number to perform DVSNI challenge. "
"Boulder in testing mode defaults to 5001.")
simple_http_port = zope.interface.Attribute(
http01_port = zope.interface.Attribute(
"Port used in the SimpleHttp challenge.")
+1 -1
View File
@@ -51,7 +51,7 @@ class PluginEntryPointTest(unittest.TestCase):
def test_description(self):
self.assertEqual(
"Automatically configure and run a simple webserver",
"Automatically use a temporary webserver",
self.plugin_ep.description)
def test_description_with_name(self):
+23 -23
View File
@@ -26,18 +26,19 @@ logger = logging.getLogger(__name__)
class Authenticator(common.Plugin):
"""Manual Authenticator.
This plugin requires user's manual intervention in setting up a HTTP
server for solving SimpleHTTP challenges and thus does not need to be
run as a privilidged process. Alternatively shows instructions on how
to use Python's built-in HTTP server.
This plugin requires user's manual intervention in setting up a HTTP
server for solving http-01 challenges and thus does not need to be
run as a privileged process. Alternatively shows instructions on how
to use Python's built-in HTTP server.
.. todo:: Support for `~.challenges.DVSNI`.
"""
zope.interface.implements(interfaces.IAuthenticator)
zope.interface.classProvides(interfaces.IPluginFactory)
hidden = True
description = "Manually configure and run a simple Python webserver"
description = "Manually configure an HTTP server"
MESSAGE_TEMPLATE = """\
Make sure your web server displays the following content at
@@ -68,9 +69,9 @@ Are you OK with your IP being logged?
# anything recursively under the cwd
CMD_TEMPLATE = """\
mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
mkdir -p {root}/public_html/{achall.URI_ROOT_PATH}
cd {root}/public_html
echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token}
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
"import BaseHTTPServer, SimpleHTTPServer; \\
@@ -95,14 +96,14 @@ s.serve_forever()" """
def more_info(self): # pylint: disable=missing-docstring,no-self-use
return ("This plugin requires user's manual intervention in setting "
"up a HTTP server for solving SimpleHTTP challenges and thus "
"does not need to be run as a privilidged process. "
"up an HTTP server for solving http-01 challenges and thus "
"does not need to be run as a privileged process. "
"Alternatively shows instructions on how to use Python's "
"built-in HTTP server.")
def get_chall_pref(self, domain):
# pylint: disable=missing-docstring,no-self-use,unused-argument
return [challenges.SimpleHTTP]
return [challenges.HTTP01]
def perform(self, achalls): # pylint: disable=missing-docstring
responses = []
@@ -130,27 +131,26 @@ s.serve_forever()" """
# same path for each challenge response would be easier for
# users, but will not work if multiple domains point at the
# same server: default command doesn't support virtual hosts
response, validation = achall.gen_response_and_validation(
tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7
response, validation = achall.response_and_validation()
port = (response.port if self.config.simple_http_port is None
else int(self.config.simple_http_port))
port = (response.port if self.config.http01_port is None
else int(self.config.http01_port))
command = self.CMD_TEMPLATE.format(
root=self._root, achall=achall, response=response,
validation=pipes.quote(validation.json_dumps()),
# TODO(kuba): pipes still necessary?
validation=pipes.quote(validation),
encoded_token=achall.chall.encode("token"),
ct=response.CONTENT_TYPE, port=port)
ct=achall.CONTENT_TYPE, port=port)
if self.conf("test-mode"):
logger.debug("Test mode. Executing the manual command: %s", command)
# sh shipped with OS X does't support echo -n
executable = "/bin/bash" if sys.platform == "darwin" else None
# sh shipped with OS X does't support echo -n, but supports printf
try:
self._httpd = subprocess.Popen(
command,
# don't care about setting stdout and stderr,
# we're in test mode anyway
shell=True,
executable=executable,
executable=None,
# "preexec_fn" is UNIX specific, but so is "command"
preexec_fn=os.setsid)
except OSError as error: # ValueError should not happen!
@@ -169,13 +169,13 @@ s.serve_forever()" """
raise errors.PluginError("Must agree to IP logging to proceed")
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
validation=validation.json_dumps(), response=response,
uri=response.uri(achall.domain, achall.challb.chall),
ct=response.CONTENT_TYPE, command=command))
validation=validation, response=response,
uri=achall.chall.uri(achall.domain),
ct=achall.CONTENT_TYPE, command=command))
if response.simple_verify(
achall.chall, achall.domain,
achall.account_key.public_key(), self.config.simple_http_port):
achall.account_key.public_key(), self.config.http01_port):
return response
else:
logger.error(
+7 -7
View File
@@ -23,13 +23,13 @@ class AuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.manual import Authenticator
self.config = mock.MagicMock(
simple_http_port=8080, manual_test_mode=False)
http01_port=8080, manual_test_mode=False)
self.auth = Authenticator(config=self.config, name="manual")
self.achalls = [achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)]
self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)]
config_test_mode = mock.MagicMock(
simple_http_port=8080, manual_test_mode=True)
http01_port=8080, manual_test_mode=True)
self.auth_test_mode = Authenticator(
config=config_test_mode, name="manual")
@@ -45,13 +45,13 @@ class AuthenticatorTest(unittest.TestCase):
@mock.patch("letsencrypt.plugins.manual.zope.component.getUtility")
@mock.patch("letsencrypt.plugins.manual.sys.stdout")
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify")
@mock.patch("acme.challenges.HTTP01Response.simple_verify")
@mock.patch("__builtin__.raw_input")
def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
mock_verify.return_value = True
mock_interaction().yesno.return_value = True
resp = challenges.SimpleHTTPResponse(tls=False)
resp = self.achalls[0].response(KEY)
self.assertEqual([resp], self.auth.perform(self.achalls))
self.assertEqual(1, mock_raw_input.call_count)
mock_verify.assert_called_with(
@@ -89,7 +89,7 @@ class AuthenticatorTest(unittest.TestCase):
@mock.patch("letsencrypt.plugins.manual.socket.socket")
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
@mock.patch("acme.challenges.HTTP01Response.simple_verify",
autospec=True)
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep,
+23 -23
View File
@@ -14,7 +14,6 @@ from acme import challenges
from acme import crypto_util as acme_crypto_util
from acme import standalone as acme_standalone
from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt import interfaces
@@ -33,7 +32,7 @@ class ServerManager(object):
`acme.crypto_util.SSLSocket.certs` and
`acme.crypto_util.SSLSocket.simple_http_resources` respectively. All
created servers share the same certificates and resources, so if
you're running both TLS and non-TLS instances, SimpleHTTP handlers
you're running both TLS and non-TLS instances, HTTP01 handlers
will serve the same URLs!
"""
@@ -52,13 +51,13 @@ class ServerManager(object):
:param int port: Port to run the server on.
:param challenge_type: Subclass of `acme.challenges.Challenge`,
either `acme.challenge.SimpleHTTP` or `acme.challenges.DVSNI`.
either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`.
:returns: Server instance.
:rtype: ACMEServerMixin
"""
assert challenge_type in (challenges.DVSNI, challenges.SimpleHTTP)
assert challenge_type in (challenges.DVSNI, challenges.HTTP01)
if port in self._instances:
return self._instances[port].server
@@ -66,13 +65,15 @@ class ServerManager(object):
try:
if challenge_type is challenges.DVSNI:
server = acme_standalone.DVSNIServer(address, self.certs)
else: # challenges.SimpleHTTP
server = acme_standalone.SimpleHTTPServer(
else: # challenges.HTTP01
server = acme_standalone.HTTP01Server(
address, self.simple_http_resources)
except socket.error as error:
raise errors.StandaloneBindError(error, port)
thread = threading.Thread(target=server.serve_forever2)
thread = threading.Thread(
# pylint: disable=no-member
target=server.serve_forever)
thread.start()
# if port == 0, then random free port on OS is taken
@@ -90,7 +91,7 @@ class ServerManager(object):
instance = self._instances[port]
logger.debug("Stopping server at %s:%d...",
*instance.server.socket.getsockname()[:2])
instance.server.shutdown2()
instance.server.shutdown()
instance.thread.join()
del self._instances[port]
@@ -108,7 +109,7 @@ class ServerManager(object):
in six.iteritems(self._instances))
SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.SimpleHTTP])
SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01])
def supported_challenges_validator(data):
@@ -137,22 +138,22 @@ 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 SimpleHTTP
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.
"""
zope.interface.implements(interfaces.IAuthenticator)
zope.interface.classProvides(interfaces.IPluginFactory)
description = "Automatically configure and run a simple webserver"
description = "Automatically use a temporary webserver"
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
# one self-signed key for all DVSNI and SimpleHTTP certificates
# one self-signed key for all DVSNI and HTTP01 certificates
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048)
# TODO: generate only when the first SimpleHTTP challenge is solved
# 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"])
@@ -183,17 +184,17 @@ class Authenticator(common.Plugin):
@property
def _necessary_ports(self):
necessary_ports = set()
if challenges.SimpleHTTP in self.supported_challenges:
necessary_ports.add(self.config.simple_http_port)
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)
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 SimpleHTTP 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 DVSNI "
"and HTTP01 challenges from the certificate authority. "
"Therefore, it does not rely on any existing server program.")
def prepare(self): # pylint: disable=missing-docstring
pass
@@ -235,13 +236,12 @@ class Authenticator(common.Plugin):
responses = []
for achall in achalls:
if isinstance(achall, achallenges.SimpleHTTP):
if isinstance(achall.chall, challenges.HTTP01):
server = self.servers.run(
self.config.simple_http_port, challenges.SimpleHTTP)
response, validation = achall.gen_response_and_validation(
tls=False)
self.config.http01_port, challenges.HTTP01)
response, validation = achall.response_and_validation()
self.simple_http_resources.add(
acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource(
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
chall=achall.chall, response=response,
validation=validation))
cert = self.simple_http_cert
+19 -18
View File
@@ -43,12 +43,12 @@ class ServerManagerTest(unittest.TestCase):
self._test_run_stop(challenges.DVSNI)
def test_run_stop_simplehttp(self):
self._test_run_stop(challenges.SimpleHTTP)
self._test_run_stop(challenges.HTTP01)
def test_run_idempotent(self):
server = self.mgr.run(port=0, challenge_type=challenges.SimpleHTTP)
server = self.mgr.run(port=0, challenge_type=challenges.HTTP01)
port = server.socket.getsockname()[1] # pylint: disable=no-member
server2 = self.mgr.run(port=port, challenge_type=challenges.SimpleHTTP)
server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01)
self.assertEqual(self.mgr.running(), {port: server})
self.assertTrue(server is server2)
self.mgr.stop(port)
@@ -60,7 +60,7 @@ class ServerManagerTest(unittest.TestCase):
port = some_server.getsockname()[1]
self.assertRaises(
errors.StandaloneBindError, self.mgr.run, port,
challenge_type=challenges.SimpleHTTP)
challenge_type=challenges.HTTP01)
self.assertEqual(self.mgr.running(), {})
@@ -74,9 +74,9 @@ class SupportedChallengesValidatorTest(unittest.TestCase):
def test_correct(self):
self.assertEqual("dvsni", self._call("dvsni"))
self.assertEqual("simpleHttp", self._call("simpleHttp"))
self.assertEqual("dvsni,simpleHttp", self._call("dvsni,simpleHttp"))
self.assertEqual("simpleHttp,dvsni", self._call("simpleHttp,dvsni"))
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"))
def test_unrecognized(self):
assert "foo" not in challenges.Challenge.TYPES
@@ -91,25 +91,26 @@ class AuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.standalone import Authenticator
self.config = mock.MagicMock(dvsni_port=1234, simple_http_port=4321,
standalone_supported_challenges="dvsni,simpleHttp")
self.config = mock.MagicMock(
dvsni_port=1234, http01_port=4321,
standalone_supported_challenges="dvsni,http-01")
self.auth = Authenticator(self.config, name="standalone")
def test_supported_challenges(self):
self.assertEqual(self.auth.supported_challenges,
set([challenges.DVSNI, challenges.SimpleHTTP]))
set([challenges.DVSNI, 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.SimpleHTTP]))
set([challenges.DVSNI, challenges.HTTP01]))
@mock.patch("letsencrypt.plugins.standalone.util")
def test_perform_alredy_listening(self, mock_util):
for chall, port in ((challenges.DVSNI.typ, 1234),
(challenges.SimpleHTTP.typ, 4321)):
(challenges.HTTP01.typ, 4321)):
mock_util.already_listening.return_value = True
self.config.standalone_supported_challenges = chall
self.assertRaises(
@@ -152,8 +153,8 @@ 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.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP_P, domain=domain, account_key=key)
simple_http = 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)
@@ -167,11 +168,11 @@ class AuthenticatorTest(unittest.TestCase):
self.assertTrue(isinstance(responses, list))
self.assertEqual(2, len(responses))
self.assertTrue(isinstance(responses[0], challenges.SimpleHTTPResponse))
self.assertTrue(isinstance(responses[0], challenges.HTTP01Response))
self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse))
self.assertEqual(self.auth.servers.run.mock_calls, [
mock.call(4321, challenges.SimpleHTTP),
mock.call(4321, challenges.HTTP01),
mock.call(1234, challenges.DVSNI),
])
self.assertEqual(self.auth.served, {
@@ -181,8 +182,8 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(1, len(self.auth.simple_http_resources))
self.assertEqual(2, len(self.auth.certs))
self.assertEqual(list(self.auth.simple_http_resources), [
acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource(
acme_util.SIMPLE_HTTP, responses[0], mock.ANY)])
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
acme_util.HTTP01, responses[0], mock.ANY)])
def test_cleanup(self):
self.auth.servers = mock.Mock()
+46 -8
View File
@@ -1,4 +1,43 @@
"""Webroot plugin."""
"""Webroot plugin.
Content-Type
------------
This plugin requires your webserver to use a specific `Content-Type`
header in the HTTP response.
Apache2
~~~~~~~
.. note:: Instructions written and tested for Debian Jessie. Other
operating systems might use something very similar, but you might
still need to readjust some commands.
Create ``/etc/apache2/conf-available/letsencrypt-simplehttp.conf``, with
the following contents::
<IfModule mod_headers.c>
<LocationMatch "/.well-known/acme-challenge/*">
Header set Content-Type "application/jose+json"
</LocationMatch>
</IfModule>
and then run ``a2enmod headers; a2enconf letsencrypt``; depending on the
output you will have to either ``service apache2 restart`` or ``service
apache2 reload``.
nginx
~~~~~
Use the following snippet in your ``server{...}`` stanza::
location ~ /.well-known/acme-challenge/(.*) {
default_type application/jose+json;
}
and reload your daemon.
"""
import errno
import logging
import os
@@ -23,7 +62,7 @@ class Authenticator(common.Plugin):
description = "Webroot Authenticator"
MORE_INFO = """\
Authenticator plugin that performs SimpleHTTP challenge by saving
Authenticator plugin that performs http-01 challenge by saving
necessary validation resources to appropriate paths on the file
system. It expects that there is some other HTTP server configured
to serve all files under specified web root ({0})."""
@@ -37,7 +76,7 @@ to serve all files under specified web root ({0})."""
def get_chall_pref(self, domain): # pragma: no cover
# pylint: disable=missing-docstring,no-self-use,unused-argument
return [challenges.SimpleHTTP]
return [challenges.HTTP01]
def __init__(self, *args, **kwargs):
super(Authenticator, self).__init__(*args, **kwargs)
@@ -51,8 +90,7 @@ to serve all files under specified web root ({0})."""
if not os.path.isdir(path):
raise errors.PluginError(
path + " does not exist or is not a directory")
self.full_root = os.path.join(
path, challenges.SimpleHTTPResponse.URI_ROOT_PATH)
self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH)
logger.debug("Creating root challenges validation dir at %s",
self.full_root)
@@ -61,7 +99,7 @@ to serve all files under specified web root ({0})."""
except OSError as exception:
if exception.errno != errno.EEXIST:
raise errors.PluginError(
"Couldn't create root for SimpleHTTP "
"Couldn't create root for http-01 "
"challenge responses: {0}", exception)
def perform(self, achalls): # pylint: disable=missing-docstring
@@ -72,11 +110,11 @@ to serve all files under specified web root ({0})."""
return os.path.join(self.full_root, achall.chall.encode("token"))
def _perform_single(self, achall):
response, validation = achall.gen_response_and_validation(tls=False)
response, validation = achall.response_and_validation()
path = self._path_for_achall(achall)
logger.debug("Attempting to save validation to %s", path)
with open(path, "w") as validation_file:
validation_file.write(validation.json_dumps())
validation_file.write(validation.encode())
return response
def cleanup(self, achalls): # pylint: disable=missing-docstring
+8 -5
View File
@@ -6,6 +6,7 @@ import unittest
import mock
from acme import challenges
from acme import jose
from letsencrypt import achallenges
@@ -21,8 +22,8 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class AuthenticatorTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.webroot.Authenticator."""
achall = achallenges.SimpleHTTP(
challb=acme_util.SIMPLE_HTTP_P, domain=None, account_key=KEY)
achall = achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain=None, account_key=KEY)
def setUp(self):
from letsencrypt.plugins.webroot import Authenticator
@@ -70,9 +71,11 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(1, len(responses))
self.assertTrue(os.path.exists(self.validation_path))
with open(self.validation_path) as validation_f:
validation = jose.JWS.json_loads(validation_f.read())
self.assertTrue(responses[0].check_validation(
validation, self.achall.chall, KEY.public_key()))
validation = validation_f.read()
self.assertTrue(
challenges.KeyAuthorizationChallengeResponse(
key_authorization=validation).verify(
self.achall.chall, KEY.public_key()))
self.auth.cleanup([self.achall])
self.assertFalse(os.path.exists(self.validation_path))
+1 -1
View File
@@ -76,7 +76,7 @@ def renew(cert, old_version):
# was an int, not a str)
config.rsa_key_size = int(config.rsa_key_size)
config.dvsni_port = int(config.dvsni_port)
config.namespace.simple_http_port = int(config.namespace.simple_http_port)
config.namespace.http01_port = int(config.namespace.http01_port)
zope.component.provideUtility(config)
try:
authenticator = plugins[renewalparams["authenticator"]]
+14 -15
View File
@@ -2,7 +2,6 @@
import datetime
import os
import re
import time
import configobj
import parsedatetime
@@ -24,8 +23,8 @@ def config_with_defaults(config=None):
return defaults_copy
def parse_time_interval(interval, textparser=parsedatetime.Calendar()):
"""Parse the time specified time interval.
def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()):
"""Parse the time specified time interval, and add it to the base_time
The interval can be in the English-language format understood by
parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or
@@ -33,15 +32,19 @@ def parse_time_interval(interval, textparser=parsedatetime.Calendar()):
hours'. If an integer is found with no associated unit, it is
interpreted by default as a number of days.
:param datetime.datetime base_time: The time to be added with the interval.
:param str interval: The time interval to parse.
:returns: The interpretation of the time interval.
:rtype: :class:`datetime.timedelta`"""
:returns: The base_time plus the interpretation of the time interval.
:rtype: :class:`datetime.datetime`"""
if interval.strip().isdigit():
interval += " days"
return datetime.timedelta(0, time.mktime(textparser.parse(
interval, time.localtime(0))[0]))
# try to use the same timezone, but fallback to UTC
tzinfo = base_time.tzinfo or pytz.UTC
return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0]
class RenewableCert(object): # pylint: disable=too-many-instance-attributes
@@ -465,11 +468,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
if self.has_pending_deployment():
interval = self.configuration.get("deploy_before_expiry",
"5 days")
autodeploy_interval = parse_time_interval(interval)
expiry = crypto_util.notAfter(self.current_target("cert"))
now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
remaining = expiry - now
if remaining < autodeploy_interval:
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
if expiry < add_time_interval(now, interval):
return True
return False
@@ -532,12 +533,10 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
# Renewals on the basis of expiry time
interval = self.configuration.get("renew_before_expiry", "10 days")
autorenew_interval = parse_time_interval(interval)
expiry = crypto_util.notAfter(self.version(
"cert", self.latest_common_version()))
now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
remaining = expiry - now
if remaining < autorenew_interval:
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
if expiry < add_time_interval(now, interval):
return True
return False
+4 -4
View File
@@ -12,7 +12,7 @@ from letsencrypt.tests import test_util
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
# Challenges
SIMPLE_HTTP = challenges.SimpleHTTP(
HTTP01 = challenges.HTTP01(
token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
DVSNI = challenges.DVSNI(
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
@@ -41,7 +41,7 @@ POP = challenges.ProofOfPossession(
)
)
CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, POP]
CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP]
DV_CHALLENGES = [chall for chall in CHALLENGES
if isinstance(chall, challenges.DVChallenge)]
CONT_CHALLENGES = [chall for chall in CHALLENGES
@@ -80,12 +80,12 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
# Pending ChallengeBody objects
DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING)
SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, 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 = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
CHALLENGES_P = [HTTP01_P, DVSNI_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 = [
+29 -23
View File
@@ -9,21 +9,13 @@ from acme import challenges
from acme import client as acme_client
from acme import messages
from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.tests import acme_util
TRANSLATE = {
"dvsni": "DVSNI",
"simpleHttp": "SimpleHTTP",
"dns": "DNS",
"recoveryContact": "RecoveryContact",
"proofOfPossession": "ProofOfPossession",
}
class ChallengeFactoryTest(unittest.TestCase):
# pylint: disable=protected-access
@@ -283,6 +275,22 @@ class PollChallengesTest(unittest.TestCase):
return (new_authzr, "response")
class ChallbToAchallTest(unittest.TestCase):
"""Tests for letsencrypt.auth_handler.challb_to_achall."""
def _call(self, challb):
from letsencrypt.auth_handler import challb_to_achall
return challb_to_achall(challb, "account_key", "domain")
def test_it(self):
self.assertEqual(
self._call(acme_util.HTTP01_P),
achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, account_key="account_key",
domain="domain"),
)
class GenChallengePathTest(unittest.TestCase):
"""Tests for letsencrypt.auth_handler.gen_challenge_path.
@@ -301,8 +309,8 @@ class GenChallengePathTest(unittest.TestCase):
return gen_challenge_path(challbs, preferences, combinations)
def test_common_case(self):
"""Given DVSNI and SimpleHTTP with appropriate combos."""
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P)
"""Given DVSNI and HTTP01 with appropriate combos."""
challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P)
prefs = [challenges.DVSNI]
combos = ((0,), (1,))
@@ -317,7 +325,7 @@ class GenChallengePathTest(unittest.TestCase):
challbs = (acme_util.POP_P,
acme_util.RECOVERY_CONTACT_P,
acme_util.DVSNI_P,
acme_util.SIMPLE_HTTP_P)
acme_util.HTTP01_P)
prefs = [challenges.ProofOfPossession, challenges.DVSNI]
combos = acme_util.gen_combos(challbs)
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
@@ -329,12 +337,12 @@ class GenChallengePathTest(unittest.TestCase):
challbs = (acme_util.RECOVERY_CONTACT_P,
acme_util.POP_P,
acme_util.DVSNI_P,
acme_util.SIMPLE_HTTP_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.SimpleHTTP,
challenges.HTTP01,
challenges.DVSNI,
challenges.RecoveryContact]
combos = acme_util.gen_combos(challbs)
@@ -403,8 +411,8 @@ 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.SimpleHTTP]),
frozenset([challenges.DNS, challenges.SimpleHTTP]),
frozenset([challenges.DVSNI, challenges.HTTP01]),
frozenset([challenges.DNS, challenges.HTTP01]),
]))
def test_empty_satisfied(self):
@@ -413,7 +421,7 @@ class IsPreferredTest(unittest.TestCase):
def test_mutually_exclusvie(self):
self.assertFalse(
self._call(
acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P])))
acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P])))
def test_mutually_exclusive_same_type(self):
self.assertTrue(
@@ -425,16 +433,14 @@ class ReportFailedChallsTest(unittest.TestCase):
# pylint: disable=protected-access
def setUp(self):
from letsencrypt import achallenges
kwargs = {
"chall": acme_util.SIMPLE_HTTP,
"chall": acme_util.HTTP01,
"uri": "uri",
"status": messages.STATUS_INVALID,
"error": messages.Error(typ="tls", detail="detail"),
}
self.simple_http = achallenges.SimpleHTTP(
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
# pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),
domain="example.com",
@@ -458,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.simple_http, self.dvsni_same])
auth_handler._report_failed_challs([self.http01, self.dvsni_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])
@@ -467,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.simple_http, self.dvsni_diff])
auth_handler._report_failed_challs([self.http01, self.dvsni_diff])
self.assertTrue(mock_zope().add_message.call_count == 2)
+73 -40
View File
@@ -64,35 +64,62 @@ class CLITest(unittest.TestCase):
self._call([])
self.assertEqual(1, mock_run.call_count)
def _help_output(self, args):
"Run a help command, and return the help string for scrutiny"
output = StringIO.StringIO()
with mock.patch('letsencrypt.cli.sys.stdout', new=output):
self.assertRaises(SystemExit, self._call_stdout, args)
out = output.getvalue()
return out
def test_help(self):
self.assertRaises(SystemExit, self._call, ['--help'])
self.assertRaises(SystemExit, self._call, ['--help', 'all'])
output = StringIO.StringIO()
with mock.patch('letsencrypt.cli.sys.stdout', new=output):
self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all'])
out = output.getvalue()
self.assertTrue("--configurator" in out)
self.assertTrue("how a cert is deployed" in out)
self.assertTrue("--manual-test-mode" in out)
output.truncate(0)
self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx'])
out = output.getvalue()
if "nginx" in disco.PluginsRegistry.find_all():
# may be false while building distributions without plugins
self.assertTrue("--nginx-ctl" in out)
self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--checkpoints" not in out)
output.truncate(0)
self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins'])
out = output.getvalue()
self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--prepare" in out)
self.assertTrue("Plugin options" in out)
output.truncate(0)
self.assertRaises(SystemExit, self._call_stdout, ['-h'])
out = output.getvalue()
from letsencrypt import cli
self.assertTrue(cli.USAGE in out)
plugins = disco.PluginsRegistry.find_all()
out = self._help_output(['--help', 'all'])
self.assertTrue("--configurator" in out)
self.assertTrue("how a cert is deployed" in out)
self.assertTrue("--manual-test-mode" in out)
out = self._help_output(['-h', 'nginx'])
if "nginx" in plugins:
# may be false while building distributions without plugins
self.assertTrue("--nginx-ctl" in out)
self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--checkpoints" not in out)
out = self._help_output(['-h'])
if "nginx" in plugins:
self.assertTrue("Use the Nginx plugin" in out)
else:
self.assertTrue("(nginx support is experimental" in out)
out = self._help_output(['--help', 'plugins'])
self.assertTrue("--manual-test-mode" not in out)
self.assertTrue("--prepare" in out)
self.assertTrue("Plugin options" in out)
out = self._help_output(['--help', 'install'])
self.assertTrue("--cert-path" in out)
self.assertTrue("--key-path" in out)
out = self._help_output(['--help', 'revoke'])
self.assertTrue("--cert-path" in out)
self.assertTrue("--key-path" in out)
out = self._help_output(['-h', 'config_changes'])
self.assertTrue("--cert-path" not in out)
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.display_ops')
def test_installer_selection(self, mock_display_ops):
self._call(['install', '--domain', '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):
real_plugins = disco.PluginsRegistry.find_all()
@@ -115,6 +142,12 @@ class CLITest(unittest.TestCase):
# (we can only do that if letsencrypt-nginx is actually present)
ret, _, _, _ = self._call(args)
self.assertTrue("The nginx plugin is not working" in ret)
self.assertTrue("Could not find configuration root" in ret)
self.assertTrue("NoInstallationError" in ret)
with MockedVerb("certonly") as mock_certonly:
self._call(["auth", "--standalone"])
self.assertEqual(1, mock_certonly.call_count)
def test_rollback(self):
_, _, _, client = self._call(['rollback'])
@@ -135,16 +168,16 @@ class CLITest(unittest.TestCase):
for r in xrange(len(flags)))):
self._call(['plugins'] + list(args))
def test_auth_bad_args(self):
ret, _, _, _ = self._call(['-d', 'foo.bar', 'auth', '--csr', CSR])
def test_certonly_bad_args(self):
ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth'])
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
@mock.patch('letsencrypt.crypto_util.notAfter')
@mock.patch('letsencrypt.cli.zope.component.getUtility')
def test_auth_new_request_success(self, mock_get_utility, mock_notAfter):
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
cert_path = '/etc/letsencrypt/live/foo.bar'
date = '1970-01-01'
mock_notAfter().date.return_value = date
@@ -152,7 +185,7 @@ class CLITest(unittest.TestCase):
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path)
mock_client = mock.MagicMock()
mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
self._auth_new_request_common(mock_client)
self._certonly_new_request_common(mock_client)
self.assertEqual(
mock_client.obtain_and_enroll_certificate.call_count, 1)
self.assertTrue(
@@ -160,23 +193,23 @@ class CLITest(unittest.TestCase):
self.assertTrue(
date in mock_get_utility().add_message.call_args[0][0])
def test_auth_new_request_failure(self):
def test_certonly_new_request_failure(self):
mock_client = mock.MagicMock()
mock_client.obtain_and_enroll_certificate.return_value = False
self.assertRaises(errors.Error,
self._auth_new_request_common, mock_client)
self._certonly_new_request_common, mock_client)
def _auth_new_request_common(self, mock_client):
def _certonly_new_request_common(self, mock_client):
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
mock_renewal.return_value = None
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
mock_init.return_value = mock_client
self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth'])
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
@mock.patch('letsencrypt.cli.zope.component.getUtility')
@mock.patch('letsencrypt.cli._treat_as_renewal')
@mock.patch('letsencrypt.cli._init_le_client')
def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility):
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility):
cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
@@ -190,7 +223,7 @@ class CLITest(unittest.TestCase):
mock_init.return_value = mock_client
with mock.patch('letsencrypt.cli.OpenSSL'):
with mock.patch('letsencrypt.cli.crypto_util'):
self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth'])
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
mock_client.obtain_certificate.assert_called_once_with(['foo.bar'])
self.assertEqual(mock_lineage.save_successor.call_count, 1)
mock_lineage.update_all_links_to.assert_called_once_with(
@@ -202,8 +235,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_auth_csr(self, mock_init, mock_get_utility,
mock_pick_installer, mock_notAfter):
def test_certonly_csr(self, mock_init, mock_get_utility,
mock_pick_installer, mock_notAfter):
cert_path = '/etc/letsencrypt/live/blahcert.pem'
date = '1970-01-01'
mock_notAfter().date.return_value = date
@@ -216,7 +249,7 @@ class CLITest(unittest.TestCase):
installer = 'installer'
self._call(
['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR,
['-a', 'standalone', '-i', installer, 'certonly', '--csr', CSR,
'--cert-path', cert_path, '--fullchain-path', '/',
'--chain-path', '/'])
self.assertEqual(mock_pick_installer.call_args[0][1], installer)
+1 -1
View File
@@ -163,7 +163,7 @@ class ClientTest(unittest.TestCase):
domain='foo.bar',
fullchain_path='fullchain',
key_path=os.path.abspath("key"))
self.assertEqual(installer.save.call_count, 1)
self.assertEqual(installer.save.call_count, 2)
installer.restart.assert_called_once_with()
@mock.patch("letsencrypt.client.enhancements")
+5 -5
View File
@@ -14,7 +14,7 @@ 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, simple_http_port=4321)
dvsni_port=1234, http01_port=4321)
from letsencrypt.configuration import NamespaceConfig
self.config = NamespaceConfig(self.namespace)
@@ -54,10 +54,10 @@ class NamespaceConfigTest(unittest.TestCase):
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
def test_simple_http_port(self):
self.assertEqual(4321, self.config.simple_http_port)
self.namespace.simple_http_port = None
self.assertEqual(80, self.config.simple_http_port)
def test_http01_port(self):
self.assertEqual(4321, self.config.http01_port)
self.namespace.http01_port = None
self.assertEqual(80, self.config.http01_port)
class RenewerConfigurationTest(unittest.TestCase):
+42 -12
View File
@@ -1,5 +1,6 @@
"""Tests for letsencrypt.renewer."""
import datetime
import pytz
import os
import tempfile
import shutil
@@ -622,18 +623,47 @@ class RenewableCertTests(BaseRenewableCertTest):
# OCSP server to test against.
self.assertFalse(self.test_rc.ocsp_revoked())
def test_parse_time_interval(self):
def test_add_time_interval(self):
from letsencrypt import storage
# XXX: I'm not sure if intervals related to years and months
# take account of the current date (if so, some of these
# may fail in the future, like in leap years or even in
# months of different lengths!)
intended = {"": 0, "17 days": 17, "23": 23, "1 month": 31,
"7 weeks": 49, "1 year 1 day": 366, "1 year-1 day": 364,
"4 years": 1461}
for time in intended:
self.assertEqual(storage.parse_time_interval(time),
datetime.timedelta(intended[time]))
# this month has 30 days, and the next year is a leap year
time_1 = pytz.UTC.fromutc(datetime.datetime(2003, 11, 20, 11, 59, 21))
# this month has 31 days, and the next year is not a leap year
time_2 = pytz.UTC.fromutc(datetime.datetime(2012, 10, 18, 21, 31, 16))
# in different time zone (GMT+8)
time_3 = pytz.timezone('Asia/Shanghai').fromutc(
datetime.datetime(2015, 10, 26, 22, 25, 41))
intended = {
(time_1, ""): time_1,
(time_2, ""): time_2,
(time_3, ""): time_3,
(time_1, "17 days"): time_1 + datetime.timedelta(17),
(time_2, "17 days"): time_2 + datetime.timedelta(17),
(time_1, "30"): time_1 + datetime.timedelta(30),
(time_2, "30"): time_2 + datetime.timedelta(30),
(time_1, "7 weeks"): time_1 + datetime.timedelta(49),
(time_2, "7 weeks"): time_2 + datetime.timedelta(49),
# 1 month is always 30 days, no matter which month it is
(time_1, "1 month"): time_1 + datetime.timedelta(30),
(time_2, "1 month"): time_2 + datetime.timedelta(31),
# 1 year could be 365 or 366 days, depends on the year
(time_1, "1 year"): time_1 + datetime.timedelta(366),
(time_2, "1 year"): time_2 + datetime.timedelta(365),
(time_1, "1 year 1 day"): time_1 + datetime.timedelta(367),
(time_2, "1 year 1 day"): time_2 + datetime.timedelta(366),
(time_1, "1 year-1 day"): time_1 + datetime.timedelta(365),
(time_2, "1 year-1 day"): time_2 + datetime.timedelta(364),
(time_1, "4 years"): time_1 + datetime.timedelta(1461),
(time_2, "4 years"): time_2 + datetime.timedelta(1461),
}
for parameters, excepted in intended.items():
base_time, interval = parameters
self.assertEqual(storage.add_time_interval(base_time, interval),
excepted)
@mock.patch("letsencrypt.renewer.plugins_disco")
@mock.patch("letsencrypt.account.AccountFileStorage")
@@ -659,7 +689,7 @@ class RenewableCertTests(BaseRenewableCertTest):
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"]["simple_http_port"] = "1234"
self.test_rc.configfile["renewalparams"]["http01_port"] = "1234"
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
mock_auth = mock.MagicMock()
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
+2 -2
View File
@@ -306,6 +306,6 @@ texinfo_documents = [
intersphinx_mapping = {
'python': ('https://docs.python.org/', None),
'acme': ('https://acme-python.readthedocs.org', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
}
+1
View File
@@ -34,6 +34,7 @@ setup(
'Operating System :: POSIX :: Linux',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
+1 -1
View File
@@ -28,7 +28,7 @@ common() {
}
common --domains le1.wtf --standalone-supported-challenges dvsni auth
common --domains le2.wtf --standalone-supported-challenges simpleHttp run
common --domains le2.wtf --standalone-supported-challenges http-01 run
common -a manual -d le.wtf auth
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
+1 -1
View File
@@ -16,7 +16,7 @@ letsencrypt_test () {
--server "${SERVER:-http://localhost:4000/directory}" \
--no-verify-ssl \
--dvsni-port 5001 \
--simple-http-port 5002 \
--http-01-port 5002 \
--manual-test-mode \
$store_flags \
--text \
+3 -3
View File
@@ -2,9 +2,9 @@
#
# Find all Python imports.
#
# ./deps.sh letsencrypt
# ./deps.sh acme
# ./deps.sh letsencrypt-apache
# ./tools/deps.sh letsencrypt
# ./tools/deps.sh acme
# ./tools/deps.sh letsencrypt-apache
# ...
#
# Manually compare the output with deps in setup.py.