mirror of
https://github.com/certbot/certbot.git
synced 2026-05-28 04:34:11 -04:00
Merge remote-tracking branch 'github/letsencrypt/master' into csr
Conflicts: letsencrypt/cli.py letsencrypt/client.py letsencrypt/tests/client_test.py
This commit is contained in:
commit
e51f300ee6
112 changed files with 2889 additions and 3262 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -15,4 +15,7 @@ dist/
|
|||
# editor temporary files
|
||||
*~
|
||||
*.swp
|
||||
\#*#
|
||||
\#*#
|
||||
|
||||
# auth --cert-path --chain-path
|
||||
/*.pem
|
||||
|
|
@ -3,7 +3,7 @@ ChangeLog
|
|||
|
||||
Please note:
|
||||
the change log will only get updated after first release - for now please use the
|
||||
`commit log <https://github.com/letsencrypt/lets-encrypt-preview/commits/master>`_.
|
||||
`commit log <https://github.com/letsencrypt/letsencrypt/commits/master>`_.
|
||||
|
||||
|
||||
Release 0.1.0 (not released yet)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# https://github.com/letsencrypt/lets-encrypt-preview/pull/431#issuecomment-103659297
|
||||
# https://github.com/letsencrypt/letsencrypt/pull/431#issuecomment-103659297
|
||||
# it is more likely developers will already have ubuntu:trusty rather
|
||||
# than e.g. debian:jessie and image size differences are negligible
|
||||
FROM ubuntu:trusty
|
||||
|
|
@ -48,6 +48,7 @@ COPY letsencrypt_apache /opt/letsencrypt/src/letsencrypt_apache/
|
|||
COPY letsencrypt_nginx /opt/letsencrypt/src/letsencrypt_nginx/
|
||||
|
||||
|
||||
# requirements.txt not installed!
|
||||
RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
|
||||
/opt/letsencrypt/venv/bin/pip install -e /opt/letsencrypt/src
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Let's Encrypt Preview:
|
||||
Let's Encrypt:
|
||||
Copyright (c) Internet Security Research Group
|
||||
Licensed Apache Version 2.0
|
||||
|
||||
|
|
|
|||
31
README.rst
31
README.rst
|
|
@ -1,3 +1,13 @@
|
|||
.. notice for github users
|
||||
|
||||
Official **documentation**, including `installation instructions`_, is
|
||||
available at https://letsencrypt.readthedocs.org.
|
||||
|
||||
Generic information about Let's Encrypt project can be found at
|
||||
https://letsencrypt.org. Please read `Frequently Asked Questions (FAQ)
|
||||
<https://letsencrypt.org/faq/>`_.
|
||||
|
||||
|
||||
About the Let's Encrypt Client
|
||||
==============================
|
||||
|
||||
|
|
@ -31,22 +41,25 @@ server automatically!::
|
|||
**Encrypt ALL the things!**
|
||||
|
||||
|
||||
.. |build-status| image:: https://travis-ci.org/letsencrypt/lets-encrypt-preview.svg?branch=master
|
||||
:target: https://travis-ci.org/letsencrypt/lets-encrypt-preview
|
||||
.. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master
|
||||
:target: https://travis-ci.org/letsencrypt/letsencrypt
|
||||
:alt: Travis CI status
|
||||
|
||||
.. |coverage| image:: https://coveralls.io/repos/letsencrypt/lets-encrypt-preview/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/letsencrypt/lets-encrypt-preview
|
||||
.. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master
|
||||
:target: https://coveralls.io/r/letsencrypt/letsencrypt
|
||||
:alt: Coverage status
|
||||
|
||||
.. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/
|
||||
:target: https://readthedocs.org/projects/letsencrypt/
|
||||
:alt: Documentation status
|
||||
|
||||
.. |container| image:: https://quay.io/repository/letsencrypt/lets-encrypt-preview/status
|
||||
:target: https://quay.io/repository/letsencrypt/lets-encrypt-preview
|
||||
.. |container| image:: https://quay.io/repository/letsencrypt/letsencrypt/status
|
||||
:target: https://quay.io/repository/letsencrypt/letsencrypt
|
||||
:alt: Docker Repository on Quay.io
|
||||
|
||||
.. _`installation instructions`:
|
||||
https://letsencrypt.readthedocs.org/en/latest/using.html
|
||||
|
||||
.. _watch demo video: https://www.youtube.com/watch?v=Gas_sSB-5SU
|
||||
|
||||
|
||||
|
|
@ -85,9 +98,9 @@ Current Features
|
|||
Links
|
||||
-----
|
||||
|
||||
Documentation: https://letsencrypt.readthedocs.org/
|
||||
Documentation: https://letsencrypt.readthedocs.org
|
||||
|
||||
Software project: https://github.com/letsencrypt/lets-encrypt-preview
|
||||
Software project: https://github.com/letsencrypt/letsencrypt
|
||||
|
||||
Notes for developers: CONTRIBUTING.md_
|
||||
|
||||
|
|
@ -100,4 +113,4 @@ email to client-dev+subscribe@letsencrypt.org)
|
|||
|
||||
.. _Freenode: https://freenode.net
|
||||
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
|
||||
.. _CONTRIBUTING.md: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/CONTRIBUTING.md
|
||||
.. _CONTRIBUTING.md: https://github.com/letsencrypt/letsencrypt/blob/master/CONTRIBUTING.md
|
||||
|
|
|
|||
2
Vagrantfile
vendored
2
Vagrantfile
vendored
|
|
@ -8,8 +8,6 @@ VAGRANTFILE_API_VERSION = "2"
|
|||
$ubuntu_setup_script = <<SETUP_SCRIPT
|
||||
cd /vagrant
|
||||
sudo ./bootstrap/ubuntu.sh
|
||||
sudo apt-get -y --no-install-recommends install git-core
|
||||
# the above is required by the 'git+https' lines of requirements.txt
|
||||
if [ ! -d "venv" ]; then
|
||||
virtualenv --no-site-packages -p python2 venv
|
||||
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ class SimpleHTTP(DVChallenge):
|
|||
"""ACME "simpleHttp" challenge."""
|
||||
typ = "simpleHttp"
|
||||
token = jose.Field("token")
|
||||
tls = jose.Field("tls", default=True, omitempty=True)
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
|
|
@ -54,20 +53,43 @@ class SimpleHTTPResponse(ChallengeResponse):
|
|||
"""ACME "simpleHttp" challenge response."""
|
||||
typ = "simpleHttp"
|
||||
path = jose.Field("path")
|
||||
tls = jose.Field("tls", default=True, omitempty=True)
|
||||
|
||||
URI_TEMPLATE = "https://{domain}/.well-known/acme-challenge/{path}"
|
||||
"""URI template for HTTPS server provisioned resource."""
|
||||
URI_ROOT_PATH = ".well-known/acme-challenge"
|
||||
"""URI root path for the server provisioned resource."""
|
||||
|
||||
_URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{path}"
|
||||
|
||||
MAX_PATH_LEN = 25
|
||||
"""Maximum allowed `path` length."""
|
||||
|
||||
@property
|
||||
def good_path(self):
|
||||
"""Is `path` good?
|
||||
|
||||
.. todo:: acme-spec: "The value MUST be comprised entirely of
|
||||
characters from the URL-safe alphabet for Base64 encoding
|
||||
[RFC4648]", base64.b64decode ignores those characters
|
||||
|
||||
"""
|
||||
return len(self.path) <= 25
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
"""URL scheme for the provisioned resource."""
|
||||
return "https" if self.tls else "http"
|
||||
|
||||
def uri(self, domain):
|
||||
"""Create an URI to the provisioned resource.
|
||||
|
||||
Forms an URI to the HTTPS server provisioned resource (containing
|
||||
:attr:`~SimpleHTTP.token`) by populating the :attr:`URI_TEMPLATE`.
|
||||
Forms an URI to the HTTPS server provisioned resource
|
||||
(containing :attr:`~SimpleHTTP.token`).
|
||||
|
||||
:param str domain: Domain name being verified.
|
||||
|
||||
"""
|
||||
return self.URI_TEMPLATE.format(domain=domain, path=self.path)
|
||||
return self._URI_TEMPLATE.format(
|
||||
scheme=self.scheme, domain=domain, path=self.path)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
|
|
|
|||
|
|
@ -18,6 +18,13 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
|||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
|
||||
|
||||
|
||||
class ChallengeResponseTest(unittest.TestCase):
|
||||
|
||||
def test_from_json_none(self):
|
||||
from acme.challenges import ChallengeResponse
|
||||
self.assertTrue(ChallengeResponse.from_json(None) is None)
|
||||
|
||||
|
||||
class SimpleHTTPTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
|
|
@ -27,17 +34,8 @@ class SimpleHTTPTest(unittest.TestCase):
|
|||
self.jmsg = {
|
||||
'type': 'simpleHttp',
|
||||
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA',
|
||||
'tls': True,
|
||||
}
|
||||
|
||||
def test_no_tls(self):
|
||||
from acme.challenges import SimpleHTTP
|
||||
self.assertEqual(SimpleHTTP(token='tok', tls=False).to_json(), {
|
||||
'tls': False,
|
||||
'token': 'tok',
|
||||
'type': 'simpleHttp',
|
||||
})
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
|
|
@ -54,27 +52,51 @@ class SimpleHTTPResponseTest(unittest.TestCase):
|
|||
|
||||
def setUp(self):
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
self.msg = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA')
|
||||
self.jmsg = {
|
||||
self.msg_http = SimpleHTTPResponse(
|
||||
path='6tbIMBC5Anhl5bOlWT5ZFA', tls=False)
|
||||
self.msg_https = SimpleHTTPResponse(path='6tbIMBC5Anhl5bOlWT5ZFA')
|
||||
self.jmsg_http = {
|
||||
'type': 'simpleHttp',
|
||||
'path': '6tbIMBC5Anhl5bOlWT5ZFA',
|
||||
'tls': False,
|
||||
}
|
||||
self.jmsg_https = {
|
||||
'type': 'simpleHttp',
|
||||
'path': '6tbIMBC5Anhl5bOlWT5ZFA',
|
||||
'tls': True,
|
||||
}
|
||||
|
||||
def test_good_path(self):
|
||||
self.assertTrue(self.msg_http.good_path)
|
||||
self.assertTrue(self.msg_https.good_path)
|
||||
self.assertFalse(
|
||||
self.msg_http.update(path=(self.msg_http.path * 10)).good_path)
|
||||
|
||||
def test_scheme(self):
|
||||
self.assertEqual('http', self.msg_http.scheme)
|
||||
self.assertEqual('https', self.msg_https.scheme)
|
||||
|
||||
def test_uri(self):
|
||||
self.assertEqual('http://example.com/.well-known/acme-challenge/'
|
||||
'6tbIMBC5Anhl5bOlWT5ZFA', self.msg_http.uri('example.com'))
|
||||
self.assertEqual('https://example.com/.well-known/acme-challenge/'
|
||||
'6tbIMBC5Anhl5bOlWT5ZFA', self.msg.uri('example.com'))
|
||||
'6tbIMBC5Anhl5bOlWT5ZFA', self.msg_https.uri('example.com'))
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
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, SimpleHTTPResponse.from_json(self.jmsg))
|
||||
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))
|
||||
hash(SimpleHTTPResponse.from_json(self.jmsg_http))
|
||||
hash(SimpleHTTPResponse.from_json(self.jmsg_https))
|
||||
|
||||
|
||||
class DVSNITest(unittest.TestCase):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Networking for ACME protocol v02."""
|
||||
"""ACME client API."""
|
||||
import datetime
|
||||
import heapq
|
||||
import httplib
|
||||
|
|
@ -9,23 +9,22 @@ import M2Crypto
|
|||
import requests
|
||||
import werkzeug
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import jws as acme_jws
|
||||
from acme import messages2
|
||||
|
||||
from letsencrypt import errors
|
||||
from acme import jws
|
||||
from acme import messages
|
||||
|
||||
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
|
||||
|
||||
class Network(object):
|
||||
"""ACME networking.
|
||||
class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
"""ACME client.
|
||||
|
||||
.. todo::
|
||||
Clean up raised error types hierarchy, document, and handle (wrap)
|
||||
instances of `.DeserializationError` raised in `from_json()``.
|
||||
instances of `.DeserializationError` raised in `from_json()`.
|
||||
|
||||
:ivar str new_reg_uri: Location of new-reg
|
||||
:ivar key: `.JWK` (private)
|
||||
|
|
@ -33,8 +32,6 @@ class Network(object):
|
|||
:ivar bool verify_ssl: Verify SSL certificates?
|
||||
|
||||
"""
|
||||
|
||||
# TODO: Move below to acme module?
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
|
||||
|
|
@ -58,7 +55,7 @@ class Network(object):
|
|||
"""
|
||||
dumps = obj.json_dumps()
|
||||
logging.debug('Serialized JSON: %s', dumps)
|
||||
return acme_jws.JWS.sign(
|
||||
return jws.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
|
||||
|
||||
@classmethod
|
||||
|
|
@ -75,12 +72,14 @@ class Network(object):
|
|||
function will raise an error. Otherwise, wrong Content-Type
|
||||
is ignored, but logged.
|
||||
|
||||
:raises letsencrypt.messages2.Error: If server response body
|
||||
:raises .messages.Error: If server response body
|
||||
carries HTTP Problem (draft-ietf-appsawg-http-problem-00).
|
||||
:raises letsencrypt.errors.NetworkError: In case of other
|
||||
networking errors.
|
||||
:raises .ClientError: In case of other networking errors.
|
||||
|
||||
"""
|
||||
logging.debug('Received response %s (headers: %s): %r',
|
||||
response, response.headers, response.content)
|
||||
|
||||
response_ct = response.headers.get('Content-Type')
|
||||
try:
|
||||
# TODO: response.json() is called twice, once here, and
|
||||
|
|
@ -96,15 +95,13 @@ class Network(object):
|
|||
'Ignoring wrong Content-Type (%r) for JSON Error',
|
||||
response_ct)
|
||||
try:
|
||||
logging.error("Error: %s", jobj)
|
||||
logging.error("Response from server: %s", response.content)
|
||||
raise messages2.Error.from_json(jobj)
|
||||
raise messages.Error.from_json(jobj)
|
||||
except jose.DeserializationError as error:
|
||||
# Couldn't deserialize JSON object
|
||||
raise errors.NetworkError((response, error))
|
||||
raise errors.ClientError((response, error))
|
||||
else:
|
||||
# response is not JSON object
|
||||
raise errors.NetworkError(response)
|
||||
raise errors.ClientError(response)
|
||||
else:
|
||||
if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE:
|
||||
logging.debug(
|
||||
|
|
@ -112,13 +109,13 @@ class Network(object):
|
|||
'response', response_ct)
|
||||
|
||||
if content_type == cls.JSON_CONTENT_TYPE and jobj is None:
|
||||
raise errors.NetworkError(
|
||||
raise errors.ClientError(
|
||||
'Unexpected response Content-Type: {0}'.format(response_ct))
|
||||
|
||||
def _get(self, uri, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
"""Send GET request.
|
||||
|
||||
:raises letsencrypt.errors.NetworkError:
|
||||
:raises .ClientError:
|
||||
|
||||
:returns: HTTP Response
|
||||
:rtype: `requests.Response`
|
||||
|
|
@ -129,22 +126,22 @@ class Network(object):
|
|||
try:
|
||||
response = requests.get(uri, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise errors.NetworkError(error)
|
||||
raise errors.ClientError(error)
|
||||
self._check_response(response, content_type=content_type)
|
||||
return response
|
||||
|
||||
def _add_nonce(self, response):
|
||||
if self.REPLAY_NONCE_HEADER in response.headers:
|
||||
nonce = response.headers[self.REPLAY_NONCE_HEADER]
|
||||
error = acme_jws.Header.validate_nonce(nonce)
|
||||
error = jws.Header.validate_nonce(nonce)
|
||||
if error is None:
|
||||
logging.debug('Storing nonce: %r', nonce)
|
||||
self._nonces.add(nonce)
|
||||
else:
|
||||
raise errors.NetworkError('Invalid nonce ({0}): {1}'.format(
|
||||
raise errors.ClientError('Invalid nonce ({0}): {1}'.format(
|
||||
nonce, error))
|
||||
else:
|
||||
raise errors.NetworkError(
|
||||
raise errors.ClientError(
|
||||
'Server {0} response did not include a replay nonce'.format(
|
||||
response.request.method))
|
||||
|
||||
|
|
@ -160,7 +157,7 @@ class Network(object):
|
|||
:param JSONDeSerializable obj: Will be wrapped in JWS.
|
||||
:param str content_type: Expected ``Content-Type``, fails if not set.
|
||||
|
||||
:raises acme.messages2.NetworkError:
|
||||
:raises acme.messages.ClientError:
|
||||
|
||||
:returns: HTTP Response
|
||||
:rtype: `requests.Response`
|
||||
|
|
@ -172,8 +169,7 @@ class Network(object):
|
|||
try:
|
||||
response = requests.post(uri, data=data, **kwargs)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise errors.NetworkError(error)
|
||||
logging.debug('Received response %s: %r', response, response.text)
|
||||
raise errors.ClientError(error)
|
||||
|
||||
self._add_nonce(response)
|
||||
self._check_response(response, content_type=content_type)
|
||||
|
|
@ -190,15 +186,15 @@ class Network(object):
|
|||
try:
|
||||
new_authzr_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"next" link missing')
|
||||
raise errors.ClientError('"next" link missing')
|
||||
|
||||
return messages2.RegistrationResource(
|
||||
body=messages2.Registration.from_json(response.json()),
|
||||
return messages.RegistrationResource(
|
||||
body=messages.Registration.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_authzr_uri=new_authzr_uri,
|
||||
terms_of_service=terms_of_service)
|
||||
|
||||
def register(self, contact=messages2.Registration._fields[
|
||||
def register(self, contact=messages.Registration._fields[
|
||||
'contact'].default):
|
||||
"""Register.
|
||||
|
||||
|
|
@ -208,10 +204,10 @@ class Network(object):
|
|||
:returns: Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
:raises letsencrypt.errors.UnexpectedUpdate:
|
||||
:raises .UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
new_reg = messages2.Registration(contact=contact)
|
||||
new_reg = messages.Registration(contact=contact)
|
||||
|
||||
response = self._post(self.new_reg_uri, new_reg)
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
|
|
@ -222,24 +218,6 @@ class Network(object):
|
|||
|
||||
return regr
|
||||
|
||||
def register_from_account(self, account):
|
||||
"""Register with server.
|
||||
|
||||
:param account: Account
|
||||
:type account: :class:`letsencrypt.account.Account`
|
||||
|
||||
:returns: Updated account
|
||||
:rtype: :class:`letsencrypt.account.Account`
|
||||
|
||||
"""
|
||||
details = (
|
||||
"mailto:" + account.email if account.email is not None else None,
|
||||
"tel:" + account.phone if account.phone is not None else None,
|
||||
)
|
||||
account.regr = self.register(contact=tuple(
|
||||
det for det in details if det is not None))
|
||||
return account
|
||||
|
||||
def update_registration(self, regr):
|
||||
"""Update registration.
|
||||
|
||||
|
|
@ -262,7 +240,6 @@ class Network(object):
|
|||
response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri,
|
||||
terms_of_service=regr.terms_of_service)
|
||||
if updated_regr != regr:
|
||||
# TODO: Boulder reregisters with new recoveryToken and new URI
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
return updated_regr
|
||||
|
||||
|
|
@ -288,10 +265,10 @@ class Network(object):
|
|||
try:
|
||||
new_cert_uri = response.links['next']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"next" link missing')
|
||||
raise errors.ClientError('"next" link missing')
|
||||
|
||||
authzr = messages2.AuthorizationResource(
|
||||
body=messages2.Authorization.from_json(response.json()),
|
||||
authzr = messages.AuthorizationResource(
|
||||
body=messages.Authorization.from_json(response.json()),
|
||||
uri=response.headers.get('Location', uri),
|
||||
new_cert_uri=new_cert_uri)
|
||||
if authzr.body.identifier != identifier:
|
||||
|
|
@ -302,7 +279,7 @@ class Network(object):
|
|||
"""Request challenges.
|
||||
|
||||
:param identifier: Identifier to be challenged.
|
||||
:type identifier: `.messages2.Identifier`
|
||||
:type identifier: `.messages.Identifier`
|
||||
|
||||
:param str new_authzr_uri: new-authorization URI
|
||||
|
||||
|
|
@ -310,7 +287,7 @@ class Network(object):
|
|||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages2.Authorization(identifier=identifier)
|
||||
new_authz = messages.Authorization(identifier=identifier)
|
||||
response = self._post(new_authzr_uri, new_authz)
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
return self._authzr_from_response(response, identifier)
|
||||
|
|
@ -329,8 +306,8 @@ class Network(object):
|
|||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
return self.request_challenges(messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain), new_authz_uri)
|
||||
return self.request_challenges(messages.Identifier(
|
||||
typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri)
|
||||
|
||||
def answer_challenge(self, challb, response):
|
||||
"""Answer challenge.
|
||||
|
|
@ -344,17 +321,17 @@ class Network(object):
|
|||
:returns: Challenge Resource with updated body.
|
||||
:rtype: `.ChallengeResource`
|
||||
|
||||
:raises errors.UnexpectedUpdate:
|
||||
:raises .UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
response = self._post(challb.uri, response)
|
||||
try:
|
||||
authzr_uri = response.links['up']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"up" Link header missing')
|
||||
challr = messages2.ChallengeResource(
|
||||
raise errors.ClientError('"up" Link header missing')
|
||||
challr = messages.ChallengeResource(
|
||||
authzr_uri=authzr_uri,
|
||||
body=messages2.ChallengeBody.from_json(response.json()))
|
||||
body=messages.ChallengeBody.from_json(response.json()))
|
||||
# TODO: check that challr.uri == response.headers['Location']?
|
||||
if challr.uri != challb.uri:
|
||||
raise errors.UnexpectedUpdate(challr.uri)
|
||||
|
|
@ -413,14 +390,14 @@ class Network(object):
|
|||
:param authzrs: `list` of `.AuthorizationResource`
|
||||
|
||||
:returns: Issued certificate
|
||||
:rtype: `.messages2.CertificateResource`
|
||||
:rtype: `.messages.CertificateResource`
|
||||
|
||||
"""
|
||||
assert authzrs, "Authorizations list is empty"
|
||||
logging.debug("Requesting issuance...")
|
||||
|
||||
# TODO: assert len(authzrs) == number of SANs
|
||||
req = messages2.CertificateRequest(
|
||||
req = messages.CertificateRequest(
|
||||
csr=csr, authorizations=tuple(authzr.uri for authzr in authzrs))
|
||||
|
||||
content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument
|
||||
|
|
@ -435,9 +412,9 @@ class Network(object):
|
|||
try:
|
||||
uri = response.headers['Location']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"Location" Header missing')
|
||||
raise errors.ClientError('"Location" Header missing')
|
||||
|
||||
return messages2.CertificateResource(
|
||||
return messages.CertificateResource(
|
||||
uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri,
|
||||
body=jose.ComparableX509(
|
||||
M2Crypto.X509.load_cert_der_string(response.content)))
|
||||
|
|
@ -460,7 +437,7 @@ class Network(object):
|
|||
``Retry-After`` is not present in the response.
|
||||
|
||||
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
|
||||
the issued certificate (`.messages2.CertificateResource.),
|
||||
the issued certificate (`.messages.CertificateResource.),
|
||||
and ``updated_authzrs`` is a `tuple` consisting of updated
|
||||
Authorization Resources (`.AuthorizationResource`) as
|
||||
present in the responses from server, and in the same order
|
||||
|
|
@ -489,7 +466,8 @@ class Network(object):
|
|||
updated_authzr, response = self.poll(updated[authzr])
|
||||
updated[authzr] = updated_authzr
|
||||
|
||||
if updated_authzr.body.status != messages2.STATUS_VALID:
|
||||
# pylint: disable=no-member
|
||||
if updated_authzr.body.status != messages.STATUS_VALID:
|
||||
# push back to the priority queue, with updated retry_after
|
||||
heapq.heappush(waiting, (self.retry_after(
|
||||
response, default=mintime), authzr))
|
||||
|
|
@ -527,7 +505,7 @@ class Network(object):
|
|||
# "refresh cert", and this method integrated with self.refresh
|
||||
response, cert = self._get_cert(certr.uri)
|
||||
if 'Location' not in response.headers:
|
||||
raise errors.NetworkError('Location header missing')
|
||||
raise errors.ClientError('Location header missing')
|
||||
if response.headers['Location'] != certr.uri:
|
||||
raise errors.UnexpectedUpdate(response.text)
|
||||
return certr.update(body=cert)
|
||||
|
|
@ -562,22 +540,17 @@ class Network(object):
|
|||
else:
|
||||
return None
|
||||
|
||||
def revoke(self, certr, when=messages2.Revocation.NOW):
|
||||
def revoke(self, cert):
|
||||
"""Revoke certificate.
|
||||
|
||||
:param certr: Certificate Resource
|
||||
:type certr: `.CertificateResource`
|
||||
:param .ComparableX509 cert: `M2Crypto.X509.X509` wrapped in
|
||||
`.ComparableX509`
|
||||
|
||||
:param when: When should the revocation take place? Takes
|
||||
the same values as `.messages2.Revocation.revoke`.
|
||||
|
||||
:raises letsencrypt.errors.NetworkError: If revocation is
|
||||
unsuccessful.
|
||||
:raises .ClientError: If revocation is unsuccessful.
|
||||
|
||||
"""
|
||||
rev = messages2.Revocation(revoke=when, authorizations=tuple(
|
||||
authzr.uri for authzr in certr.authzrs))
|
||||
response = self._post(certr.uri, rev)
|
||||
response = self._post(messages.Revocation.url(self.new_reg_uri),
|
||||
messages.Revocation(certificate=cert))
|
||||
if response.status_code != httplib.OK:
|
||||
raise errors.NetworkError(
|
||||
raise errors.ClientError(
|
||||
'Successful revocation must return HTTP OK status')
|
||||
|
|
@ -1,42 +1,29 @@
|
|||
"""Tests for letsencrypt.network2."""
|
||||
"""Tests for acme.client."""
|
||||
import datetime
|
||||
import httplib
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import M2Crypto
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import jws as acme_jws
|
||||
from acme import messages2
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import errors
|
||||
from acme import messages
|
||||
from acme import messages_test
|
||||
|
||||
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'cert.pem'))))
|
||||
CERT2 = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'cert-san.pem'))))
|
||||
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
|
||||
pkg_resources.resource_string(
|
||||
__name__, os.path.join('testdata', 'csr.pem'))))
|
||||
KEY = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
|
||||
KEY2 = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa256_key.pem')))
|
||||
|
||||
|
||||
class NetworkTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.network2.Network."""
|
||||
class ClientTest(unittest.TestCase):
|
||||
"""Tests for acme.client.Client."""
|
||||
|
||||
# pylint: disable=too-many-instance-attributes,too-many-public-methods
|
||||
|
||||
|
|
@ -44,8 +31,8 @@ class NetworkTest(unittest.TestCase):
|
|||
self.verify_ssl = mock.MagicMock()
|
||||
self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped)
|
||||
|
||||
from letsencrypt.network2 import Network
|
||||
self.net = Network(
|
||||
from acme.client import Client
|
||||
self.net = Client(
|
||||
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl)
|
||||
self.nonce = jose.b64encode('Nonce')
|
||||
|
|
@ -58,44 +45,39 @@ class NetworkTest(unittest.TestCase):
|
|||
self.post = mock.MagicMock(return_value=self.response)
|
||||
self.get = mock.MagicMock(return_value=self.response)
|
||||
|
||||
self.identifier = messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com')
|
||||
|
||||
self.config = mock.Mock(accounts_dir=tempfile.mkdtemp())
|
||||
self.identifier = messages.Identifier(
|
||||
typ=messages.IDENTIFIER_FQDN, value='example.com')
|
||||
|
||||
# Registration
|
||||
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
|
||||
reg = messages2.Registration(
|
||||
reg = messages.Registration(
|
||||
contact=self.contact, key=KEY.public(), recovery_token='t')
|
||||
self.regr = messages2.RegistrationResource(
|
||||
self.regr = messages.RegistrationResource(
|
||||
body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1',
|
||||
new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg',
|
||||
terms_of_service='https://www.letsencrypt-demo.org/tos')
|
||||
|
||||
# Authorization
|
||||
authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1'
|
||||
challb = messages2.ChallengeBody(
|
||||
uri=(authzr_uri + '/1'), status=messages2.STATUS_VALID,
|
||||
challb = messages.ChallengeBody(
|
||||
uri=(authzr_uri + '/1'), status=messages.STATUS_VALID,
|
||||
chall=challenges.DNS(token='foo'))
|
||||
self.challr = messages2.ChallengeResource(
|
||||
self.challr = messages.ChallengeResource(
|
||||
body=challb, authzr_uri=authzr_uri)
|
||||
self.authz = messages2.Authorization(
|
||||
identifier=messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com'),
|
||||
self.authz = messages.Authorization(
|
||||
identifier=messages.Identifier(
|
||||
typ=messages.IDENTIFIER_FQDN, value='example.com'),
|
||||
challenges=(challb,), combinations=None)
|
||||
self.authzr = messages2.AuthorizationResource(
|
||||
self.authzr = messages.AuthorizationResource(
|
||||
body=self.authz, uri=authzr_uri,
|
||||
new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert')
|
||||
|
||||
# Request issuance
|
||||
self.certr = messages2.CertificateResource(
|
||||
body=CERT, authzrs=(self.authzr,),
|
||||
self.certr = messages.CertificateResource(
|
||||
body=messages_test.CERT, authzrs=(self.authzr,),
|
||||
uri='https://www.letsencrypt-demo.org/acme/cert/1',
|
||||
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.config.accounts_dir)
|
||||
|
||||
def _mock_post_get(self):
|
||||
# pylint: disable=protected-access
|
||||
self.net._post = self.post
|
||||
|
|
@ -127,22 +109,22 @@ class NetworkTest(unittest.TestCase):
|
|||
self.response.json.return_value = {}
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response)
|
||||
errors.ClientError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_not_ok_jobj_error(self):
|
||||
self.response.ok = False
|
||||
self.response.json.return_value = messages2.Error(
|
||||
self.response.json.return_value = messages.Error(
|
||||
detail='foo', typ='serverInternal', title='some title').to_json()
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
messages2.Error, self.net._check_response, self.response)
|
||||
messages.Error, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_not_ok_no_jobj(self):
|
||||
self.response.ok = False
|
||||
self.response.json.side_effect = ValueError
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response)
|
||||
errors.ClientError, self.net._check_response, self.response)
|
||||
|
||||
def test_check_response_ok_no_jobj_ct_required(self):
|
||||
self.response.json.side_effect = ValueError
|
||||
|
|
@ -150,7 +132,7 @@ class NetworkTest(unittest.TestCase):
|
|||
self.response.headers['Content-Type'] = response_ct
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._check_response, self.response,
|
||||
errors.ClientError, self.net._check_response, self.response,
|
||||
content_type=self.net.JSON_CONTENT_TYPE)
|
||||
|
||||
def test_check_response_ok_no_jobj_no_ct(self):
|
||||
|
|
@ -167,14 +149,14 @@ class NetworkTest(unittest.TestCase):
|
|||
# pylint: disable=protected-access
|
||||
self.net._check_response(self.response)
|
||||
|
||||
@mock.patch('letsencrypt.network2.requests')
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_get_requests_error_passthrough(self, requests_mock):
|
||||
requests_mock.exceptions = requests.exceptions
|
||||
requests_mock.get.side_effect = requests.exceptions.RequestException
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.NetworkError, self.net._get, 'uri')
|
||||
self.assertRaises(errors.ClientError, self.net._get, 'uri')
|
||||
|
||||
@mock.patch('letsencrypt.network2.requests')
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_get(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
|
|
@ -186,16 +168,16 @@ class NetworkTest(unittest.TestCase):
|
|||
# pylint: disable=protected-access
|
||||
self.net._wrap_in_jws = self.wrap_in_jws
|
||||
|
||||
@mock.patch('letsencrypt.network2.requests')
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_post_requests_error_passthrough(self, requests_mock):
|
||||
requests_mock.exceptions = requests.exceptions
|
||||
requests_mock.post.side_effect = requests.exceptions.RequestException
|
||||
# pylint: disable=protected-access
|
||||
self._mock_wrap_in_jws()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
|
||||
@mock.patch('letsencrypt.network2.requests')
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_post(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
|
|
@ -206,7 +188,7 @@ class NetworkTest(unittest.TestCase):
|
|||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct')
|
||||
|
||||
@mock.patch('letsencrypt.network2.requests')
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_post_replay_nonce_handling(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self.net._check_response = mock.MagicMock()
|
||||
|
|
@ -214,7 +196,7 @@ class NetworkTest(unittest.TestCase):
|
|||
|
||||
self.net._nonces.clear()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
|
||||
nonce2 = jose.b64encode('Nonce2')
|
||||
requests_mock.head('uri').headers = {
|
||||
|
|
@ -231,9 +213,9 @@ class NetworkTest(unittest.TestCase):
|
|||
# wrong nonce
|
||||
requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'}
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
|
||||
@mock.patch('letsencrypt.client.network2.requests')
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_get_post_verify_ssl(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self._mock_wrap_in_jws()
|
||||
|
|
@ -274,30 +256,7 @@ class NetworkTest(unittest.TestCase):
|
|||
self.response.status_code = httplib.CREATED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.register, self.regr.body)
|
||||
|
||||
def test_register_from_account(self):
|
||||
self.net.register = mock.Mock()
|
||||
acc = account.Account(
|
||||
self.config, 'key', email='cert-admin@example.com',
|
||||
phone='+12025551212')
|
||||
|
||||
self.net.register_from_account(acc)
|
||||
|
||||
self.net.register.assert_called_with(contact=self.contact)
|
||||
|
||||
def test_register_from_account_partial_info(self):
|
||||
self.net.register = mock.Mock()
|
||||
acc = account.Account(
|
||||
self.config, 'key', email='cert-admin@example.com')
|
||||
acc2 = account.Account(self.config, 'key')
|
||||
|
||||
self.net.register_from_account(acc)
|
||||
self.net.register.assert_called_with(
|
||||
contact=('mailto:cert-admin@example.com',))
|
||||
|
||||
self.net.register_from_account(acc2)
|
||||
self.net.register.assert_called_with(contact=())
|
||||
errors.ClientError, self.net.register, self.regr.body)
|
||||
|
||||
def test_update_registration(self):
|
||||
self.response.headers['Location'] = self.regr.uri
|
||||
|
|
@ -339,7 +298,7 @@ class NetworkTest(unittest.TestCase):
|
|||
self.response.status_code = httplib.CREATED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.request_challenges,
|
||||
errors.ClientError, self.net.request_challenges,
|
||||
self.identifier, self.regr)
|
||||
|
||||
def test_request_domain_challenges(self):
|
||||
|
|
@ -363,7 +322,7 @@ class NetworkTest(unittest.TestCase):
|
|||
|
||||
def test_answer_challenge_missing_next(self):
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.answer_challenge,
|
||||
self.assertRaises(errors.ClientError, self.net.answer_challenge,
|
||||
self.challr.body, challenges.DNSResponse())
|
||||
|
||||
def test_retry_after_date(self):
|
||||
|
|
@ -372,7 +331,7 @@ class NetworkTest(unittest.TestCase):
|
|||
datetime.datetime(1999, 12, 31, 23, 59, 59),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.network2.datetime')
|
||||
@mock.patch('acme.client.datetime')
|
||||
def test_retry_after_invalid(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
|
@ -382,7 +341,7 @@ class NetworkTest(unittest.TestCase):
|
|||
datetime.datetime(2015, 3, 27, 0, 0, 10),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.network2.datetime')
|
||||
@mock.patch('acme.client.datetime')
|
||||
def test_retry_after_seconds(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
|
@ -392,7 +351,7 @@ class NetworkTest(unittest.TestCase):
|
|||
datetime.datetime(2015, 3, 27, 0, 0, 50),
|
||||
self.net.retry_after(response=self.response, default=10))
|
||||
|
||||
@mock.patch('letsencrypt.network2.datetime')
|
||||
@mock.patch('acme.client.datetime')
|
||||
def test_retry_after_missing(self, dt_mock):
|
||||
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
|
||||
dt_mock.timedelta = datetime.timedelta
|
||||
|
|
@ -413,30 +372,30 @@ class NetworkTest(unittest.TestCase):
|
|||
self.assertRaises(errors.UnexpectedUpdate, self.net.poll, self.authzr)
|
||||
|
||||
def test_request_issuance(self):
|
||||
self.response.content = CERT.as_der()
|
||||
self.response.content = messages_test.CERT.as_der()
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self.response.links['up'] = {'url': self.certr.cert_chain_uri}
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr, self.net.request_issuance(CSR, (self.authzr,)))
|
||||
self.assertEqual(self.certr, self.net.request_issuance(
|
||||
messages_test.CSR, (self.authzr,)))
|
||||
# TODO: check POST args
|
||||
|
||||
def test_request_issuance_missing_up(self):
|
||||
self.response.content = CERT.as_der()
|
||||
self.response.content = messages_test.CERT.as_der()
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr.update(cert_chain_uri=None),
|
||||
self.net.request_issuance(CSR, (self.authzr,)))
|
||||
self.net.request_issuance(messages_test.CSR, (self.authzr,)))
|
||||
|
||||
def test_request_issuance_missing_location(self):
|
||||
self._mock_post_get()
|
||||
self.assertRaises(
|
||||
errors.NetworkError, self.net.request_issuance,
|
||||
CSR, (self.authzr,))
|
||||
errors.ClientError, self.net.request_issuance,
|
||||
messages_test.CSR, (self.authzr,))
|
||||
|
||||
@mock.patch('letsencrypt.network2.datetime')
|
||||
@mock.patch('letsencrypt.network2.time')
|
||||
@mock.patch('acme.client.datetime')
|
||||
@mock.patch('acme.client.time')
|
||||
def test_poll_and_request_issuance(self, time_mock, dt_mock):
|
||||
# clock.dt | pylint: disable=no-member
|
||||
clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27))
|
||||
|
|
@ -462,7 +421,7 @@ class NetworkTest(unittest.TestCase):
|
|||
|
||||
if not authzr.retries: # no more retries
|
||||
done = mock.MagicMock(uri=authzr.uri, times=authzr.times)
|
||||
done.body.status = messages2.STATUS_VALID
|
||||
done.body.status = messages.STATUS_VALID
|
||||
return done, []
|
||||
|
||||
# response (2nd result tuple element) is reduced to only
|
||||
|
|
@ -517,10 +476,10 @@ class NetworkTest(unittest.TestCase):
|
|||
|
||||
def test_check_cert(self):
|
||||
self.response.headers['Location'] = self.certr.uri
|
||||
self.response.content = CERT2.as_der()
|
||||
self.response.content = messages_test.CERT.as_der()
|
||||
self._mock_post_get()
|
||||
self.assertEqual(
|
||||
self.certr.update(body=CERT2), self.net.check_cert(self.certr))
|
||||
self.assertEqual(self.certr.update(body=messages_test.CERT),
|
||||
self.net.check_cert(self.certr))
|
||||
|
||||
# TODO: split here and separate test
|
||||
self.response.headers['Location'] = 'foo'
|
||||
|
|
@ -528,9 +487,9 @@ class NetworkTest(unittest.TestCase):
|
|||
errors.UnexpectedUpdate, self.net.check_cert, self.certr)
|
||||
|
||||
def test_check_cert_missing_location(self):
|
||||
self.response.content = CERT2.as_der()
|
||||
self.response.content = messages_test.CERT.as_der()
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.check_cert, self.certr)
|
||||
self.assertRaises(errors.ClientError, self.net.check_cert, self.certr)
|
||||
|
||||
def test_refresh(self):
|
||||
self.net.check_cert = mock.MagicMock()
|
||||
|
|
@ -550,13 +509,14 @@ class NetworkTest(unittest.TestCase):
|
|||
|
||||
def test_revoke(self):
|
||||
self._mock_post_get()
|
||||
self.net.revoke(self.certr, when=messages2.Revocation.NOW)
|
||||
self.post.assert_called_once_with(self.certr.uri, mock.ANY)
|
||||
self.net.revoke(self.certr.body)
|
||||
self.post.assert_called_once_with(messages.Revocation.url(
|
||||
self.net.new_reg_uri), mock.ANY)
|
||||
|
||||
def test_revoke_bad_status_raises_error(self):
|
||||
self.response.status_code = httplib.METHOD_NOT_ALLOWED
|
||||
self._mock_post_get()
|
||||
self.assertRaises(errors.NetworkError, self.net.revoke, self.certr)
|
||||
self.assertRaises(errors.ClientError, self.net.revoke, self.certr)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
@ -1,8 +1,15 @@
|
|||
"""ACME errors."""
|
||||
from acme.jose import errors as jose_errors
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Generic ACME error."""
|
||||
|
||||
class SchemaValidationError(jose_errors.DeserializationError):
|
||||
"""JSON schema ACME object validation error."""
|
||||
|
||||
class ClientError(Error):
|
||||
"""Network error."""
|
||||
|
||||
class UnexpectedUpdate(ClientError):
|
||||
"""Unexpected update."""
|
||||
|
|
|
|||
|
|
@ -218,11 +218,12 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
def fields_to_partial_json(self):
|
||||
"""Serialize fields to JSON."""
|
||||
jobj = {}
|
||||
omitted = set()
|
||||
for slot, field in self._fields.iteritems():
|
||||
value = getattr(self, slot)
|
||||
|
||||
if field.omit(value):
|
||||
logging.debug('Omitting empty field "%s" (%s)', slot, value)
|
||||
omitted.add((slot, value))
|
||||
else:
|
||||
try:
|
||||
jobj[field.json_name] = field.encode(value)
|
||||
|
|
@ -230,6 +231,10 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
|||
raise errors.SerializationError(
|
||||
'Could not encode {0} ({1}): {2}'.format(
|
||||
slot, value, error))
|
||||
if omitted:
|
||||
# pylint: disable=star-args
|
||||
logging.debug('Omitted empty fields: %s', ', '.join(
|
||||
'{0!s}={1!r}'.format(*field) for field in omitted))
|
||||
return jobj
|
||||
|
||||
def to_partial_json(self):
|
||||
|
|
|
|||
9
acme/jose/testdata/README
vendored
9
acme/jose/testdata/README
vendored
|
|
@ -4,7 +4,8 @@ The following command has been used to generate test keys:
|
|||
|
||||
and for the CSR:
|
||||
|
||||
python -c from 'letsencrypt.crypto_util import make_csr;
|
||||
import pkg_resources; open("csr2.pem",
|
||||
"w").write(make_csr(pkg_resources.resource_string("letsencrypt.tests",
|
||||
"testdata/rsa512_key.pem"), ["example2.com"])[0])'
|
||||
openssl req -key rsa512_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
|
||||
|
|
|
|||
BIN
acme/jose/testdata/cert.der
vendored
Normal file
BIN
acme/jose/testdata/cert.der
vendored
Normal file
Binary file not shown.
BIN
acme/jose/testdata/csr.der
vendored
Normal file
BIN
acme/jose/testdata/csr.der
vendored
Normal file
Binary file not shown.
10
acme/jose/testdata/csr2.pem
vendored
10
acme/jose/testdata/csr2.pem
vendored
|
|
@ -1,10 +0,0 @@
|
|||
-----BEGIN CERTIFICATE REQUEST-----
|
||||
MIIBXzCCAQkCAQAwejELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
|
||||
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
|
||||
c2l0eSBvZiBNaWNoaWdhbjEVMBMGA1UEAwwMZXhhbXBsZTIuY29tMFwwDQYJKoZI
|
||||
hvcNAQEBBQADSwAwSAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNH
|
||||
tPXJmLlM8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAaAqMCgGCSqGSIb3
|
||||
DQEJDjEbMBkwFwYDVR0RBBAwDoIMZXhhbXBsZTIuY29tMA0GCSqGSIb3DQEBCwUA
|
||||
A0EAwsdL4FLIgISKV4vXFmc6QTV7CjBiP4XmPFbeN+gMFdR7QcnRyyxSpXxB0v8Z
|
||||
oqYboP5LGFt9zC6/9GyjcI9/IQ==
|
||||
-----END CERTIFICATE REQUEST-----
|
||||
632
acme/messages.py
632
acme/messages.py
|
|
@ -1,106 +1,294 @@
|
|||
"""ACME protocol v00 messages.
|
||||
|
||||
.. warning:: This module is an implementation of the draft `ACME
|
||||
protocol version 00`_, and not the "RESTified" `ACME protocol version
|
||||
01`_ or later. It should work with `older Node.js implementation`_,
|
||||
but will definitely not work with Boulder_. It is kept for reference
|
||||
purposes only.
|
||||
|
||||
|
||||
.. _`ACME protocol version 00`:
|
||||
https://github.com/letsencrypt/acme-spec/blob/v00/draft-barnes-acme.md
|
||||
|
||||
.. _`ACME protocol version 01`:
|
||||
https://github.com/letsencrypt/acme-spec/blob/v01/draft-barnes-acme.md
|
||||
|
||||
.. _Boulder: https://github.com/letsencrypt/boulder
|
||||
|
||||
.. _`older Node.js implementation`:
|
||||
https://github.com/letsencrypt/node-acme/commit/f42aa5b7fad4cd2fc289653c4ab14f18052367b3
|
||||
|
||||
|
||||
"""
|
||||
import jsonschema
|
||||
"""ACME protocol messages."""
|
||||
import urlparse
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors
|
||||
from acme import fields
|
||||
from acme import jose
|
||||
from acme import other
|
||||
from acme import util
|
||||
|
||||
|
||||
class Message(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_partial_json | pylint: disable=abstract-method
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""ACME message."""
|
||||
TYPES = {}
|
||||
type_field_name = "type"
|
||||
class Error(jose.JSONObjectWithFields, Exception):
|
||||
"""ACME error.
|
||||
|
||||
schema = NotImplemented
|
||||
"""JSON schema the object is tested against in :meth:`from_json`.
|
||||
|
||||
Subclasses must overrride it with a value that is acceptable by
|
||||
:func:`jsonschema.validate`, most probably using
|
||||
:func:`acme.util.load_schema`.
|
||||
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
|
||||
"""
|
||||
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
|
||||
ERROR_TYPE_DESCRIPTIONS = {
|
||||
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
|
||||
'badNonce': 'The client sent an unacceptable anti-replay nonce',
|
||||
'connection': 'The server could not connect to the client for DV',
|
||||
'dnssec': 'The server could not validate a DNSSEC signed domain',
|
||||
'malformed': 'The request message was malformed',
|
||||
'serverInternal': 'The server experienced an internal error',
|
||||
'tls': 'The server experienced a TLS error during DV',
|
||||
'unauthorized': 'The client lacks sufficient authorization',
|
||||
'unknownHost': 'The server could not resolve a domain name',
|
||||
}
|
||||
|
||||
typ = jose.Field('type')
|
||||
title = jose.Field('title', omitempty=True)
|
||||
detail = jose.Field('detail')
|
||||
|
||||
@typ.encoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return Error.ERROR_TYPE_NAMESPACE + value
|
||||
|
||||
@typ.decoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
# pylint thinks isinstance(value, Error), so startswith is not found
|
||||
# pylint: disable=no-member
|
||||
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
|
||||
raise jose.DeserializationError('Missing error type prefix')
|
||||
|
||||
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
|
||||
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
|
||||
raise jose.DeserializationError('Error type not recognized')
|
||||
|
||||
return without_prefix
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Hardcoded error description based on its type."""
|
||||
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
|
||||
|
||||
def __str__(self):
|
||||
if self.typ is not None:
|
||||
return ' :: '.join([self.typ, self.description, self.detail])
|
||||
else:
|
||||
return str(self.detail)
|
||||
|
||||
|
||||
class _Constant(jose.JSONDeSerializable):
|
||||
"""ACME constant."""
|
||||
__slots__ = ('name',)
|
||||
POSSIBLE_NAMES = NotImplemented
|
||||
|
||||
def __init__(self, name):
|
||||
self.POSSIBLE_NAMES[name] = self
|
||||
self.name = name
|
||||
|
||||
def to_partial_json(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
"""Deserialize from (possibly invalid) JSON object.
|
||||
def from_json(cls, value):
|
||||
if value not in cls.POSSIBLE_NAMES:
|
||||
raise jose.DeserializationError(
|
||||
'{0} not recognized'.format(cls.__name__))
|
||||
return cls.POSSIBLE_NAMES[value]
|
||||
|
||||
Note that the input ``jobj`` has not been sanitized in any way.
|
||||
def __repr__(self):
|
||||
return '{0}({1})'.format(self.__class__.__name__, self.name)
|
||||
|
||||
:param jobj: JSON object.
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and other.name == self.name
|
||||
|
||||
:raises acme.errors.SchemaValidationError: if the input
|
||||
JSON object could not be validated against JSON schema specified
|
||||
in :attr:`schema`.
|
||||
:raises acme.jose.errors.DeserializationError: for any
|
||||
other generic error in decoding.
|
||||
|
||||
:returns: instance of the class
|
||||
|
||||
"""
|
||||
msg_cls = cls.get_type_cls(jobj)
|
||||
|
||||
# TODO: is that schema testing still relevant?
|
||||
try:
|
||||
jsonschema.validate(jobj, msg_cls.schema)
|
||||
except jsonschema.ValidationError as error:
|
||||
raise errors.SchemaValidationError(error)
|
||||
|
||||
return super(Message, cls).from_json(jobj)
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class Challenge(Message):
|
||||
"""ACME "challenge" message.
|
||||
class Status(_Constant):
|
||||
"""ACME "status" field."""
|
||||
POSSIBLE_NAMES = {}
|
||||
STATUS_UNKNOWN = Status('unknown')
|
||||
STATUS_PENDING = Status('pending')
|
||||
STATUS_PROCESSING = Status('processing')
|
||||
STATUS_VALID = Status('valid')
|
||||
STATUS_INVALID = Status('invalid')
|
||||
STATUS_REVOKED = Status('revoked')
|
||||
|
||||
:ivar str nonce: Random data, **not** base64-encoded.
|
||||
:ivar list challenges: List of
|
||||
:class:`~acme.challenges.Challenge` objects.
|
||||
|
||||
.. todo::
|
||||
1. can challenges contain two challenges of the same type?
|
||||
2. can challenges contain duplicates?
|
||||
3. check "combinations" indices are in valid range
|
||||
4. turn "combinations" elements into sets?
|
||||
5. turn "combinations" into set?
|
||||
class IdentifierType(_Constant):
|
||||
"""ACME identifier type."""
|
||||
POSSIBLE_NAMES = {}
|
||||
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
||||
|
||||
|
||||
class Identifier(jose.JSONObjectWithFields):
|
||||
"""ACME identifier.
|
||||
|
||||
:ivar acme.messages.IdentifierType typ:
|
||||
|
||||
"""
|
||||
typ = "challenge"
|
||||
schema = util.load_schema(typ)
|
||||
typ = jose.Field('type', decoder=IdentifierType.from_json)
|
||||
value = jose.Field('value')
|
||||
|
||||
session_id = jose.Field("sessionID")
|
||||
nonce = jose.Field("nonce", encoder=jose.b64encode,
|
||||
decoder=jose.decode_b64jose)
|
||||
challenges = jose.Field("challenges")
|
||||
combinations = jose.Field("combinations", omitempty=True, default=())
|
||||
|
||||
class Resource(jose.JSONObjectWithFields):
|
||||
"""ACME Resource.
|
||||
|
||||
:ivar str uri: Location of the resource.
|
||||
:ivar acme.messages.ResourceBody body: Resource body.
|
||||
|
||||
"""
|
||||
body = jose.Field('body')
|
||||
|
||||
|
||||
class ResourceWithURI(Resource):
|
||||
"""ACME Resource with URI.
|
||||
|
||||
:ivar str uri: Location of the resource.
|
||||
|
||||
"""
|
||||
uri = jose.Field('uri') # no ChallengeResource.uri
|
||||
|
||||
|
||||
class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact: Contact information following ACME spec
|
||||
|
||||
"""
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
contact = jose.Field('contact', omitempty=True, default=())
|
||||
recovery_token = jose.Field('recoveryToken', omitempty=True)
|
||||
agreement = jose.Field('agreement', omitempty=True)
|
||||
|
||||
phone_prefix = 'tel:'
|
||||
email_prefix = 'mailto:'
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, phone=None, email=None, **kwargs):
|
||||
"""Create registration resource from contact details."""
|
||||
details = list(kwargs.pop('contact', ()))
|
||||
if phone is not None:
|
||||
details.append(cls.phone_prefix + phone)
|
||||
if email is not None:
|
||||
details.append(cls.email_prefix + email)
|
||||
kwargs['contact'] = tuple(details)
|
||||
return cls(**kwargs)
|
||||
|
||||
def _filter_contact(self, prefix):
|
||||
return tuple(
|
||||
detail[len(prefix):] for detail in self.contact
|
||||
if detail.startswith(prefix))
|
||||
|
||||
@property
|
||||
def phones(self):
|
||||
"""All phones found in the ``contact`` field."""
|
||||
return self._filter_contact(self.phone_prefix)
|
||||
|
||||
@property
|
||||
def emails(self):
|
||||
"""All emails found in the ``contact`` field."""
|
||||
return self._filter_contact(self.email_prefix)
|
||||
|
||||
@property
|
||||
def phone(self):
|
||||
"""Phone."""
|
||||
assert len(self.phones) == 1
|
||||
return self.phones[0]
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""Email."""
|
||||
assert len(self.emails) == 1
|
||||
return self.emails[0]
|
||||
|
||||
|
||||
class RegistrationResource(ResourceWithURI):
|
||||
"""Registration Resource.
|
||||
|
||||
:ivar acme.messages.Registration body:
|
||||
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
|
||||
:ivar str terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=Registration.from_json)
|
||||
new_authzr_uri = jose.Field('new_authzr_uri')
|
||||
terms_of_service = jose.Field('terms_of_service', omitempty=True)
|
||||
|
||||
|
||||
class ChallengeBody(ResourceBody):
|
||||
"""Challenge Resource Body.
|
||||
|
||||
.. todo::
|
||||
Confusingly, this has a similar name to `.challenges.Challenge`,
|
||||
as well as `.achallenges.AnnotatedChallenge`. Please use names
|
||||
such as ``challb`` to distinguish instances of this class from
|
||||
``achall``.
|
||||
|
||||
:ivar acme.challenges.Challenge: Wrapped challenge.
|
||||
Conveniently, all challenge fields are proxied, i.e. you can
|
||||
call ``challb.x`` to get ``challb.chall.x`` contents.
|
||||
:ivar acme.messages.Status status:
|
||||
:ivar datetime.datetime validated:
|
||||
|
||||
"""
|
||||
__slots__ = ('chall',)
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=Status.from_json,
|
||||
omitempty=True, default=STATUS_PENDING)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
error = jose.Field('error', decoder=Error.from_json,
|
||||
omitempty=True, default=None)
|
||||
|
||||
def to_partial_json(self):
|
||||
jobj = super(ChallengeBody, self).to_partial_json()
|
||||
jobj.update(self.chall.to_partial_json())
|
||||
return jobj
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
|
||||
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
|
||||
return jobj_fields
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.chall, name)
|
||||
|
||||
|
||||
class ChallengeResource(Resource):
|
||||
"""Challenge Resource.
|
||||
|
||||
:ivar acme.messages.ChallengeBody body:
|
||||
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
|
||||
|
||||
"""
|
||||
body = jose.Field('body', decoder=ChallengeBody.from_json)
|
||||
authzr_uri = jose.Field('authzr_uri')
|
||||
|
||||
@property
|
||||
def uri(self): # pylint: disable=missing-docstring,no-self-argument
|
||||
# bug? 'method already defined line None'
|
||||
# pylint: disable=function-redefined
|
||||
return self.body.uri # pylint: disable=no-member
|
||||
|
||||
|
||||
class Authorization(ResourceBody):
|
||||
"""Authorization Resource Body.
|
||||
|
||||
:ivar acme.messages.Identifier identifier:
|
||||
:ivar list challenges: `list` of `.ChallengeBody`
|
||||
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
|
||||
of `int`, as opposed to `list` of `list` from the spec).
|
||||
:ivar acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
:ivar acme.messages.Status status:
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json)
|
||||
challenges = jose.Field('challenges', omitempty=True)
|
||||
combinations = jose.Field('combinations', omitempty=True)
|
||||
|
||||
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
|
||||
# TODO: 'expires' is allowed for Authorization Resources in
|
||||
# general, but for Key Authorization '[t]he "expires" field MUST
|
||||
# be absent'... then acme-spec gives example with 'expires'
|
||||
# present... That's confusing!
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
|
||||
@challenges.decoder
|
||||
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(challenges.Challenge.from_json(chall) for chall in value)
|
||||
return tuple(ChallengeBody.from_json(chall) for chall in value)
|
||||
|
||||
@property
|
||||
def resolved_combinations(self):
|
||||
|
|
@ -109,259 +297,61 @@ class Challenge(Message):
|
|||
for combo in self.combinations)
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class ChallengeRequest(Message):
|
||||
"""ACME "challengeRequest" message."""
|
||||
typ = "challengeRequest"
|
||||
schema = util.load_schema(typ)
|
||||
identifier = jose.Field("identifier")
|
||||
class AuthorizationResource(ResourceWithURI):
|
||||
"""Authorization Resource.
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class Authorization(Message):
|
||||
"""ACME "authorization" message.
|
||||
|
||||
:ivar jwk: :class:`acme.jose.JWK`
|
||||
:ivar acme.messages.Authorization body:
|
||||
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
|
||||
|
||||
"""
|
||||
typ = "authorization"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
recovery_token = jose.Field("recoveryToken", omitempty=True)
|
||||
identifier = jose.Field("identifier", omitempty=True)
|
||||
jwk = jose.Field("jwk", decoder=jose.JWK.from_json, omitempty=True)
|
||||
body = jose.Field('body', decoder=Authorization.from_json)
|
||||
new_cert_uri = jose.Field('new_cert_uri')
|
||||
|
||||
|
||||
@Message.register
|
||||
class AuthorizationRequest(Message):
|
||||
"""ACME "authorizationRequest" message.
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar str nonce: Random data from the corresponding
|
||||
:attr:`Challenge.nonce`, **not** base64-encoded.
|
||||
:ivar list responses: List of completed challenges (
|
||||
:class:`acme.challenges.ChallengeResponse`).
|
||||
:ivar signature: Signature (:class:`acme.other.Signature`).
|
||||
:ivar acme.jose.util.ComparableX509 csr:
|
||||
`M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
:ivar tuple authorizations: `tuple` of URIs (`str`)
|
||||
|
||||
"""
|
||||
typ = "authorizationRequest"
|
||||
schema = util.load_schema(typ)
|
||||
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
authorizations = jose.Field('authorizations', decoder=tuple)
|
||||
|
||||
session_id = jose.Field("sessionID")
|
||||
nonce = jose.Field("nonce", encoder=jose.b64encode,
|
||||
decoder=jose.decode_b64jose)
|
||||
responses = jose.Field("responses")
|
||||
signature = jose.Field("signature", decoder=other.Signature.from_json)
|
||||
contact = jose.Field("contact", omitempty=True, default=())
|
||||
|
||||
@responses.decoder
|
||||
def responses(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(challenges.ChallengeResponse.from_json(chall)
|
||||
for chall in value)
|
||||
class CertificateResource(ResourceWithURI):
|
||||
"""Certificate Resource.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 body:
|
||||
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
|
||||
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
"""
|
||||
cert_chain_uri = jose.Field('cert_chain_uri')
|
||||
authzrs = jose.Field('authzrs')
|
||||
|
||||
|
||||
class Revocation(jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
:ivar .ComparableX509 certificate: `M2Crypto.X509.X509` wrapped in
|
||||
`.ComparableX509`
|
||||
|
||||
"""
|
||||
certificate = jose.Field(
|
||||
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)
|
||||
|
||||
# TODO: acme-spec#138, this allows only one ACME server instance per domain
|
||||
PATH = '/acme/revoke-cert'
|
||||
"""Path to revocation URL, see `url`"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, name, key, sig_nonce=None, **kwargs):
|
||||
"""Create signed "authorizationRequest".
|
||||
def url(cls, base):
|
||||
"""Get revocation URL.
|
||||
|
||||
:param str name: Hostname
|
||||
|
||||
:param key: Key used for signing.
|
||||
:type key: :class:`Crypto.PublicKey.RSA`
|
||||
|
||||
:param str sig_nonce: Nonce used for signature. Useful for testing.
|
||||
:kwargs: Any other arguments accepted by the class constructor.
|
||||
|
||||
:returns: Signed "authorizationRequest" ACME message.
|
||||
:rtype: :class:`AuthorizationRequest`
|
||||
:param str base: New Registration Resource or server (root) URL.
|
||||
|
||||
"""
|
||||
# pylint: disable=too-many-arguments
|
||||
signature = other.Signature.from_msg(
|
||||
name + kwargs["nonce"], key, sig_nonce)
|
||||
return cls(
|
||||
signature=signature, contact=kwargs.pop("contact", ()), **kwargs)
|
||||
|
||||
def verify(self, name):
|
||||
"""Verify signature.
|
||||
|
||||
.. warning:: Caller must check that the public key encoded in the
|
||||
:attr:`signature`'s :class:`acme.jose.JWK` object
|
||||
is the correct key for a given context.
|
||||
|
||||
:param str name: Hostname
|
||||
|
||||
:returns: True iff ``signature`` can be verified, False otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# self.signature is not Field | pylint: disable=no-member
|
||||
return self.signature.verify(name + self.nonce)
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class Certificate(Message):
|
||||
"""ACME "certificate" message.
|
||||
|
||||
:ivar certificate: The certificate (:class:`M2Crypto.X509.X509`
|
||||
wrapped in :class:`acme.util.ComparableX509`).
|
||||
|
||||
:ivar list chain: Chain of certificates (:class:`M2Crypto.X509.X509`
|
||||
wrapped in :class:`acme.util.ComparableX509` ).
|
||||
|
||||
"""
|
||||
typ = "certificate"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
certificate = jose.Field("certificate", encoder=jose.encode_cert,
|
||||
decoder=jose.decode_cert)
|
||||
chain = jose.Field("chain", omitempty=True, default=())
|
||||
refresh = jose.Field("refresh", omitempty=True)
|
||||
|
||||
@chain.decoder
|
||||
def chain(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(jose.decode_cert(cert) for cert in value)
|
||||
|
||||
@chain.encoder
|
||||
def chain(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(jose.encode_cert(cert) for cert in value)
|
||||
|
||||
|
||||
@Message.register
|
||||
class CertificateRequest(Message):
|
||||
"""ACME "certificateRequest" message.
|
||||
|
||||
:ivar csr: Certificate Signing Request (:class:`M2Crypto.X509.Request`
|
||||
wrapped in :class:`acme.util.ComparableX509`.
|
||||
:ivar signature: Signature (:class:`acme.other.Signature`).
|
||||
|
||||
"""
|
||||
typ = "certificateRequest"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
csr = jose.Field("csr", encoder=jose.encode_csr,
|
||||
decoder=jose.decode_csr)
|
||||
signature = jose.Field("signature", decoder=other.Signature.from_json)
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, sig_nonce=None, **kwargs):
|
||||
"""Create signed "certificateRequest".
|
||||
|
||||
:param key: Key used for signing.
|
||||
:type key: :class:`Crypto.PublicKey.RSA`
|
||||
|
||||
:param str sig_nonce: Nonce used for signature. Useful for testing.
|
||||
:kwargs: Any other arguments accepted by the class constructor.
|
||||
|
||||
:returns: Signed "certificateRequest" ACME message.
|
||||
:rtype: :class:`CertificateRequest`
|
||||
|
||||
"""
|
||||
return cls(signature=other.Signature.from_msg(
|
||||
kwargs["csr"].as_der(), key, sig_nonce), **kwargs)
|
||||
|
||||
def verify(self):
|
||||
"""Verify signature.
|
||||
|
||||
.. warning:: Caller must check that the public key encoded in the
|
||||
:attr:`signature`'s :class:`acme.jose.JWK` object
|
||||
is the correct key for a given context.
|
||||
|
||||
:returns: True iff ``signature`` can be verified, False otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# self.signature is not Field | pylint: disable=no-member
|
||||
return self.signature.verify(self.csr.as_der())
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class Defer(Message):
|
||||
"""ACME "defer" message."""
|
||||
typ = "defer"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
token = jose.Field("token")
|
||||
interval = jose.Field("interval", omitempty=True)
|
||||
message = jose.Field("message", omitempty=True)
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class Error(Message):
|
||||
"""ACME "error" message."""
|
||||
typ = "error"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
error = jose.Field("error")
|
||||
message = jose.Field("message", omitempty=True)
|
||||
more_info = jose.Field("moreInfo", omitempty=True)
|
||||
|
||||
MESSAGE_CODES = {
|
||||
"malformed": "The request message was malformed",
|
||||
"unauthorized": "The client lacks sufficient authorization",
|
||||
"serverInternal": "The server experienced an internal error",
|
||||
"notSupported": "The request type is not supported",
|
||||
"unknown": "The server does not recognize an ID/token in the request",
|
||||
"badCSR": "The CSR is unacceptable (e.g., due to a short key)",
|
||||
}
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class Revocation(Message):
|
||||
"""ACME "revocation" message."""
|
||||
typ = "revocation"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
|
||||
@Message.register
|
||||
class RevocationRequest(Message):
|
||||
"""ACME "revocationRequest" message.
|
||||
|
||||
:ivar certificate: Certificate (:class:`M2Crypto.X509.X509`
|
||||
wrapped in :class:`acme.util.ComparableX509`).
|
||||
:ivar signature: Signature (:class:`acme.other.Signature`).
|
||||
|
||||
"""
|
||||
typ = "revocationRequest"
|
||||
schema = util.load_schema(typ)
|
||||
|
||||
certificate = jose.Field("certificate", decoder=jose.decode_cert,
|
||||
encoder=jose.encode_cert)
|
||||
signature = jose.Field("signature", decoder=other.Signature.from_json)
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, sig_nonce=None, **kwargs):
|
||||
"""Create signed "revocationRequest".
|
||||
|
||||
:param key: Key used for signing.
|
||||
:type key: :class:`Crypto.PublicKey.RSA`
|
||||
|
||||
:param str sig_nonce: Nonce used for signature. Useful for testing.
|
||||
:kwargs: Any other arguments accepted by the class constructor.
|
||||
|
||||
:returns: Signed "revocationRequest" ACME message.
|
||||
:rtype: :class:`RevocationRequest`
|
||||
|
||||
"""
|
||||
return cls(signature=other.Signature.from_msg(
|
||||
kwargs["certificate"].as_der(), key, sig_nonce), **kwargs)
|
||||
|
||||
def verify(self):
|
||||
"""Verify signature.
|
||||
|
||||
.. warning:: Caller must check that the public key encoded in the
|
||||
:attr:`signature`'s :class:`acme.jose.JWK` object
|
||||
is the correct key for a given context.
|
||||
|
||||
:returns: True iff ``signature`` can be verified, False otherwise.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
# self.signature is not Field | pylint: disable=no-member
|
||||
return self.signature.verify(self.certificate.as_der())
|
||||
|
||||
|
||||
@Message.register # pylint: disable=too-few-public-methods
|
||||
class StatusRequest(Message):
|
||||
"""ACME "statusRequest" message."""
|
||||
typ = "statusRequest"
|
||||
schema = util.load_schema(typ)
|
||||
token = jose.Field("token")
|
||||
return urlparse.urljoin(base, cls.PATH)
|
||||
|
|
|
|||
|
|
@ -1,298 +0,0 @@
|
|||
"""ACME protocol messages."""
|
||||
from acme import challenges
|
||||
from acme import fields
|
||||
from acme import jose
|
||||
|
||||
|
||||
class Error(jose.JSONObjectWithFields, Exception):
|
||||
"""ACME error.
|
||||
|
||||
https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00
|
||||
|
||||
"""
|
||||
ERROR_TYPE_NAMESPACE = 'urn:acme:error:'
|
||||
ERROR_TYPE_DESCRIPTIONS = {
|
||||
'malformed': 'The request message was malformed',
|
||||
'unauthorized': 'The client lacks sufficient authorization',
|
||||
'serverInternal': 'The server experienced an internal error',
|
||||
'badCSR': 'The CSR is unacceptable (e.g., due to a short key)',
|
||||
'badNonce': 'The client sent an unacceptable anti-replay nonce',
|
||||
}
|
||||
|
||||
typ = jose.Field('type')
|
||||
title = jose.Field('title', omitempty=True)
|
||||
detail = jose.Field('detail')
|
||||
|
||||
@typ.encoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return Error.ERROR_TYPE_NAMESPACE + value
|
||||
|
||||
@typ.decoder
|
||||
def typ(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
# pylint thinks isinstance(value, Error), so startswith is not found
|
||||
# pylint: disable=no-member
|
||||
if not value.startswith(Error.ERROR_TYPE_NAMESPACE):
|
||||
raise jose.DeserializationError('Missing error type prefix')
|
||||
|
||||
without_prefix = value[len(Error.ERROR_TYPE_NAMESPACE):]
|
||||
if without_prefix not in Error.ERROR_TYPE_DESCRIPTIONS:
|
||||
raise jose.DeserializationError('Error type not recognized')
|
||||
|
||||
return without_prefix
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""Hardcoded error description based on its type."""
|
||||
return self.ERROR_TYPE_DESCRIPTIONS[self.typ]
|
||||
|
||||
def __str__(self):
|
||||
if self.typ is not None:
|
||||
return ' :: '.join([self.typ, self.description, self.detail])
|
||||
else:
|
||||
return str(self.detail)
|
||||
|
||||
class _Constant(jose.JSONDeSerializable):
|
||||
"""ACME constant."""
|
||||
__slots__ = ('name',)
|
||||
POSSIBLE_NAMES = NotImplemented
|
||||
|
||||
def __init__(self, name):
|
||||
self.POSSIBLE_NAMES[name] = self
|
||||
self.name = name
|
||||
|
||||
def to_partial_json(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, value):
|
||||
if value not in cls.POSSIBLE_NAMES:
|
||||
raise jose.DeserializationError(
|
||||
'{0} not recognized'.format(cls.__name__))
|
||||
return cls.POSSIBLE_NAMES[value]
|
||||
|
||||
def __repr__(self):
|
||||
return '{0}({1})'.format(self.__class__.__name__, self.name)
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, type(self)) and other.name == self.name
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
|
||||
class Status(_Constant):
|
||||
"""ACME "status" field."""
|
||||
POSSIBLE_NAMES = {}
|
||||
STATUS_UNKNOWN = Status('unknown')
|
||||
STATUS_PENDING = Status('pending')
|
||||
STATUS_PROCESSING = Status('processing')
|
||||
STATUS_VALID = Status('valid')
|
||||
STATUS_INVALID = Status('invalid')
|
||||
STATUS_REVOKED = Status('revoked')
|
||||
|
||||
|
||||
class IdentifierType(_Constant):
|
||||
"""ACME identifier type."""
|
||||
POSSIBLE_NAMES = {}
|
||||
IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder
|
||||
|
||||
|
||||
class Identifier(jose.JSONObjectWithFields):
|
||||
"""ACME identifier.
|
||||
|
||||
:ivar acme.messages2.IdentifierType typ:
|
||||
|
||||
"""
|
||||
typ = jose.Field('type', decoder=IdentifierType.from_json)
|
||||
value = jose.Field('value')
|
||||
|
||||
|
||||
class Resource(jose.ImmutableMap):
|
||||
"""ACME Resource.
|
||||
|
||||
:ivar acme.messages2.ResourceBody body: Resource body.
|
||||
:ivar str uri: Location of the resource.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri')
|
||||
|
||||
|
||||
class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class RegistrationResource(Resource):
|
||||
"""Registration Resource.
|
||||
|
||||
:ivar acme.messages2.Registration body:
|
||||
:ivar str new_authzr_uri: URI found in the 'next' ``Link`` header
|
||||
:ivar str terms_of_service: URL for the CA TOS.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'new_authzr_uri', 'terms_of_service')
|
||||
|
||||
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact: Contact information following ACME spec
|
||||
|
||||
"""
|
||||
# on new-reg key server ignores 'key' and populates it based on
|
||||
# JWS.signature.combined.jwk
|
||||
key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json)
|
||||
contact = jose.Field('contact', omitempty=True, default=())
|
||||
recovery_token = jose.Field('recoveryToken', omitempty=True)
|
||||
agreement = jose.Field('agreement', omitempty=True)
|
||||
|
||||
|
||||
class ChallengeResource(Resource, jose.JSONObjectWithFields):
|
||||
"""Challenge Resource.
|
||||
|
||||
:ivar acme.messages2.ChallengeBody body:
|
||||
:ivar str authzr_uri: URI found in the 'up' ``Link`` header.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'authzr_uri')
|
||||
|
||||
@property
|
||||
def uri(self): # pylint: disable=missing-docstring,no-self-argument
|
||||
# bug? 'method already defined line None'
|
||||
# pylint: disable=function-redefined
|
||||
return self.body.uri
|
||||
|
||||
|
||||
class ChallengeBody(ResourceBody):
|
||||
"""Challenge Resource Body.
|
||||
|
||||
.. todo::
|
||||
Confusingly, this has a similar name to `.challenges.Challenge`,
|
||||
as well as `.achallenges.AnnotatedChallenge`. Please use names
|
||||
such as ``challb`` to distinguish instances of this class from
|
||||
``achall``.
|
||||
|
||||
:ivar acme.challenges.Challenge: Wrapped challenge.
|
||||
Conveniently, all challenge fields are proxied, i.e. you can
|
||||
call ``challb.x`` to get ``challb.chall.x`` contents.
|
||||
:ivar acme.messages2.Status status:
|
||||
:ivar datetime.datetime validated:
|
||||
|
||||
"""
|
||||
__slots__ = ('chall',)
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
def to_partial_json(self):
|
||||
jobj = super(ChallengeBody, self).to_partial_json()
|
||||
jobj.update(self.chall.to_partial_json())
|
||||
return jobj
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
|
||||
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
|
||||
return jobj_fields
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.chall, name)
|
||||
|
||||
|
||||
class AuthorizationResource(Resource):
|
||||
"""Authorization Resource.
|
||||
|
||||
:ivar acme.messages2.Authorization body:
|
||||
:ivar str new_cert_uri: URI found in the 'next' ``Link`` header
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'new_cert_uri')
|
||||
|
||||
|
||||
class Authorization(ResourceBody):
|
||||
"""Authorization Resource Body.
|
||||
|
||||
:ivar acme.messages2.Identifier identifier:
|
||||
:ivar list challenges: `list` of `.ChallengeBody`
|
||||
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
|
||||
of `int`, as opposed to `list` of `list` from the spec).
|
||||
:ivar acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
:ivar acme.messages2.Status status:
|
||||
:ivar datetime.datetime expires:
|
||||
|
||||
"""
|
||||
identifier = jose.Field('identifier', decoder=Identifier.from_json)
|
||||
challenges = jose.Field('challenges', omitempty=True)
|
||||
combinations = jose.Field('combinations', omitempty=True)
|
||||
|
||||
status = jose.Field('status', omitempty=True, decoder=Status.from_json)
|
||||
# TODO: 'expires' is allowed for Authorization Resources in
|
||||
# general, but for Key Authorization '[t]he "expires" field MUST
|
||||
# be absent'... then acme-spec gives example with 'expires'
|
||||
# present... That's confusing!
|
||||
expires = fields.RFC3339Field('expires', omitempty=True)
|
||||
|
||||
@challenges.decoder
|
||||
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(ChallengeBody.from_json(chall) for chall in value)
|
||||
|
||||
@property
|
||||
def resolved_combinations(self):
|
||||
"""Combinations with challenges instead of indices."""
|
||||
return tuple(tuple(self.challenges[idx] for idx in combo)
|
||||
for combo in self.combinations)
|
||||
|
||||
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 csr:
|
||||
`M2Crypto.X509.Request` wrapped in `.ComparableX509`
|
||||
:ivar tuple authorizations: `tuple` of URIs (`str`)
|
||||
|
||||
"""
|
||||
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
|
||||
authorizations = jose.Field('authorizations', decoder=tuple)
|
||||
|
||||
|
||||
class CertificateResource(Resource):
|
||||
"""Certificate Resource.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 body:
|
||||
`M2Crypto.X509.X509` wrapped in `.ComparableX509`
|
||||
:ivar str cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
"""
|
||||
__slots__ = ('body', 'uri', 'cert_chain_uri', 'authzrs')
|
||||
|
||||
|
||||
class Revocation(jose.JSONObjectWithFields):
|
||||
"""Revocation message.
|
||||
|
||||
:ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`.
|
||||
:ivar tuple authorizations: Same as `CertificateRequest.authorizations`
|
||||
|
||||
"""
|
||||
|
||||
NOW = 'now'
|
||||
"""A possible value for `revoke`, denoting that certificate should
|
||||
be revoked now."""
|
||||
|
||||
revoke = jose.Field('revoke')
|
||||
authorizations = CertificateRequest._fields['authorizations']
|
||||
|
||||
@revoke.decoder
|
||||
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
if value == Revocation.NOW:
|
||||
return value
|
||||
else:
|
||||
return fields.RFC3339Field.default_decoder(value)
|
||||
|
||||
@revoke.encoder
|
||||
def revoke(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
if value == Revocation.NOW:
|
||||
return value
|
||||
else:
|
||||
return fields.RFC3339Field.default_encoder(value)
|
||||
|
|
@ -1,250 +0,0 @@
|
|||
"""Tests for acme.messages2."""
|
||||
import datetime
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import pytz
|
||||
from Crypto.PublicKey import RSA
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
|
||||
|
||||
KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
|
||||
|
||||
|
||||
class ErrorTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2.Error."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages2 import Error
|
||||
self.error = Error(detail='foo', typ='malformed', title='title')
|
||||
self.jobj = {'detail': 'foo', 'title': 'some title'}
|
||||
|
||||
def test_typ_prefix(self):
|
||||
self.assertEqual('malformed', self.error.typ)
|
||||
self.assertEqual(
|
||||
'urn:acme:error:malformed', self.error.to_partial_json()['type'])
|
||||
self.assertEqual(
|
||||
'malformed', self.error.from_json(self.error.to_partial_json()).typ)
|
||||
|
||||
def test_typ_decoder_missing_prefix(self):
|
||||
from acme.messages2 import Error
|
||||
self.jobj['type'] = 'malformed'
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
|
||||
self.jobj['type'] = 'not valid bare type'
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
|
||||
|
||||
def test_typ_decoder_not_recognized(self):
|
||||
from acme.messages2 import Error
|
||||
self.jobj['type'] = 'urn:acme:error:baz'
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
'The request message was malformed', self.error.description)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages2 import Error
|
||||
hash(Error.from_json(self.error.to_json()))
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(
|
||||
'malformed :: The request message was malformed :: foo',
|
||||
str(self.error))
|
||||
self.assertEqual('foo', str(self.error.update(typ=None)))
|
||||
|
||||
|
||||
class ConstantTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2._Constant."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages2 import _Constant
|
||||
class MockConstant(_Constant): # pylint: disable=missing-docstring
|
||||
POSSIBLE_NAMES = {}
|
||||
|
||||
self.MockConstant = MockConstant # pylint: disable=invalid-name
|
||||
self.const_a = MockConstant('a')
|
||||
self.const_b = MockConstant('b')
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual('a', self.const_a.to_partial_json())
|
||||
self.assertEqual('b', self.const_b.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, self.MockConstant.from_json, 'c')
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
hash(self.MockConstant.from_json('a'))
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('MockConstant(a)', repr(self.const_a))
|
||||
self.assertEqual('MockConstant(b)', repr(self.const_b))
|
||||
|
||||
def test_equality(self):
|
||||
const_a_prime = self.MockConstant('a')
|
||||
self.assertFalse(self.const_a == self.const_b)
|
||||
self.assertTrue(self.const_a == const_a_prime)
|
||||
|
||||
self.assertTrue(self.const_a != self.const_b)
|
||||
self.assertFalse(self.const_a != const_a_prime)
|
||||
|
||||
class RegistrationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2.Registration."""
|
||||
|
||||
def setUp(self):
|
||||
key = jose.jwk.JWKRSA(key=KEY.publickey())
|
||||
contact = ('mailto:letsencrypt-client@letsencrypt.org',)
|
||||
recovery_token = 'XYZ'
|
||||
agreement = 'https://letsencrypt.org/terms'
|
||||
|
||||
from acme.messages2 import Registration
|
||||
self.reg = Registration(
|
||||
key=key, contact=contact, recovery_token=recovery_token,
|
||||
agreement=agreement)
|
||||
|
||||
self.jobj_to = {
|
||||
'contact': contact,
|
||||
'recoveryToken': recovery_token,
|
||||
'agreement': agreement,
|
||||
'key': key,
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['key'] = key.to_json()
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj_to, self.reg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages2 import Registration
|
||||
self.assertEqual(self.reg, Registration.from_json(self.jobj_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages2 import Registration
|
||||
hash(Registration.from_json(self.jobj_from))
|
||||
|
||||
|
||||
class ChallengeResourceTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2.ChallengeResource."""
|
||||
|
||||
def test_uri(self):
|
||||
from acme.messages2 import ChallengeResource
|
||||
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
|
||||
uri='http://challb'), authzr_uri='http://authz').uri)
|
||||
|
||||
|
||||
class ChallengeBodyTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2.ChallengeBody."""
|
||||
|
||||
def setUp(self):
|
||||
self.chall = challenges.DNS(token='foo')
|
||||
|
||||
from acme.messages2 import ChallengeBody
|
||||
from acme.messages2 import STATUS_VALID
|
||||
self.status = STATUS_VALID
|
||||
self.challb = ChallengeBody(
|
||||
uri='http://challb', chall=self.chall, status=self.status)
|
||||
|
||||
self.jobj_to = {
|
||||
'uri': 'http://challb',
|
||||
'status': self.status,
|
||||
'type': 'dns',
|
||||
'token': 'foo',
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['status'] = 'valid'
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj_to, self.challb.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages2 import ChallengeBody
|
||||
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages2 import ChallengeBody
|
||||
hash(ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
def test_proxy(self):
|
||||
self.assertEqual('foo', self.challb.token)
|
||||
|
||||
|
||||
class AuthorizationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2.Authorization."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages2 import ChallengeBody
|
||||
from acme.messages2 import STATUS_VALID
|
||||
self.challbs = (
|
||||
ChallengeBody(
|
||||
uri='http://challb1', status=STATUS_VALID,
|
||||
chall=challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A')),
|
||||
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
||||
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
|
||||
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
|
||||
chall=challenges.RecoveryToken()),
|
||||
)
|
||||
combinations = ((0, 2), (1, 2))
|
||||
|
||||
from acme.messages2 import Authorization
|
||||
from acme.messages2 import Identifier
|
||||
from acme.messages2 import IDENTIFIER_FQDN
|
||||
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
|
||||
self.authz = Authorization(
|
||||
identifier=identifier, combinations=combinations,
|
||||
challenges=self.challbs)
|
||||
|
||||
self.jobj_from = {
|
||||
'identifier': identifier.to_json(),
|
||||
'challenges': [challb.to_json() for challb in self.challbs],
|
||||
'combinations': combinations,
|
||||
}
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages2 import Authorization
|
||||
Authorization.from_json(self.jobj_from)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages2 import Authorization
|
||||
hash(Authorization.from_json(self.jobj_from))
|
||||
|
||||
def test_resolved_combinations(self):
|
||||
self.assertEqual(self.authz.resolved_combinations, (
|
||||
(self.challbs[0], self.challbs[2]),
|
||||
(self.challbs[1], self.challbs[2]),
|
||||
))
|
||||
|
||||
|
||||
class RevocationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages2.RevocationTest."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages2 import Revocation
|
||||
self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW)
|
||||
self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime(
|
||||
2015, 3, 27, tzinfo=pytz.utc))
|
||||
self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW}
|
||||
self.jobj_date = {'authorizations': (),
|
||||
'revoke': '2015-03-27T00:00:00Z'}
|
||||
|
||||
def test_revoke_decoder(self):
|
||||
from acme.messages2 import Revocation
|
||||
self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now))
|
||||
self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date))
|
||||
|
||||
def test_revoke_encoder(self):
|
||||
self.assertEqual(self.jobj_now, self.rev_now.to_partial_json())
|
||||
self.assertEqual(self.jobj_date, self.rev_date.to_partial_json())
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages2 import Revocation
|
||||
hash(Revocation.from_json(self.rev_now.to_json()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -3,477 +3,346 @@ import os
|
|||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
from Crypto.PublicKey import RSA
|
||||
import M2Crypto
|
||||
import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import other
|
||||
|
||||
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert_string(
|
||||
pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
|
||||
'acme.jose', os.path.join('testdata', 'cert.der')),
|
||||
M2Crypto.X509.FORMAT_DER))
|
||||
CSR = jose.ComparableX509(M2Crypto.X509.load_request_string(
|
||||
pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'csr.der')),
|
||||
M2Crypto.X509.FORMAT_DER))
|
||||
KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
|
||||
CERT = jose.ComparableX509(M2Crypto.X509.load_cert(
|
||||
pkg_resources.resource_filename(
|
||||
'letsencrypt.tests', os.path.join('testdata', 'cert.pem'))))
|
||||
CSR = jose.ComparableX509(M2Crypto.X509.load_request(
|
||||
pkg_resources.resource_filename(
|
||||
'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))))
|
||||
CSR2 = jose.ComparableX509(M2Crypto.X509.load_request(
|
||||
pkg_resources.resource_filename(
|
||||
'acme.jose', os.path.join('testdata', 'csr2.pem'))))
|
||||
|
||||
|
||||
class MessageTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Message."""
|
||||
|
||||
def setUp(self):
|
||||
# pylint: disable=missing-docstring,too-few-public-methods
|
||||
from acme.messages import Message
|
||||
|
||||
class MockParentMessage(Message):
|
||||
# pylint: disable=abstract-method
|
||||
TYPES = {}
|
||||
|
||||
@MockParentMessage.register
|
||||
class MockMessage(MockParentMessage):
|
||||
typ = 'test'
|
||||
schema = {
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'price': {'type': 'number'},
|
||||
'name': {'type': 'string'},
|
||||
},
|
||||
}
|
||||
price = jose.Field('price')
|
||||
name = jose.Field('name')
|
||||
|
||||
self.parent_cls = MockParentMessage
|
||||
self.msg = MockMessage(price=123, name='foo')
|
||||
|
||||
def test_from_json_validates(self):
|
||||
self.assertRaises(errors.SchemaValidationError,
|
||||
self.parent_cls.from_json,
|
||||
{'type': 'test', 'price': 'asd'})
|
||||
|
||||
|
||||
class ChallengeTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
challs = (
|
||||
challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'),
|
||||
challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'),
|
||||
challenges.RecoveryToken(),
|
||||
)
|
||||
combinations = ((0, 2), (1, 2))
|
||||
|
||||
from acme.messages import Challenge
|
||||
self.msg = Challenge(
|
||||
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
|
||||
challenges=challs, combinations=combinations)
|
||||
|
||||
self.jmsg_to = {
|
||||
'type': 'challenge',
|
||||
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
|
||||
'challenges': challs,
|
||||
'combinations': combinations,
|
||||
}
|
||||
|
||||
self.jmsg_from = {
|
||||
'type': 'challenge',
|
||||
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
|
||||
'challenges': [chall.to_json() for chall in challs],
|
||||
'combinations': [[0, 2], [1, 2]], # TODO array tuples
|
||||
}
|
||||
|
||||
def test_resolved_combinations(self):
|
||||
self.assertEqual(self.msg.resolved_combinations, (
|
||||
(
|
||||
challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A'),
|
||||
challenges.RecoveryToken()
|
||||
),
|
||||
(
|
||||
challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA'),
|
||||
challenges.RecoveryToken(),
|
||||
)
|
||||
))
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import Challenge
|
||||
self.assertEqual(Challenge.from_json(self.jmsg_from), self.msg)
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg_from['combinations']
|
||||
del self.jmsg_to['combinations']
|
||||
|
||||
from acme.messages import Challenge
|
||||
msg = Challenge.from_json(self.jmsg_from)
|
||||
|
||||
self.assertEqual(msg.combinations, ())
|
||||
self.assertEqual(msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
|
||||
class ChallengeRequestTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import ChallengeRequest
|
||||
self.msg = ChallengeRequest(identifier='example.com')
|
||||
|
||||
self.jmsg = {
|
||||
'type': 'challengeRequest',
|
||||
'identifier': 'example.com',
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import ChallengeRequest
|
||||
self.assertEqual(ChallengeRequest.from_json(self.jmsg), self.msg)
|
||||
|
||||
|
||||
class AuthorizationTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
jwk = jose.JWKRSA(key=KEY.publickey())
|
||||
|
||||
from acme.messages import Authorization
|
||||
self.msg = Authorization(recovery_token='tok', jwk=jwk,
|
||||
identifier='example.com')
|
||||
|
||||
self.jmsg = {
|
||||
'type': 'authorization',
|
||||
'recoveryToken': 'tok',
|
||||
'identifier': 'example.com',
|
||||
'jwk': jwk,
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
self.jmsg['jwk'] = self.jmsg['jwk'].to_partial_json()
|
||||
|
||||
from acme.messages import Authorization
|
||||
self.assertEqual(Authorization.from_json(self.jmsg), self.msg)
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['recoveryToken']
|
||||
del self.jmsg['identifier']
|
||||
del self.jmsg['jwk']
|
||||
|
||||
from acme.messages import Authorization
|
||||
msg = Authorization.from_json(self.jmsg)
|
||||
|
||||
self.assertTrue(msg.recovery_token is None)
|
||||
self.assertTrue(msg.identifier is None)
|
||||
self.assertTrue(msg.jwk is None)
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
|
||||
|
||||
class AuthorizationRequestTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.responses = (
|
||||
challenges.SimpleHTTPResponse(path='Hf5GrX4Q7EBax9hc2jJnfw'),
|
||||
None, # null
|
||||
challenges.RecoveryTokenResponse(token='23029d88d9e123e'),
|
||||
)
|
||||
self.contact = ("mailto:cert-admin@example.com", "tel:+12025551212")
|
||||
signature = other.Signature(
|
||||
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()),
|
||||
sig='-v\xd8\xc2\xa3\xba0\xd6\x92\x16\xb5.\xbe\xa1[\x04\xbe'
|
||||
'\x1b\xa1X\xd2)\x18\x94\x8f\xd7\xd0\xc0\xbbcI`W\xdf v'
|
||||
'\xe4\xed\xe8\x03J\xe8\xc8<?\xc8W\x94\x94cj(\xe7\xaa$'
|
||||
'\x92\xe9\x96\x11\xc2\xefx\x0bR',
|
||||
nonce='\xab?\x08o\xe6\x81$\x9f\xa1\xc9\x025\x1c\x1b\xa5+')
|
||||
|
||||
from acme.messages import AuthorizationRequest
|
||||
self.msg = AuthorizationRequest(
|
||||
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
|
||||
responses=self.responses,
|
||||
signature=signature,
|
||||
contact=self.contact,
|
||||
)
|
||||
|
||||
self.jmsg_to = {
|
||||
'type': 'authorizationRequest',
|
||||
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
|
||||
'responses': self.responses,
|
||||
'signature': signature,
|
||||
'contact': self.contact,
|
||||
}
|
||||
self.jmsg_from = {
|
||||
'type': 'authorizationRequest',
|
||||
'sessionID': 'aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
'nonce': '7Nbyb1lI6xPVI3Hg3aKSqQ',
|
||||
'responses': [None if response is None else response.to_json()
|
||||
for response in self.responses],
|
||||
'signature': signature.to_json(),
|
||||
# TODO: schema validation doesn't recognize tuples as
|
||||
# arrays :(
|
||||
'contact': list(self.contact),
|
||||
}
|
||||
|
||||
def test_create(self):
|
||||
from acme.messages import AuthorizationRequest
|
||||
self.assertEqual(self.msg, AuthorizationRequest.create(
|
||||
name='example.com', key=KEY, responses=self.responses,
|
||||
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9',
|
||||
session_id='aefoGaavieG9Wihuk2aufai3aeZ5EeW4',
|
||||
sig_nonce='\xab?\x08o\xe6\x81$\x9f\xa1\xc9\x025\x1c\x1b\xa5+',
|
||||
contact=self.contact))
|
||||
|
||||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify('example.com'))
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import AuthorizationRequest
|
||||
self.assertEqual(
|
||||
self.msg, AuthorizationRequest.from_json(self.jmsg_from))
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg_from['contact']
|
||||
del self.jmsg_to['contact']
|
||||
|
||||
from acme.messages import AuthorizationRequest
|
||||
msg = AuthorizationRequest.from_json(self.jmsg_from)
|
||||
|
||||
self.assertEqual(msg.contact, ())
|
||||
self.assertEqual(self.jmsg_to, msg.to_partial_json())
|
||||
|
||||
|
||||
class CertificateTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
refresh = 'https://example.com/refresh/Dr8eAwTVQfSS/'
|
||||
|
||||
from acme.messages import Certificate
|
||||
self.msg = Certificate(
|
||||
certificate=CERT, chain=(CERT,), refresh=refresh)
|
||||
|
||||
self.jmsg_to = {
|
||||
'type': 'certificate',
|
||||
'certificate': jose.b64encode(CERT.as_der()),
|
||||
'chain': (jose.b64encode(CERT.as_der()),),
|
||||
'refresh': refresh,
|
||||
}
|
||||
self.jmsg_from = self.jmsg_to.copy()
|
||||
# TODO: schema validation array tuples
|
||||
self.jmsg_from['chain'] = list(self.jmsg_from['chain'])
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import Certificate
|
||||
self.assertEqual(Certificate.from_json(self.jmsg_from), self.msg)
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg_from['chain']
|
||||
del self.jmsg_from['refresh']
|
||||
del self.jmsg_to['chain']
|
||||
del self.jmsg_to['refresh']
|
||||
|
||||
from acme.messages import Certificate
|
||||
msg = Certificate.from_json(self.jmsg_from)
|
||||
|
||||
self.assertEqual(msg.chain, ())
|
||||
self.assertTrue(msg.refresh is None)
|
||||
self.assertEqual(self.jmsg_to, msg.to_partial_json())
|
||||
|
||||
|
||||
class CertificateRequestTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
signature = other.Signature(
|
||||
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()),
|
||||
sig='\x15\xed\x84\xaa:\xf2DO\x0e9 \xbcg\xf8\xc0\xcf\x87\x9a'
|
||||
'\x95\xeb\xffT[\x84[\xec\x85\x7f\x8eK\xe9\xc2\x12\xc8Q'
|
||||
'\xafo\xc6h\x07\xba\xa6\xdf\xd1\xa7"$\xba=Z\x13n\x14\x0b'
|
||||
'k\xfe\xee\xb4\xe4\xc8\x05\x9a\x08\xa7',
|
||||
nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9')
|
||||
|
||||
from acme.messages import CertificateRequest
|
||||
self.msg = CertificateRequest(csr=CSR, signature=signature)
|
||||
|
||||
self.jmsg_to = {
|
||||
'type': 'certificateRequest',
|
||||
'csr': jose.b64encode(CSR.as_der()),
|
||||
'signature': signature,
|
||||
}
|
||||
self.jmsg_from = self.jmsg_to.copy()
|
||||
self.jmsg_from['signature'] = self.jmsg_from['signature'].to_json()
|
||||
|
||||
def test_create(self):
|
||||
from acme.messages import CertificateRequest
|
||||
self.assertEqual(self.msg, CertificateRequest.create(
|
||||
csr=CSR, key=KEY,
|
||||
sig_nonce='\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'))
|
||||
|
||||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify())
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import CertificateRequest
|
||||
self.assertEqual(self.msg, CertificateRequest.from_json(self.jmsg_from))
|
||||
|
||||
|
||||
class DeferTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import Defer
|
||||
self.msg = Defer(
|
||||
token='O7-s9MNq1siZHlgrMzi9_A', interval=60,
|
||||
message='Warming up the HSM')
|
||||
|
||||
self.jmsg = {
|
||||
'type': 'defer',
|
||||
'token': 'O7-s9MNq1siZHlgrMzi9_A',
|
||||
'interval': 60,
|
||||
'message': 'Warming up the HSM',
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import Defer
|
||||
self.assertEqual(Defer.from_json(self.jmsg), self.msg)
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['interval']
|
||||
del self.jmsg['message']
|
||||
|
||||
from acme.messages import Defer
|
||||
msg = Defer.from_json(self.jmsg)
|
||||
|
||||
self.assertTrue(msg.interval is None)
|
||||
self.assertTrue(msg.message is None)
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
format=M2Crypto.X509.FORMAT_DER, file=pkg_resources.resource_filename(
|
||||
'acme.jose', os.path.join('testdata', 'cert.der'))))
|
||||
|
||||
|
||||
class ErrorTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Error."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import Error
|
||||
self.msg = Error(
|
||||
error='badCSR', message='RSA keys must be at least 2048 bits long',
|
||||
more_info='https://ca.example.com/documentation/csr-requirements')
|
||||
self.error = Error(detail='foo', typ='malformed', title='title')
|
||||
self.jobj = {'detail': 'foo', 'title': 'some title'}
|
||||
|
||||
self.jmsg = {
|
||||
'type': 'error',
|
||||
'error': 'badCSR',
|
||||
'message':'RSA keys must be at least 2048 bits long',
|
||||
'moreInfo': 'https://ca.example.com/documentation/csr-requirements',
|
||||
}
|
||||
def test_typ_prefix(self):
|
||||
self.assertEqual('malformed', self.error.typ)
|
||||
self.assertEqual(
|
||||
'urn:acme:error:malformed', self.error.to_partial_json()['type'])
|
||||
self.assertEqual(
|
||||
'malformed', self.error.from_json(self.error.to_partial_json()).typ)
|
||||
|
||||
def test_typ_decoder_missing_prefix(self):
|
||||
from acme.messages import Error
|
||||
self.jobj['type'] = 'malformed'
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
|
||||
self.jobj['type'] = 'not valid bare type'
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
|
||||
|
||||
def test_typ_decoder_not_recognized(self):
|
||||
from acme.messages import Error
|
||||
self.jobj['type'] = 'urn:acme:error:baz'
|
||||
self.assertRaises(jose.DeserializationError, Error.from_json, self.jobj)
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
'The request message was malformed', self.error.description)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages import Error
|
||||
hash(Error.from_json(self.error.to_json()))
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(
|
||||
'malformed :: The request message was malformed :: foo',
|
||||
str(self.error))
|
||||
self.assertEqual('foo', str(self.error.update(typ=None)))
|
||||
|
||||
|
||||
class ConstantTest(unittest.TestCase):
|
||||
"""Tests for acme.messages._Constant."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import _Constant
|
||||
class MockConstant(_Constant): # pylint: disable=missing-docstring
|
||||
POSSIBLE_NAMES = {}
|
||||
|
||||
self.MockConstant = MockConstant # pylint: disable=invalid-name
|
||||
self.const_a = MockConstant('a')
|
||||
self.const_b = MockConstant('b')
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
self.assertEqual('a', self.const_a.to_partial_json())
|
||||
self.assertEqual('b', self.const_b.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, self.MockConstant.from_json, 'c')
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
hash(self.MockConstant.from_json('a'))
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('MockConstant(a)', repr(self.const_a))
|
||||
self.assertEqual('MockConstant(b)', repr(self.const_b))
|
||||
|
||||
def test_equality(self):
|
||||
const_a_prime = self.MockConstant('a')
|
||||
self.assertFalse(self.const_a == self.const_b)
|
||||
self.assertTrue(self.const_a == const_a_prime)
|
||||
|
||||
self.assertTrue(self.const_a != self.const_b)
|
||||
self.assertFalse(self.const_a != const_a_prime)
|
||||
|
||||
|
||||
class RegistrationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Registration."""
|
||||
|
||||
def setUp(self):
|
||||
key = jose.jwk.JWKRSA(key=KEY.publickey())
|
||||
contact = (
|
||||
'mailto:admin@foo.com',
|
||||
'tel:1234',
|
||||
)
|
||||
recovery_token = 'XYZ'
|
||||
agreement = 'https://letsencrypt.org/terms'
|
||||
|
||||
from acme.messages import Registration
|
||||
self.reg = Registration(
|
||||
key=key, contact=contact, recovery_token=recovery_token,
|
||||
agreement=agreement)
|
||||
|
||||
self.jobj_to = {
|
||||
'contact': contact,
|
||||
'recoveryToken': recovery_token,
|
||||
'agreement': agreement,
|
||||
'key': key,
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['key'] = key.to_json()
|
||||
|
||||
def test_from_data(self):
|
||||
from acme.messages import Registration
|
||||
reg = Registration.from_data(phone='1234', email='admin@foo.com')
|
||||
self.assertEqual(reg.contact, (
|
||||
'tel:1234',
|
||||
'mailto:admin@foo.com',
|
||||
))
|
||||
|
||||
def test_phones(self):
|
||||
self.assertEqual(('1234',), self.reg.phones)
|
||||
|
||||
def test_emails(self):
|
||||
self.assertEqual(('admin@foo.com',), self.reg.emails)
|
||||
|
||||
def test_phone(self):
|
||||
self.assertEqual('1234', self.reg.phone)
|
||||
|
||||
def test_email(self):
|
||||
self.assertEqual('admin@foo.com', self.reg.email)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj_to, self.reg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import Registration
|
||||
self.assertEqual(self.reg, Registration.from_json(self.jobj_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages import Registration
|
||||
hash(Registration.from_json(self.jobj_from))
|
||||
|
||||
|
||||
class RegistrationResourceTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.RegistrationResource."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import RegistrationResource
|
||||
self.regr = RegistrationResource(
|
||||
body=mock.sentinel.body, uri=mock.sentinel.uri,
|
||||
new_authzr_uri=mock.sentinel.new_authzr_uri,
|
||||
terms_of_service=mock.sentinel.terms_of_service)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.regr.to_json(), {
|
||||
'body': mock.sentinel.body,
|
||||
'uri': mock.sentinel.uri,
|
||||
'new_authzr_uri': mock.sentinel.new_authzr_uri,
|
||||
'terms_of_service': mock.sentinel.terms_of_service,
|
||||
})
|
||||
|
||||
|
||||
class ChallengeResourceTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.ChallengeResource."""
|
||||
|
||||
def test_uri(self):
|
||||
from acme.messages import ChallengeResource
|
||||
self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock(
|
||||
uri='http://challb'), authzr_uri='http://authz').uri)
|
||||
|
||||
|
||||
class ChallengeBodyTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.ChallengeBody."""
|
||||
|
||||
def setUp(self):
|
||||
self.chall = challenges.DNS(token='foo')
|
||||
|
||||
from acme.messages import ChallengeBody
|
||||
from acme.messages import Error
|
||||
self.assertEqual(Error.from_json(self.jmsg), self.msg)
|
||||
from acme.messages import STATUS_INVALID
|
||||
self.status = STATUS_INVALID
|
||||
error = Error(typ='serverInternal',
|
||||
detail='Unable to communicate with DNS server')
|
||||
self.challb = ChallengeBody(
|
||||
uri='http://challb', chall=self.chall, status=self.status,
|
||||
error=error)
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['message']
|
||||
del self.jmsg['moreInfo']
|
||||
self.jobj_to = {
|
||||
'uri': 'http://challb',
|
||||
'status': self.status,
|
||||
'type': 'dns',
|
||||
'token': 'foo',
|
||||
'error': error,
|
||||
}
|
||||
self.jobj_from = self.jobj_to.copy()
|
||||
self.jobj_from['status'] = 'invalid'
|
||||
self.jobj_from['error'] = {
|
||||
'type': 'urn:acme:error:serverInternal',
|
||||
'detail': 'Unable to communicate with DNS server',
|
||||
}
|
||||
|
||||
from acme.messages import Error
|
||||
msg = Error.from_json(self.jmsg)
|
||||
|
||||
self.assertTrue(msg.message is None)
|
||||
self.assertTrue(msg.more_info is None)
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jobj_to, self.challb.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import ChallengeBody
|
||||
self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages import ChallengeBody
|
||||
hash(ChallengeBody.from_json(self.jobj_from))
|
||||
|
||||
def test_proxy(self):
|
||||
self.assertEqual('foo', self.challb.token)
|
||||
|
||||
|
||||
class AuthorizationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.Authorization."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import ChallengeBody
|
||||
from acme.messages import STATUS_VALID
|
||||
self.challbs = (
|
||||
ChallengeBody(
|
||||
uri='http://challb1', status=STATUS_VALID,
|
||||
chall=challenges.SimpleHTTP(token='IlirfxKKXAsHtmzK29Pj8A')),
|
||||
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
||||
chall=challenges.DNS(token='DGyRejmCefe7v4NfDGDKfA')),
|
||||
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
|
||||
chall=challenges.RecoveryToken()),
|
||||
)
|
||||
combinations = ((0, 2), (1, 2))
|
||||
|
||||
from acme.messages import Authorization
|
||||
from acme.messages import Identifier
|
||||
from acme.messages import IDENTIFIER_FQDN
|
||||
identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com')
|
||||
self.authz = Authorization(
|
||||
identifier=identifier, combinations=combinations,
|
||||
challenges=self.challbs)
|
||||
|
||||
self.jobj_from = {
|
||||
'identifier': identifier.to_json(),
|
||||
'challenges': [challb.to_json() for challb in self.challbs],
|
||||
'combinations': combinations,
|
||||
}
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import Authorization
|
||||
Authorization.from_json(self.jobj_from)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages import Authorization
|
||||
hash(Authorization.from_json(self.jobj_from))
|
||||
|
||||
def test_resolved_combinations(self):
|
||||
self.assertEqual(self.authz.resolved_combinations, (
|
||||
(self.challbs[0], self.challbs[2]),
|
||||
(self.challbs[1], self.challbs[2]),
|
||||
))
|
||||
|
||||
|
||||
class AuthorizationResourceTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.AuthorizationResource."""
|
||||
|
||||
def test_json_de_serializable(self):
|
||||
from acme.messages import AuthorizationResource
|
||||
authzr = AuthorizationResource(
|
||||
uri=mock.sentinel.uri,
|
||||
body=mock.sentinel.body,
|
||||
new_cert_uri=mock.sentinel.new_cert_uri,
|
||||
)
|
||||
self.assertTrue(isinstance(authzr, jose.JSONDeSerializable))
|
||||
|
||||
|
||||
class CertificateRequestTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.CertificateRequest."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import CertificateRequest
|
||||
self.req = CertificateRequest(csr=CSR, authorizations=('foo',))
|
||||
|
||||
def test_json_de_serializable(self):
|
||||
self.assertTrue(isinstance(self.req, jose.JSONDeSerializable))
|
||||
from acme.messages import CertificateRequest
|
||||
self.assertEqual(
|
||||
self.req, CertificateRequest.from_json(self.req.to_json()))
|
||||
|
||||
|
||||
class CertificateResourceTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.CertificateResourceTest."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import CertificateResource
|
||||
self.certr = CertificateResource(
|
||||
body=CERT, uri=mock.sentinel.uri, authzrs=(),
|
||||
cert_chain_uri=mock.sentinel.cert_chain_uri)
|
||||
|
||||
def test_json_de_serializable(self):
|
||||
self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable))
|
||||
from acme.messages import CertificateResource
|
||||
self.assertEqual(
|
||||
self.certr, CertificateResource.from_json(self.certr.to_json()))
|
||||
|
||||
|
||||
class RevocationTest(unittest.TestCase):
|
||||
"""Tests for acme.messages.RevocationTest."""
|
||||
|
||||
def test_url(self):
|
||||
from acme.messages import Revocation
|
||||
url = 'https://letsencrypt-demo.org/acme/revoke-cert'
|
||||
self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org'))
|
||||
self.assertEqual(
|
||||
url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg'))
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import Revocation
|
||||
self.msg = Revocation()
|
||||
self.jmsg = {'type': 'revocation'}
|
||||
self.rev = Revocation(certificate=CERT)
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
def test_from_json_hashable(self):
|
||||
from acme.messages import Revocation
|
||||
self.assertEqual(Revocation.from_json(self.jmsg), self.msg)
|
||||
|
||||
|
||||
class RevocationRequestTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.sig_nonce = '\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
|
||||
|
||||
signature = other.Signature(
|
||||
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.publickey()),
|
||||
sig='eJ\xfe\x12"U\x87\x8b\xbf/ ,\xdeP\xb2\xdc1\xb00\xe5\x1dB'
|
||||
'\xfch<\xc6\x9eH@!\x1c\x16\xb2\x0b_\xc4\xddP\x89\xc8\xce?'
|
||||
'\x16g\x069I\xb9\xb3\x91\xb9\x0e$3\x9f\x87\x8e\x82\xca\xc5'
|
||||
's\xd9\xd0\xe7',
|
||||
nonce=self.sig_nonce)
|
||||
|
||||
from acme.messages import RevocationRequest
|
||||
self.msg = RevocationRequest(certificate=CERT, signature=signature)
|
||||
|
||||
self.jmsg_to = {
|
||||
'type': 'revocationRequest',
|
||||
'certificate': jose.b64encode(CERT.as_der()),
|
||||
'signature': signature,
|
||||
}
|
||||
self.jmsg_from = self.jmsg_to.copy()
|
||||
self.jmsg_from['signature'] = self.jmsg_from['signature'].to_json()
|
||||
|
||||
def test_create(self):
|
||||
from acme.messages import RevocationRequest
|
||||
self.assertEqual(self.msg, RevocationRequest.create(
|
||||
certificate=CERT, key=KEY, sig_nonce=self.sig_nonce))
|
||||
|
||||
def test_verify(self):
|
||||
self.assertTrue(self.msg.verify())
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg_to)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import RevocationRequest
|
||||
self.assertEqual(self.msg, RevocationRequest.from_json(self.jmsg_from))
|
||||
|
||||
|
||||
class StatusRequestTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.messages import StatusRequest
|
||||
self.msg = StatusRequest(token=u'O7-s9MNq1siZHlgrMzi9_A')
|
||||
self.jmsg = {
|
||||
'type': 'statusRequest',
|
||||
'token': u'O7-s9MNq1siZHlgrMzi9_A',
|
||||
}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), self.jmsg)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.messages import StatusRequest
|
||||
self.assertEqual(StatusRequest.from_json(self.jmsg), self.msg)
|
||||
hash(Revocation.from_json(self.rev.to_json()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/authorization#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for an authorization message",
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "authorization" ]
|
||||
},
|
||||
"recoveryToken" : {
|
||||
"type": "string"
|
||||
},
|
||||
"identifier" : {
|
||||
"type": "string"
|
||||
},
|
||||
"jwk": {
|
||||
"$ref": "file:acme/schemata/jwk.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/authorizationRequest#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for an authorizationRequest message",
|
||||
"type": "object",
|
||||
"required": ["type", "sessionID", "nonce", "signature", "responses"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "authorizationRequest" ]
|
||||
},
|
||||
"sessionID" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"nonce" : {
|
||||
"type": "string"
|
||||
},
|
||||
"signature" : {
|
||||
"$ref": "file:acme/schemata/signature.json"
|
||||
},
|
||||
"responses": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "$ref": "file:acme/schemata/responseobject.json" },
|
||||
{ "type": "null" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/certificate#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a certificate message",
|
||||
"type": "object",
|
||||
"required": ["type", "certificate"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "certificate" ]
|
||||
},
|
||||
"certificate" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"chain" : {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"refresh" : {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/certificateRequest#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a certificateRequest message",
|
||||
"type": "object",
|
||||
"required": ["type", "csr", "signature"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "certificateRequest" ]
|
||||
},
|
||||
"csr" : {
|
||||
"type" : "string" ,
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"signature" : {
|
||||
"$ref": "file:acme/schemata/signature.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/challenge#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a challenge message",
|
||||
"type": "object",
|
||||
"required": ["type", "sessionID", "nonce", "challenges"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "challenge" ]
|
||||
},
|
||||
"sessionID" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"nonce" : {
|
||||
"type": "string"
|
||||
},
|
||||
"challenges": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"$ref": "file:acme/schemata/challengeobject.json"
|
||||
}
|
||||
},
|
||||
"combinations": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/challengeRequest#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a challengeRequest message",
|
||||
"type": "object",
|
||||
"required": ["type", "identifier"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "challengeRequest" ]
|
||||
},
|
||||
"identifier" : {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/challengeobject#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Subschema for an individual challenge (within challenge)",
|
||||
"anyOf": [
|
||||
{ "type": "object",
|
||||
"required": ["type", "token"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "simpleHttp" ]
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type", "r", "nonce"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "dvsni" ]
|
||||
},
|
||||
"r": {
|
||||
"type" : [ "string" ],
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "recoveryContact" ]
|
||||
},
|
||||
"activationURL": {
|
||||
"type" : "string"
|
||||
},
|
||||
"successURL": {
|
||||
"type": "string"
|
||||
},
|
||||
"contact": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "recoveryToken" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type", "alg", "nonce", "hints"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "proofOfPossession" ]
|
||||
},
|
||||
"alg": {
|
||||
"type": "string"
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"hints": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"jwk": {
|
||||
"type": "object"
|
||||
},
|
||||
"certFingerprints": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]+$"
|
||||
}
|
||||
},
|
||||
"subjectKeyIdentifiers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9a-f]+$"
|
||||
}
|
||||
},
|
||||
"serialNumbers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"issuers": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"authorizedFor": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type", "token"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "dns" ]
|
||||
},
|
||||
"token": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/defer#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a defer message",
|
||||
"type": "object",
|
||||
"required": ["type", "token"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "defer" ]
|
||||
},
|
||||
"token" : {
|
||||
"type": "string"
|
||||
},
|
||||
"interval" : {
|
||||
"type": "integer"
|
||||
},
|
||||
"message": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/error#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for an error message",
|
||||
"type": "object",
|
||||
"required": ["type", "error"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "error" ]
|
||||
},
|
||||
"error" : {
|
||||
"enum" : [ "malformed", "unauthorized", "serverInternal", "nonSupported", "unknown", "badCSR" ]
|
||||
},
|
||||
"message" : {
|
||||
"type": "string"
|
||||
},
|
||||
"moreInfo": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/jwk#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a jwk (**kty RSA/e=65537 ONLY**)",
|
||||
"type": "object",
|
||||
"required": ["kty", "e", "n"],
|
||||
"properties": {
|
||||
"kty": {
|
||||
"enum" : [ "RSA" ]
|
||||
},
|
||||
"e": {
|
||||
"enum" : [ "AQAB" ]
|
||||
},
|
||||
"n": {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/responseobject#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Subschema for an individual challenge response (within authorizationRequest)",
|
||||
"anyOf": [
|
||||
{ "type": "object",
|
||||
"required": ["type", "path"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "simpleHttp" ]
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type", "s"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "dvsni" ]
|
||||
},
|
||||
"s": {
|
||||
"type" : [ "string" ],
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "recoveryContact" ]
|
||||
},
|
||||
"token": {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "recoveryToken" ]
|
||||
},
|
||||
"token": {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type", "nonce", "signature"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "proofOfPossession" ]
|
||||
},
|
||||
"nonce": {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"signature": {
|
||||
"$ref": "file:acme/schemata/signature.json"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type": {
|
||||
"enum" : [ "dns" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/revocation#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a revocation message",
|
||||
"type": "object",
|
||||
"required": ["type"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "revocation" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/revocationRequest#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a revocationRequest message",
|
||||
"type": "object",
|
||||
"required": ["type", "certificate", "signature"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "revocationRequest" ]
|
||||
},
|
||||
"certificate" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"signature" : {
|
||||
"$ref": "file:acme/schemata/signature.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,71 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/signature#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a signature (alg RS256/e=65537 or P-256 ONLY)",
|
||||
"type": "object",
|
||||
"required": ["alg", "nonce", "sig", "jwk"],
|
||||
"properties": {
|
||||
"anyOf": [
|
||||
{
|
||||
"alg" : {
|
||||
"enum" : [ "RS256" ]
|
||||
},
|
||||
"nonce" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"sig" : {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"jwk": {
|
||||
"type": "object",
|
||||
"required": ["kty", "e", "n"],
|
||||
"properties": {
|
||||
"kty": {
|
||||
"enum" : [ "RSA" ]
|
||||
},
|
||||
"e": {
|
||||
"enum" : [ "AQAB" ]
|
||||
},
|
||||
"n": {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"alg" : {
|
||||
"enum" : [ "ES256" ]
|
||||
},
|
||||
"nonce" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"sig" : {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"jwk": {
|
||||
"type": "object",
|
||||
"required": ["kty", "crv", "x", "y"],
|
||||
"properties": {
|
||||
"kty": {
|
||||
"enum" : [ "EC" ]
|
||||
},
|
||||
"crv": {
|
||||
"enum" : [ "P-256" ]
|
||||
},
|
||||
"x": {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
},
|
||||
"y": {
|
||||
"type": "string",
|
||||
"pattern": "^[-_=0-9A-Za-z]+$"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"id": "https://letsencrypt.org/schema/01/statusRequest#",
|
||||
"$schema": "http://json-schema.org/draft-04/schema#",
|
||||
"description": "Schema for a statusRequest message",
|
||||
"type": "object",
|
||||
"required": ["type", "token"],
|
||||
"properties": {
|
||||
"type" : {
|
||||
"enum" : [ "statusRequest" ]
|
||||
},
|
||||
"token" : {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,7 @@
|
|||
This directory contains scripts that install necessary OS-specific
|
||||
prerequisite dependencies (see docs/using.rst).
|
||||
prerequisite dependencies (see docs/using.rst).
|
||||
|
||||
General dependencies:
|
||||
- git-core: requirements.txt git+https://*
|
||||
- ca-certificates: communication with demo ACMO server at
|
||||
https://www.letsencrypt-demo.org, requirements.txt git+https://*
|
||||
|
|
|
|||
|
|
@ -9,11 +9,14 @@
|
|||
# - 6.0.10 "squeeze" (x64)
|
||||
# - 7.8 "wheezy" (x64)
|
||||
# - 8.0 "jessie" (x64)
|
||||
# - Raspbian:
|
||||
# - 7.8 (armhf)
|
||||
|
||||
|
||||
# virtualenv binary can be found in different packages depending on
|
||||
# distro version (#346)
|
||||
newer () {
|
||||
apt-get install -y lsb-release --no-install-recommends
|
||||
distro=$(lsb_release -si)
|
||||
# 6.0.10 => 60, 14.04 => 1404
|
||||
# TODO: in sid version==unstable
|
||||
|
|
@ -29,6 +32,8 @@ newer () {
|
|||
fi
|
||||
}
|
||||
|
||||
apt-get update
|
||||
|
||||
# you can force newer if lsb_release is not available (e.g. Docker
|
||||
# debian:jessie base image)
|
||||
if [ "$1" = "newer" ] || newer
|
||||
|
|
@ -43,7 +48,16 @@ fi
|
|||
# #276, https://github.com/martinpaljak/M2Crypto/issues/62,
|
||||
# M2Crypto setup.py:add_multiarch_paths
|
||||
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
python python-setuptools "$virtualenv" python-dev gcc swig \
|
||||
dialog libaugeas0 libssl-dev libffi-dev ca-certificates dpkg-dev
|
||||
git-core \
|
||||
python \
|
||||
python-dev \
|
||||
"$virtualenv" \
|
||||
gcc \
|
||||
swig \
|
||||
dialog \
|
||||
libaugeas0 \
|
||||
libssl-dev \
|
||||
libffi-dev \
|
||||
ca-certificates \
|
||||
dpkg-dev \
|
||||
|
|
|
|||
20
bootstrap/_rpm_common.sh
Executable file
20
bootstrap/_rpm_common.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
|
||||
# Tested with:
|
||||
# - Fedora 22 (x64)
|
||||
# - Centos 7 (x64: on AWS EC2 t2.micro, DigitalOcean droplet)
|
||||
|
||||
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
|
||||
yum install -y \
|
||||
git-core \
|
||||
python \
|
||||
python-devel \
|
||||
python-virtualenv \
|
||||
python-devel \
|
||||
gcc \
|
||||
swig \
|
||||
dialog \
|
||||
augeas-libs \
|
||||
openssl-devel \
|
||||
libffi-devel \
|
||||
ca-certificates \
|
||||
1
bootstrap/centos.sh
Symbolic link
1
bootstrap/centos.sh
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
_rpm_common.sh
|
||||
1
bootstrap/fedora.sh
Symbolic link
1
bootstrap/fedora.sh
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
_rpm_common.sh
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
:mod:`letsencrypt.network2`
|
||||
---------------------------
|
||||
|
||||
.. automodule:: letsencrypt.network2
|
||||
:members:
|
||||
5
docs/api/plugins/manual.rst
Normal file
5
docs/api/plugins/manual.rst
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
:mod:`letsencrypt.plugins.manual`
|
||||
---------------------------------
|
||||
|
||||
.. automodule:: letsencrypt.plugins.manual
|
||||
:members:
|
||||
|
|
@ -17,6 +17,14 @@ Now you can install the development packages:
|
|||
|
||||
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]
|
||||
|
||||
.. note:: `-e` (short for `--editable`) turns on *editable mode* in
|
||||
which any source code changes in the current working
|
||||
directory are "live" and no further `pip install ...`
|
||||
invocations are necessary while developing.
|
||||
|
||||
This is roughly equivalent to `python setup.py develop`. For
|
||||
more info see `man pip`.
|
||||
|
||||
The code base, including your pull requests, **must** have 100% test
|
||||
statement coverage **and** be compliant with the :ref:`coding style
|
||||
<coding-style>`.
|
||||
|
|
@ -48,7 +56,7 @@ synced to ``/vagrant``, so you can get started with:
|
|||
|
||||
vagrant ssh
|
||||
cd /vagrant
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
./venv/bin/pip install -r requirements.txt .[dev,docs,testing]
|
||||
sudo ./venv/bin/letsencrypt
|
||||
|
||||
Support for other Linux distributions coming soon.
|
||||
|
|
@ -89,7 +97,7 @@ Configurators may implement just one of those).
|
|||
There are also `~letsencrypt.interfaces.IDisplay` plugins,
|
||||
which implement bindings to alternative UI libraries.
|
||||
|
||||
.. _interfaces.py: https://github.com/letsencrypt/lets-encrypt-preview/blob/master/letsencrypt/interfaces.py
|
||||
.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py
|
||||
|
||||
|
||||
Authenticators
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Packages
|
|||
described in `#358`_. For the time being those packages are bundled
|
||||
together into a single repo, and single documentation.
|
||||
|
||||
.. _`#358`: https://github.com/letsencrypt/lets-encrypt-preview/issues/358
|
||||
.. _`#358`: https://github.com/letsencrypt/letsencrypt/issues/358
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
|
|
|||
|
|
@ -7,21 +7,19 @@
|
|||
:members:
|
||||
|
||||
|
||||
Client
|
||||
------
|
||||
|
||||
.. automodule:: acme.client
|
||||
:members:
|
||||
|
||||
|
||||
Messages
|
||||
--------
|
||||
|
||||
v00
|
||||
~~~
|
||||
|
||||
.. automodule:: acme.messages
|
||||
:members:
|
||||
|
||||
v02
|
||||
~~~
|
||||
|
||||
.. automodule:: acme.messages2
|
||||
:members:
|
||||
|
||||
|
||||
Challenges
|
||||
----------
|
||||
|
|
@ -51,9 +49,6 @@ Errors
|
|||
:members:
|
||||
|
||||
|
||||
:members:
|
||||
|
||||
|
||||
Utilities
|
||||
---------
|
||||
|
||||
|
|
|
|||
|
|
@ -5,20 +5,42 @@ Using the Let's Encrypt client
|
|||
Quick start
|
||||
===========
|
||||
|
||||
Using docker you can quickly get yourself a testing cert. From the
|
||||
Using Docker_ you can quickly get yourself a testing cert. From the
|
||||
server that the domain your requesting a cert for resolves to,
|
||||
download docker, and issue the following command
|
||||
`install Docker`_, issue the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo docker run -it --rm -p 443:443 --name letsencrypt \
|
||||
-v "/etc/letsencrypt:/etc/letsencrypt" \
|
||||
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
|
||||
quay.io/letsencrypt/lets-encrypt-preview:latest
|
||||
quay.io/letsencrypt/letsencrypt:latest
|
||||
|
||||
And follow the instructions. Your new cert will be available in
|
||||
and follow the instructions. Your new cert will be available in
|
||||
``/etc/letsencrypt/certs``.
|
||||
|
||||
.. _Docker: https://docker.com
|
||||
.. _`install Docker`: https://docs.docker.com/docker/userguide/
|
||||
|
||||
|
||||
Getting the code
|
||||
================
|
||||
|
||||
Please `install Git`_ and run the following commands:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
git clone https://github.com/letsencrypt/letsencrypt
|
||||
cd letsencrypt
|
||||
|
||||
Alternatively you could `download the ZIP archive`_ and extract the
|
||||
snapshot of our repository, but it's strongly recommended to use the
|
||||
above method instead.
|
||||
|
||||
.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
|
||||
.. _`download the ZIP archive`:
|
||||
https://github.com/letsencrypt/letsencrypt/archive/master.zip
|
||||
|
||||
|
||||
Prerequisites
|
||||
=============
|
||||
|
|
@ -30,8 +52,8 @@ are provided mainly for the :ref:`developers <hacking>` reference.
|
|||
In general:
|
||||
|
||||
* ``sudo`` is required as a suggested way of running privileged process
|
||||
* `swig`_ is required for compiling `m2crypto`_
|
||||
* `augeas`_ is required for the ``python-augeas`` bindings
|
||||
* `SWIG`_ is required for compiling `M2Crypto`_
|
||||
* `Augeas`_ is required for the Python bindings
|
||||
|
||||
|
||||
Ubuntu
|
||||
|
|
@ -54,7 +76,7 @@ For squeeze you will need to:
|
|||
- Use ``virtualenv --no-site-packages -p python`` instead of ``-p python2``.
|
||||
|
||||
|
||||
.. _`#280`: https://github.com/letsencrypt/lets-encrypt-preview/issues/280
|
||||
.. _`#280`: https://github.com/letsencrypt/letsencrypt/issues/280
|
||||
|
||||
|
||||
Mac OSX
|
||||
|
|
@ -65,25 +87,71 @@ Mac OSX
|
|||
sudo ./bootstrap/mac.sh
|
||||
|
||||
|
||||
Fedora
|
||||
------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo ./bootstrap/fedora.sh
|
||||
|
||||
|
||||
Centos 7
|
||||
--------
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo ./bootstrap/centos.sh
|
||||
|
||||
For installation run this modified command (note the trailing
|
||||
backslash):
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
SWIG_FEATURES="-includeall -D__`uname -m`__-I/usr/include/openssl" \
|
||||
./venv/bin/pip install -r requirements.txt .
|
||||
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
virtualenv --no-site-packages -p python2 venv
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
./venv/bin/pip install -r requirements.txt .
|
||||
|
||||
.. warning:: Please do **not** use ``python setup.py install``. Please
|
||||
do **not** attempt the installation commands as
|
||||
superuser/root and/or without Virtualenv_, e.g. ``sudo
|
||||
python setup.py install``, ``sudo pip install``, ``sudo
|
||||
./venv/bin/...``. These modes of operation might corrupt
|
||||
your operating system and are **not supported** by the
|
||||
Let's Encrypt team!
|
||||
|
||||
.. note:: If your operating system uses SWIG 3.0.5+, you will need to
|
||||
run ``pip install -r requirements-swig-3.0.5.txt -r
|
||||
requirements.txt .`` instead. Known affected systems:
|
||||
|
||||
* Fedora 22
|
||||
* some versions of Mac OS X
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
The letsencrypt commandline tool has a builtin help:
|
||||
To get a new certificate run:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
./venv/bin/letsencrypt auth
|
||||
|
||||
The ``letsencrypt`` commandline tool has a builtin help:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
./venv/bin/letsencrypt --help
|
||||
|
||||
|
||||
.. _augeas: http://augeas.net/
|
||||
.. _m2crypto: https://github.com/M2Crypto/M2Crypto
|
||||
.. _swig: http://www.swig.org/
|
||||
.. _Augeas: http://augeas.net/
|
||||
.. _M2Crypto: https://github.com/M2Crypto/M2Crypto
|
||||
.. _SWIG: http://www.swig.org/
|
||||
.. _Virtualenv: https://virtualenv.pypa.io
|
||||
|
|
|
|||
45
examples/acme_client.py
Normal file
45
examples/acme_client.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
"""Example script showing how to use acme client API."""
|
||||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
import M2Crypto
|
||||
|
||||
from acme import client
|
||||
from acme import messages
|
||||
from acme import jose
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
|
||||
BITS = 2048 # minimum for Boulder
|
||||
DOMAIN = 'example1.com' # example.com is ignored by Boulder
|
||||
|
||||
key = jose.JWKRSA.load(
|
||||
Crypto.PublicKey.RSA.generate(BITS).exportKey(format="PEM"))
|
||||
acme = client.Client(NEW_REG_URL, key)
|
||||
|
||||
regr = acme.register(contact=())
|
||||
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
|
||||
acme.update_registration(regr.update(
|
||||
body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
logging.debug(regr)
|
||||
|
||||
authzr = acme.request_challenges(
|
||||
identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN),
|
||||
new_authzr_uri=regr.new_authzr_uri)
|
||||
logging.debug(authzr)
|
||||
|
||||
authzr, authzr_response = acme.poll(authzr)
|
||||
|
||||
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'csr.der')),
|
||||
M2Crypto.X509.FORMAT_DER)
|
||||
try:
|
||||
acme.request_issuance(csr, (authzr,))
|
||||
except messages.Error as error:
|
||||
print ("This script is doomed to fail as no authorization "
|
||||
"challenges are ever solved. Error from server: {0}".format(error))
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
import pkg_resources
|
||||
|
||||
import M2Crypto
|
||||
|
||||
from acme import messages2
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt import network2
|
||||
|
||||
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
|
||||
|
||||
key = jose.JWKRSA.load(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
|
||||
net = network2.Network(NEW_REG_URL, key)
|
||||
|
||||
regr = net.register(contact=(
|
||||
'mailto:cert-admin@example.com', 'tel:+12025551212'))
|
||||
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
|
||||
net.update_registration(regr.update(
|
||||
body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
logging.debug(regr)
|
||||
|
||||
authzr = net.request_challenges(
|
||||
identifier=messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example1.com'),
|
||||
new_authzr_uri=regr.new_authzr_uri)
|
||||
logging.debug(authzr)
|
||||
|
||||
authzr, authzr_response = net.poll(authzr)
|
||||
|
||||
csr = M2Crypto.X509.load_request_string(pkg_resources.resource_string(
|
||||
'letsencrypt.tests', os.path.join('testdata', 'csr.pem')))
|
||||
try:
|
||||
net.request_issuance(csr, (authzr,))
|
||||
except messages2.Error as error:
|
||||
print error.detail
|
||||
|
|
@ -6,7 +6,7 @@ import re
|
|||
import configobj
|
||||
import zope.component
|
||||
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
|
|
@ -28,7 +28,7 @@ class Account(object):
|
|||
:ivar str phone: Client's phone number
|
||||
|
||||
:ivar regr: Registration Resource
|
||||
:type regr: :class:`~acme.messages2.RegistrationResource`
|
||||
:type regr: :class:`~acme.messages.RegistrationResource`
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ class Account(object):
|
|||
acc_config = configobj.ConfigObj(
|
||||
infile=config_fp, file_error=True, create_empty=False)
|
||||
except IOError:
|
||||
raise errors.LetsEncryptClientError(
|
||||
raise errors.Error(
|
||||
"Account for %s does not exist" % os.path.basename(config_fp))
|
||||
|
||||
if os.path.basename(config_fp) != "default":
|
||||
|
|
@ -141,11 +141,11 @@ class Account(object):
|
|||
|
||||
if "RegistrationResource" in acc_config:
|
||||
acc_config_rr = acc_config["RegistrationResource"]
|
||||
regr = messages2.RegistrationResource(
|
||||
regr = messages.RegistrationResource(
|
||||
uri=acc_config_rr["uri"],
|
||||
new_authzr_uri=acc_config_rr["new_authzr_uri"],
|
||||
terms_of_service=acc_config_rr["terms_of_service"],
|
||||
body=messages2.Registration.from_json(acc_config_rr["body"]))
|
||||
body=messages.Registration.from_json(acc_config_rr["body"]))
|
||||
else:
|
||||
regr = None
|
||||
|
||||
|
|
@ -186,12 +186,12 @@ class Account(object):
|
|||
"""
|
||||
while True:
|
||||
code, email = zope.component.getUtility(interfaces.IDisplay).input(
|
||||
"Enter email address (optional, press Enter to skip)")
|
||||
"Enter email address")
|
||||
|
||||
if code == display_util.OK:
|
||||
try:
|
||||
return cls.from_email(config, email)
|
||||
except errors.LetsEncryptClientError:
|
||||
except errors.Error:
|
||||
continue
|
||||
else:
|
||||
return None
|
||||
|
|
@ -205,8 +205,7 @@ class Account(object):
|
|||
|
||||
:param str email: Email address
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptClientError: If invalid
|
||||
email address is given.
|
||||
:raises .errors.Error: If invalid email address is given.
|
||||
|
||||
"""
|
||||
if not email or cls.safe_email(email):
|
||||
|
|
@ -219,7 +218,7 @@ class Account(object):
|
|||
cls._get_config_filename(email))
|
||||
return cls(config, key, email)
|
||||
|
||||
raise errors.LetsEncryptClientError("Invalid email address.")
|
||||
raise errors.Error("Invalid email address.")
|
||||
|
||||
@classmethod
|
||||
def safe_email(cls, email):
|
||||
|
|
@ -227,5 +226,5 @@ class Account(object):
|
|||
if cls.EMAIL_REGEX.match(email):
|
||||
return not email.startswith(".") and ".." not in email
|
||||
else:
|
||||
logging.warn("Invalid email address.")
|
||||
logging.warn("Invalid email address: %s.", email)
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ Please use names such as ``achall`` to distiguish from variables "of type"
|
|||
and :class:`.ChallengeBody` (denoted by ``challb``)::
|
||||
|
||||
from acme import challenges
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
from letsencrypt import achallenges
|
||||
|
||||
chall = challenges.DNS(token='foo')
|
||||
challb = messages2.ChallengeBody(chall=chall)
|
||||
challb = messages.ChallengeBody(chall=chall)
|
||||
achall = achallenges.DNS(chall=challb, domain='example.com')
|
||||
|
||||
Note, that all annotated challenges act as a proxy objects::
|
||||
|
|
|
|||
|
|
@ -52,10 +52,10 @@ class AugeasConfigurator(common.Plugin):
|
|||
lens_path = self.aug.get(path + "/lens")
|
||||
# As aug.get may return null
|
||||
if lens_path and lens in lens_path:
|
||||
# Strip off /augeas/files and /error
|
||||
logging.error("There has been an error in parsing the file: %s",
|
||||
path[13:len(path) - 6])
|
||||
logging.error(self.aug.get(path + "/message"))
|
||||
logging.error(
|
||||
"There has been an error in parsing the file (%s): %s",
|
||||
# Strip off /augeas/files and /error
|
||||
path[13:len(path) - 6], self.aug.get(path + "/message"))
|
||||
|
||||
def save(self, title=None, temporary=False):
|
||||
"""Saves all changes to the configuration files.
|
||||
|
|
@ -122,13 +122,10 @@ class AugeasConfigurator(common.Plugin):
|
|||
# Check for the root of save problems
|
||||
new_errs = self.aug.match("/augeas//error")
|
||||
# logging.error("During Save - %s", mod_conf)
|
||||
# Only print new errors caused by recent save
|
||||
for err in new_errs:
|
||||
if err not in ex_errs:
|
||||
logging.error(
|
||||
"Unable to save file - %s", err[13:len(err) - 6])
|
||||
logging.error("Attempted Save Notes")
|
||||
logging.error(self.save_notes)
|
||||
logging.error("Unable to save files: %s. Attempted Save Notes: %s",
|
||||
", ".join(err[13:len(err) - 6] for err in new_errs
|
||||
# Only new errors caused by recent save
|
||||
if err not in ex_errs), self.save_notes)
|
||||
|
||||
# Wrapper functions for Reverter class
|
||||
def recovery_routine(self):
|
||||
|
|
|
|||
|
|
@ -3,12 +3,15 @@ import itertools
|
|||
import logging
|
||||
import time
|
||||
|
||||
import zope.component
|
||||
|
||||
from acme import challenges
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
|
||||
|
||||
class AuthHandler(object):
|
||||
|
|
@ -24,13 +27,13 @@ class AuthHandler(object):
|
|||
|
||||
:ivar network: Network object for sending and receiving authorization
|
||||
messages
|
||||
:type network: :class:`letsencrypt.network2.Network`
|
||||
:type network: :class:`letsencrypt.network.Network`
|
||||
|
||||
:ivar account: Client's Account
|
||||
:type account: :class:`letsencrypt.account.Account`
|
||||
|
||||
:ivar dict authzr: ACME Authorization Resource dict where keys are domains
|
||||
and values are :class:`acme.messages2.AuthorizationResource`
|
||||
and values are :class:`acme.messages.AuthorizationResource`
|
||||
:ivar list dv_c: DV challenges in the form of
|
||||
:class:`letsencrypt.achallenges.AnnotatedChallenge`
|
||||
:ivar list cont_c: Continuity challenges in the
|
||||
|
|
@ -60,7 +63,7 @@ class AuthHandler(object):
|
|||
form of (`completed`, `failed`)
|
||||
:rtype: tuple
|
||||
|
||||
:raises AuthorizationError: If unable to retrieve all
|
||||
:raises .AuthorizationError: If unable to retrieve all
|
||||
authorizations
|
||||
|
||||
"""
|
||||
|
|
@ -82,7 +85,7 @@ class AuthHandler(object):
|
|||
self.verify_authzr_complete()
|
||||
# Only return valid authorizations
|
||||
return [authzr for authzr in self.authzr.values()
|
||||
if authzr.body.status == messages2.STATUS_VALID]
|
||||
if authzr.body.status == messages.STATUS_VALID]
|
||||
|
||||
def _choose_challenges(self, domains):
|
||||
"""Retrieve necessary challenges to satisfy server."""
|
||||
|
|
@ -134,9 +137,11 @@ class AuthHandler(object):
|
|||
self._send_responses(self.cont_c, cont_resp, chall_update))
|
||||
|
||||
# Check for updated status...
|
||||
self._poll_challenges(chall_update, best_effort)
|
||||
# This removes challenges from self.dv_c and self.cont_c
|
||||
self._cleanup_challenges(active_achalls)
|
||||
try:
|
||||
self._poll_challenges(chall_update, best_effort)
|
||||
finally:
|
||||
# This removes challenges from self.dv_c and self.cont_c
|
||||
self._cleanup_challenges(active_achalls)
|
||||
|
||||
def _send_responses(self, achalls, resps, chall_update):
|
||||
"""Send responses and make sure errors are handled.
|
||||
|
|
@ -150,6 +155,9 @@ class AuthHandler(object):
|
|||
# Don't send challenges for None and False authenticator responses
|
||||
if resp:
|
||||
self.network.answer_challenge(achall.challb, resp)
|
||||
# TODO: answer_challenge returns challr, with URI,
|
||||
# that can be used in _find_updated_challr
|
||||
# comparisons...
|
||||
active_achalls.append(achall)
|
||||
if achall.domain in chall_update:
|
||||
chall_update[achall.domain].append(achall)
|
||||
|
|
@ -168,23 +176,28 @@ class AuthHandler(object):
|
|||
while dom_to_check and rounds < max_rounds:
|
||||
# TODO: Use retry-after...
|
||||
time.sleep(min_sleep)
|
||||
all_failed_achalls = set()
|
||||
for domain in dom_to_check:
|
||||
comp_challs, failed_challs = self._handle_check(
|
||||
comp_achalls, failed_achalls = self._handle_check(
|
||||
domain, chall_update[domain])
|
||||
|
||||
if len(comp_challs) == len(chall_update[domain]):
|
||||
if len(comp_achalls) == len(chall_update[domain]):
|
||||
comp_domains.add(domain)
|
||||
elif not failed_challs:
|
||||
for chall in comp_challs:
|
||||
chall_update[domain].remove(chall)
|
||||
elif not failed_achalls:
|
||||
for achall, _ in comp_achalls:
|
||||
chall_update[domain].remove(achall)
|
||||
# We failed some challenges... damage control
|
||||
else:
|
||||
# Right now... just assume a loss and carry on...
|
||||
if best_effort:
|
||||
comp_domains.add(domain)
|
||||
else:
|
||||
raise errors.AuthorizationError(
|
||||
"Failed Authorization procedure for %s" % domain)
|
||||
all_failed_achalls.update(
|
||||
updated for _, updated in failed_achalls)
|
||||
|
||||
if all_failed_achalls:
|
||||
_report_failed_challs(all_failed_achalls)
|
||||
raise errors.FailedChallenges(all_failed_achalls)
|
||||
|
||||
dom_to_check -= comp_domains
|
||||
comp_domains.clear()
|
||||
|
|
@ -196,38 +209,37 @@ class AuthHandler(object):
|
|||
failed = []
|
||||
|
||||
self.authzr[domain], _ = self.network.poll(self.authzr[domain])
|
||||
if self.authzr[domain].body.status == messages2.STATUS_VALID:
|
||||
if self.authzr[domain].body.status == messages.STATUS_VALID:
|
||||
return achalls, []
|
||||
|
||||
# Note: if the whole authorization is invalid, the individual failed
|
||||
# challenges will be determined here...
|
||||
for achall in achalls:
|
||||
status = self._get_chall_status(self.authzr[domain], achall)
|
||||
updated_achall = achall.update(challb=self._find_updated_challb(
|
||||
self.authzr[domain], achall))
|
||||
|
||||
# This does nothing for challenges that have yet to be decided yet.
|
||||
if status == messages2.STATUS_VALID:
|
||||
completed.append(achall)
|
||||
elif status == messages2.STATUS_INVALID:
|
||||
failed.append(achall)
|
||||
if updated_achall.status == messages.STATUS_VALID:
|
||||
completed.append((achall, updated_achall))
|
||||
elif updated_achall.status == messages.STATUS_INVALID:
|
||||
failed.append((achall, updated_achall))
|
||||
|
||||
return completed, failed
|
||||
|
||||
def _get_chall_status(self, authzr, achall): # pylint: disable=no-self-use
|
||||
"""Get the status of the challenge.
|
||||
def _find_updated_challb(self, authzr, achall): # pylint: disable=no-self-use
|
||||
"""Find updated challenge body within Authorization Resource.
|
||||
|
||||
.. warning:: This assumes only one instance of type of challenge in
|
||||
each challenge resource.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: :class:`acme.messages2.AuthorizationResource`
|
||||
|
||||
:param achall: Annotated challenge for which to get status
|
||||
:type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge`
|
||||
:param .AuthorizationResource authzr: Authorization Resource
|
||||
:param .AnnotatedChallenge achall: Annotated challenge for which
|
||||
to get status
|
||||
|
||||
"""
|
||||
for authzr_challb in authzr.body.challenges:
|
||||
if type(authzr_challb.chall) is type(achall.challb.chall):
|
||||
return authzr_challb.status
|
||||
return authzr_challb
|
||||
raise errors.AuthorizationError(
|
||||
"Target challenge not found in authorization resource")
|
||||
|
||||
|
|
@ -277,8 +289,8 @@ class AuthHandler(object):
|
|||
|
||||
"""
|
||||
for authzr in self.authzr.values():
|
||||
if (authzr.body.status != messages2.STATUS_VALID and
|
||||
authzr.body.status != messages2.STATUS_INVALID):
|
||||
if (authzr.body.status != messages.STATUS_VALID and
|
||||
authzr.body.status != messages.STATUS_INVALID):
|
||||
raise errors.AuthorizationError("Incomplete authorizations")
|
||||
|
||||
def _challenge_factory(self, domain, path):
|
||||
|
|
@ -294,8 +306,7 @@ class AuthHandler(object):
|
|||
:class:`letsencrypt.achallenges.Indexed`
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptClientError: If Challenge type is not
|
||||
recognized
|
||||
:raises .errors.Error: if challenge type is not recognized
|
||||
|
||||
"""
|
||||
dv_chall = []
|
||||
|
|
@ -319,7 +330,7 @@ def challb_to_achall(challb, key, domain):
|
|||
"""Converts a ChallengeBody object to an AnnotatedChallenge.
|
||||
|
||||
:param challb: ChallengeBody
|
||||
:type challb: :class:`acme.messages2.ChallengeBody`
|
||||
:type challb: :class:`acme.messages.ChallengeBody`
|
||||
|
||||
:param key: Key
|
||||
:type key: :class:`letsencrypt.le_util.Key`
|
||||
|
|
@ -331,35 +342,28 @@ def challb_to_achall(challb, key, domain):
|
|||
|
||||
"""
|
||||
chall = challb.chall
|
||||
logging.info("%s challenge for %s", chall.typ, domain)
|
||||
|
||||
if isinstance(chall, challenges.DVSNI):
|
||||
logging.info(" DVSNI challenge for %s.", domain)
|
||||
return achallenges.DVSNI(
|
||||
challb=challb, domain=domain, key=key)
|
||||
elif isinstance(chall, challenges.SimpleHTTP):
|
||||
logging.info(" SimpleHTTP challenge for %s.", domain)
|
||||
return achallenges.SimpleHTTP(
|
||||
challb=challb, domain=domain, key=key)
|
||||
elif isinstance(chall, challenges.DNS):
|
||||
logging.info(" DNS challenge for %s.", domain)
|
||||
return achallenges.DNS(challb=challb, domain=domain)
|
||||
|
||||
elif isinstance(chall, challenges.RecoveryToken):
|
||||
logging.info(" Recovery Token Challenge for %s.", domain)
|
||||
return achallenges.RecoveryToken(challb=challb, domain=domain)
|
||||
elif isinstance(chall, challenges.RecoveryContact):
|
||||
logging.info(" Recovery Contact Challenge for %s.", domain)
|
||||
return achallenges.RecoveryContact(
|
||||
challb=challb, domain=domain)
|
||||
elif isinstance(chall, challenges.ProofOfPossession):
|
||||
logging.info(" Proof-of-Possession Challenge for %s", domain)
|
||||
return achallenges.ProofOfPossession(
|
||||
challb=challb, domain=domain)
|
||||
|
||||
else:
|
||||
raise errors.LetsEncryptClientError(
|
||||
"Received unsupported challenge of type: %s",
|
||||
chall.typ)
|
||||
raise errors.Error(
|
||||
"Received unsupported challenge of type: %s", chall.typ)
|
||||
|
||||
|
||||
def gen_challenge_path(challbs, preferences, combinations):
|
||||
|
|
@ -368,8 +372,8 @@ def gen_challenge_path(challbs, preferences, combinations):
|
|||
.. todo:: This can be possibly be rewritten to use resolved_combinations.
|
||||
|
||||
:param tuple challbs: A tuple of challenges
|
||||
(:class:`acme.messages2.Challenge`) from
|
||||
:class:`acme.messages2.AuthorizationResource` to be
|
||||
(:class:`acme.messages.Challenge`) from
|
||||
:class:`acme.messages.AuthorizationResource` to be
|
||||
fulfilled by the client in order to prove possession of the
|
||||
identifier.
|
||||
|
||||
|
|
@ -480,3 +484,80 @@ def is_preferred(offered_challb, satisfied,
|
|||
different=True):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
_ERROR_HELP_COMMON = (
|
||||
"To fix these errors, please make sure that your domain name was entered "
|
||||
"correctly and the DNS A/AAAA record(s) for that domain contains 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.",
|
||||
"dnssec" :
|
||||
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
|
||||
"your domain, please ensure 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 "
|
||||
"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.",
|
||||
"unauthorized" : _ERROR_HELP_COMMON,
|
||||
"unknownHost" : _ERROR_HELP_COMMON,}
|
||||
|
||||
|
||||
def _report_failed_challs(failed_achalls):
|
||||
"""Notifies the user about failed challenges.
|
||||
|
||||
:param set failed_achalls: A set of failed
|
||||
:class:`letsencrypt.achallenges.AnnotatedChallenge`.
|
||||
|
||||
"""
|
||||
problems = dict()
|
||||
for achall in failed_achalls:
|
||||
if achall.error:
|
||||
problems.setdefault(achall.error.typ, []).append(achall)
|
||||
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
for achalls in problems.itervalues():
|
||||
reporter.add_message(
|
||||
_generate_failed_chall_msg(achalls), reporter.MEDIUM_PRIORITY, True)
|
||||
|
||||
|
||||
def _generate_failed_chall_msg(failed_achalls):
|
||||
"""Creates a user friendly error message about failed challenges.
|
||||
|
||||
:param list failed_achalls: A list of failed
|
||||
:class:`letsencrypt.achallenges.AnnotatedChallenge` with the same error
|
||||
type.
|
||||
|
||||
:returns: A formatted error message for the client.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
typ = failed_achalls[0].error.typ
|
||||
msg = [
|
||||
"The following '{0}' errors were reported by the server:".format(typ)]
|
||||
|
||||
problems = dict()
|
||||
for achall in failed_achalls:
|
||||
problems.setdefault(achall.error.description, set()).add(achall.domain)
|
||||
for problem in problems:
|
||||
msg.append("\n\nDomains: ")
|
||||
msg.append(", ".join(sorted(problems[problem])))
|
||||
msg.append("\nError: {0}".format(problem))
|
||||
|
||||
if typ in _ERROR_HELP:
|
||||
msg.append("\n\n")
|
||||
msg.append(_ERROR_HELP[typ])
|
||||
|
||||
return "".join(msg)
|
||||
|
|
|
|||
|
|
@ -25,9 +25,49 @@ from letsencrypt import reporter
|
|||
|
||||
from letsencrypt.display import util as display_util
|
||||
from letsencrypt.display import ops as display_ops
|
||||
|
||||
from letsencrypt.plugins import disco as plugins_disco
|
||||
|
||||
# Argparse's help formatting has a lot of unhelpful peculiarities, so we want
|
||||
# to replace as much of it as we can...
|
||||
|
||||
# This is the stub to include in help generated by argparse
|
||||
|
||||
SHORT_USAGE = """
|
||||
letsencrypt [SUBCOMMAND] [options] [domains]
|
||||
|
||||
The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates. By
|
||||
default, it will attempt to use a webserver both for obtaining and installing
|
||||
the cert. """
|
||||
|
||||
# This is the short help for letsencrypt --help, where we disable argparse
|
||||
# altogether
|
||||
USAGE = SHORT_USAGE + """Major SUBCOMMANDS are:
|
||||
|
||||
(default) everything Obtain & install a cert in your current webserver
|
||||
auth Authenticate & obtain cert, but do not install it
|
||||
install Install a previously obtained cert in a server
|
||||
revoke Revoke a previously obtained certificate
|
||||
rollback Rollback server configuration changes made during install
|
||||
config-changes Show changes made to server config during installation
|
||||
|
||||
Choice of server for authentication/installation:
|
||||
|
||||
--apache Use the Apache plugin for authentication & installation
|
||||
--nginx Use the Nginx plugin for authentication & installation
|
||||
--standalone Run a standalone HTTPS server (for authentication only)
|
||||
OR:
|
||||
--authenticator standalone --installer nginx
|
||||
|
||||
More detailed help:
|
||||
|
||||
-h, --help [topic] print this message, or detailed help on a topic;
|
||||
the available topics are:
|
||||
|
||||
all, apache, automation, nginx, paths, security, testing, or any of the
|
||||
sucommands
|
||||
"""
|
||||
|
||||
|
||||
|
||||
def _account_init(args, config):
|
||||
le_util.make_or_verify_dir(
|
||||
|
|
@ -115,13 +155,14 @@ def run(args, config, plugins):
|
|||
|
||||
|
||||
def auth(args, config, plugins):
|
||||
"""Obtain a certificate (no install)."""
|
||||
"""Authenticate & obtain cert, but do not install it."""
|
||||
# XXX: Update for renewer / RenewableCert
|
||||
|
||||
if args.domains is not None and args.csr is not None:
|
||||
# TODO: --csr could have a priority, when --domains is
|
||||
# supplied, check if CSR matches given domains?
|
||||
return "--domains and --csr are mutually exclusive"
|
||||
|
||||
# XXX: Update for renewer / RenewableCert
|
||||
acc = _account_init(args, config)
|
||||
if acc is None:
|
||||
return None
|
||||
|
|
@ -150,7 +191,7 @@ def auth(args, config, plugins):
|
|||
return "Certificate could not be obtained"
|
||||
|
||||
def install(args, config, plugins):
|
||||
"""Install (no auth)."""
|
||||
"""Install a previously obtained cert in a server."""
|
||||
# XXX: Update for renewer/RenewableCert
|
||||
acc = _account_init(args, config)
|
||||
if acc is None:
|
||||
|
|
@ -167,7 +208,7 @@ def install(args, config, plugins):
|
|||
|
||||
|
||||
def revoke(args, unused_config, unused_plugins):
|
||||
"""Revoke."""
|
||||
"""Revoke a previously obtained certificate."""
|
||||
if args.rev_cert is None and args.rev_key is None:
|
||||
return "At least one of --certificate or --key is required"
|
||||
|
||||
|
|
@ -179,12 +220,12 @@ def revoke(args, unused_config, unused_plugins):
|
|||
|
||||
|
||||
def rollback(args, config, plugins):
|
||||
"""Rollback."""
|
||||
"""Rollback server configuration changes made during install."""
|
||||
client.rollback(args.installer, args.checkpoints, config, plugins)
|
||||
|
||||
|
||||
def config_changes(unused_args, config, unused_plugins):
|
||||
"""View config changes.
|
||||
"""Show changes made to server config during installation
|
||||
|
||||
View checkpoints and associated configuration changes.
|
||||
|
||||
|
|
@ -193,7 +234,7 @@ def config_changes(unused_args, config, unused_plugins):
|
|||
|
||||
|
||||
def plugins_cmd(args, config, plugins): # TODO: Use IDiplay rathern than print
|
||||
"""List plugins."""
|
||||
"""List server software plugins."""
|
||||
logging.debug("Expected interfaces: %s", args.ifaces)
|
||||
|
||||
ifaces = [] if args.ifaces is None else args.ifaces
|
||||
|
|
@ -240,51 +281,201 @@ def flag_default(name):
|
|||
"""Default value for CLI flag."""
|
||||
return constants.CLI_DEFAULTS[name]
|
||||
|
||||
def config_help(name):
|
||||
|
||||
def config_help(name, hidden=False):
|
||||
"""Help message for `.IConfig` attribute."""
|
||||
return interfaces.IConfig[name].__doc__
|
||||
if hidden:
|
||||
return argparse.SUPPRESS
|
||||
else:
|
||||
return interfaces.IConfig[name].__doc__
|
||||
|
||||
|
||||
def create_parser(plugins):
|
||||
class SilentParser(object): # pylint: disable=too-few-public-methods
|
||||
"""Silent wrapper around argparse.
|
||||
|
||||
A mini parser wrapper that doesn't print help for its
|
||||
arguments. This is needed for the use of callbacks to define
|
||||
arguments within plugins.
|
||||
|
||||
"""
|
||||
def __init__(self, parser):
|
||||
self.parser = parser
|
||||
def add_argument(self, *args, **kwargs):
|
||||
"""Wrap, but silence help"""
|
||||
kwargs["help"] = argparse.SUPPRESS
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
|
||||
|
||||
HELP_TOPICS = ["all", "security", "paths", "automation", "testing"]
|
||||
|
||||
|
||||
class HelpfulArgumentParser(object):
|
||||
"""Argparse Wrapper.
|
||||
|
||||
This class wraps argparse, adding the ability to make --help less
|
||||
verbose, and request help on specific subcategories at a time, eg
|
||||
'letsencrypt --help security' for security options.
|
||||
|
||||
"""
|
||||
def __init__(self, args, plugins):
|
||||
self.args = args
|
||||
plugin_names = [name for name, _p in plugins.iteritems()]
|
||||
self.help_topics = HELP_TOPICS + plugin_names
|
||||
self.parser = configargparse.ArgParser(
|
||||
usage=SHORT_USAGE,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
args_for_setting_config_path=["-c", "--config"],
|
||||
default_config_files=flag_default("config_files"))
|
||||
|
||||
# This is the only way to turn off overly verbose config flag documentation
|
||||
self.parser._add_config_file_help = False # pylint: disable=protected-access
|
||||
self.silent_parser = SilentParser(self.parser)
|
||||
|
||||
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 == True:
|
||||
# just --help with no topic; avoid argparse altogether
|
||||
print USAGE
|
||||
sys.exit(0)
|
||||
self.visible_topics = self.determine_help_topics(help_arg)
|
||||
#print self.visible_topics
|
||||
self.groups = {} # elements are added by .add_group()
|
||||
self.add_plugin_args(plugins)
|
||||
|
||||
def prescan_for_flag(self, flag, possible_arguments):
|
||||
"""Checks cli input for flags.
|
||||
|
||||
Check for a flag, which accepts a fixed set of possible arguments, in
|
||||
the command line; we will use this information to configure argparse's
|
||||
help correctly. Return the flag's argument, if it has one that matches
|
||||
the sequence @possible_arguments; otherwise return whether the flag is
|
||||
present.
|
||||
|
||||
"""
|
||||
if flag not in self.args:
|
||||
return False
|
||||
pos = self.args.index(flag)
|
||||
try:
|
||||
nxt = self.args[pos + 1]
|
||||
if nxt in possible_arguments:
|
||||
return nxt
|
||||
except IndexError:
|
||||
pass
|
||||
return True
|
||||
|
||||
def add(self, topic, *args, **kwargs):
|
||||
"""Add a new command line argument.
|
||||
|
||||
@topic is required, to indicate which part of the help will document
|
||||
it, but can be None for `always documented'.
|
||||
|
||||
"""
|
||||
if topic and self.visible_topics[topic]:
|
||||
group = self.groups[topic]
|
||||
group.add_argument(*args, **kwargs)
|
||||
else:
|
||||
kwargs["help"] = argparse.SUPPRESS
|
||||
self.parser.add_argument(*args, **kwargs)
|
||||
|
||||
def add_group(self, topic, **kwargs):
|
||||
"""
|
||||
|
||||
This has to be called once for every topic; but we leave those calls
|
||||
next to the argument definitions for clarity. Return something
|
||||
arguments can be added to if necessary, either the parser or an argument
|
||||
group.
|
||||
|
||||
"""
|
||||
if self.visible_topics[topic]:
|
||||
#print "Adding visible group " + topic
|
||||
group = self.parser.add_argument_group(topic, **kwargs)
|
||||
self.groups[topic] = group
|
||||
return group
|
||||
else:
|
||||
#print "Invisible group " + topic
|
||||
return self.silent_parser
|
||||
|
||||
def add_plugin_args(self, plugins):
|
||||
"""
|
||||
|
||||
Let each of the plugins add its own command line arguments, which
|
||||
may or may not be displayed as help topics.
|
||||
|
||||
"""
|
||||
# TODO: plugin_parser should be called for every detected plugin
|
||||
for name, plugin_ep in plugins.iteritems():
|
||||
parser_or_group = self.add_group(name, description=plugin_ep.description)
|
||||
#print parser_or_group
|
||||
plugin_ep.plugin_cls.inject_parser_options(parser_or_group, name)
|
||||
|
||||
def determine_help_topics(self, chosen_topic):
|
||||
"""
|
||||
|
||||
The user may have requested help on a topic, return a dict of which
|
||||
topics to display. @chosen_topic has prescan_for_flag's return type
|
||||
|
||||
:returns: dict
|
||||
|
||||
"""
|
||||
# topics maps each topic to whether it should be documented by
|
||||
# argparse on the command line
|
||||
if chosen_topic == "all":
|
||||
return dict([(t, True) for t in self.help_topics])
|
||||
elif not chosen_topic:
|
||||
return dict([(t, False) for t in self.help_topics])
|
||||
else:
|
||||
return dict([(t, t == chosen_topic) for t in self.help_topics])
|
||||
|
||||
|
||||
def create_parser(plugins, args):
|
||||
"""Create parser."""
|
||||
parser = configargparse.ArgParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
args_for_setting_config_path=["-c", "--config"],
|
||||
default_config_files=flag_default("config_files"))
|
||||
add = parser.add_argument
|
||||
helpful = HelpfulArgumentParser(args, plugins)
|
||||
|
||||
# --help is automatically provided by argparse
|
||||
add("--version", action="version", version="%(prog)s {0}".format(
|
||||
letsencrypt.__version__))
|
||||
add("-v", "--verbose", dest="verbose_count", action="count",
|
||||
helpful.add(
|
||||
None, "-v", "--verbose", dest="verbose_count", action="count",
|
||||
default=flag_default("verbose_count"), help="This flag can be used "
|
||||
"multiple times to incrementally increase the verbosity of output, "
|
||||
"e.g. -vvv.")
|
||||
add("--no-confirm", dest="no_confirm", action="store_true",
|
||||
# --help is automatically provided by argparse
|
||||
|
||||
helpful.add_group(
|
||||
"automation",
|
||||
description="Arguments for automating execution & other tweaks")
|
||||
helpful.add(
|
||||
"automation", "--version", action="version",
|
||||
version="%(prog)s {0}".format(letsencrypt.__version__),
|
||||
help="show program's version number and exit")
|
||||
helpful.add(
|
||||
"automation", "--no-confirm", dest="no_confirm", action="store_true",
|
||||
help="Turn off confirmation screens, currently used for --revoke")
|
||||
add("-e", "--agree-tos", dest="tos", action="store_true",
|
||||
help="Skip the end user license agreement screen.")
|
||||
add("-t", "--text", dest="text_mode", action="store_true",
|
||||
helpful.add(
|
||||
"automation", "--agree-eula", "-e", dest="tos", action="store_true",
|
||||
help="Agree to the Let's Encrypt Subscriber Agreement")
|
||||
helpful.add(
|
||||
None, "-t", "--text", dest="text_mode", action="store_true",
|
||||
help="Use the text output instead of the curses UI.")
|
||||
|
||||
add("--no-simple-http-tls", action="store_true",
|
||||
help=config_help("no_simple_http_tls"))
|
||||
|
||||
testing_group = parser.add_argument_group(
|
||||
helpful.add_group(
|
||||
"testing", description="The following flags are meant for "
|
||||
"testing purposes only! Do NOT change them, unless you "
|
||||
"really know what you're doing!")
|
||||
testing_group.add_argument(
|
||||
"--no-verify-ssl", action="store_true",
|
||||
helpful.add(
|
||||
"testing", "--no-verify-ssl", action="store_true",
|
||||
help=config_help("no_verify_ssl"),
|
||||
default=flag_default("no_verify_ssl"))
|
||||
# TODO: apache and nginx plugins do NOT respect it
|
||||
testing_group.add_argument(
|
||||
"--dvsni-port", type=int, help=config_help("dvsni_port"),
|
||||
default=flag_default("dvsni_port"))
|
||||
helpful.add(
|
||||
"testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"),
|
||||
help=config_help("dvsni_port"))
|
||||
|
||||
helpful.add("testing", "--no-simple-http-tls", action="store_true",
|
||||
help=config_help("no_simple_http_tls"))
|
||||
|
||||
subparsers = helpful.parser.add_subparsers(metavar="SUBCOMMAND")
|
||||
|
||||
subparsers = parser.add_subparsers(metavar="SUBCOMMAND")
|
||||
def add_subparser(name, func): # pylint: disable=missing-docstring
|
||||
subparser = subparsers.add_parser(
|
||||
name, help=func.__doc__.splitlines()[0], description=func.__doc__)
|
||||
|
|
@ -301,6 +492,12 @@ def create_parser(plugins):
|
|||
parser_auth.add_argument(
|
||||
"--csr", type=read_file, help="Path to a Certificate Signing "
|
||||
"Request (CSR) in DER format.")
|
||||
parser_auth.add_argument(
|
||||
"--cert-path", default=flag_default("cert_path"),
|
||||
help="When using --csr this is where certificate is saved.")
|
||||
parser_auth.add_argument(
|
||||
"--chain-path", default=flag_default("chain_path"),
|
||||
help="When using --csr this is where certificate chain is saved.")
|
||||
|
||||
parser_plugins = add_subparser("plugins", plugins_cmd)
|
||||
parser_plugins.add_argument("--init", action="store_true")
|
||||
|
|
@ -312,25 +509,28 @@ def create_parser(plugins):
|
|||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller)
|
||||
|
||||
parser.add_argument("--configurator")
|
||||
parser.add_argument("-a", "--authenticator")
|
||||
parser.add_argument("-i", "--installer")
|
||||
helpful.add(None, "--configurator")
|
||||
helpful.add(None, "-a", "--authenticator")
|
||||
helpful.add(None, "-i", "--installer")
|
||||
|
||||
# positional arg shadows --domains, instead of appending, and
|
||||
# --domains is useful, because it can be stored in config
|
||||
#for subparser in parser_run, parser_auth, parser_install:
|
||||
# subparser.add_argument("domains", nargs="*", metavar="domain")
|
||||
|
||||
add("-d", "--domains", metavar="DOMAIN", action="append")
|
||||
add("-s", "--server", default=flag_default("server"),
|
||||
help=config_help("server"))
|
||||
add("-k", "--authkey", type=read_file,
|
||||
help="Path to the authorized key file")
|
||||
add("-m", "--email", help=config_help("email"))
|
||||
add("-B", "--rsa-key-size", type=int, metavar="N",
|
||||
helpful.add(None, "-d", "--domains", metavar="DOMAIN", action="append")
|
||||
helpful.add(None, "-k", "--accountkey", type=read_file,
|
||||
help="Path to the account key file")
|
||||
helpful.add(None, "-m", "--email", help=config_help("email"))
|
||||
|
||||
helpful.add_group(
|
||||
"security", description="Security parameters & server settings")
|
||||
helpful.add(
|
||||
"security", "-B", "--rsa-key-size", type=int, metavar="N",
|
||||
default=flag_default("rsa_key_size"), help=config_help("rsa_key_size"))
|
||||
# TODO: resolve - assumes binary logic while client.py assumes ternary.
|
||||
add("-r", "--redirect", action="store_true",
|
||||
helpful.add(
|
||||
"security", "-r", "--redirect", action="store_true",
|
||||
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.")
|
||||
|
||||
|
|
@ -346,48 +546,37 @@ def create_parser(plugins):
|
|||
default=flag_default("rollback_checkpoints"),
|
||||
help="Revert configuration N number of checkpoints.")
|
||||
|
||||
_paths_parser(parser.add_argument_group("paths"))
|
||||
_paths_parser(helpful)
|
||||
|
||||
# TODO: plugin_parser should be called for every detected plugin
|
||||
for name, plugin_ep in plugins.iteritems():
|
||||
plugin_ep.plugin_cls.inject_parser_options(
|
||||
parser.add_argument_group(
|
||||
name, description=plugin_ep.description), name)
|
||||
|
||||
return parser
|
||||
return helpful.parser
|
||||
|
||||
|
||||
def _paths_parser(parser):
|
||||
add = parser.add_argument
|
||||
add("--config-dir", default=flag_default("config_dir"),
|
||||
def _paths_parser(helpful):
|
||||
add = helpful.add
|
||||
helpful.add_group("paths", description="Arguments changing execution paths & servers")
|
||||
add("paths", "--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
add("--work-dir", default=flag_default("work_dir"),
|
||||
add("paths", "--work-dir", default=flag_default("work_dir"),
|
||||
help=config_help("work_dir"))
|
||||
add("--backup-dir", default=flag_default("backup_dir"),
|
||||
add("paths", "--backup-dir", default=flag_default("backup_dir"),
|
||||
help=config_help("backup_dir"))
|
||||
add("--key-dir", default=flag_default("key_dir"),
|
||||
add("paths", "--key-dir", default=flag_default("key_dir"),
|
||||
help=config_help("key_dir"))
|
||||
add("--cert-dir", default=flag_default("certs_dir"),
|
||||
add("paths", "--cert-dir", default=flag_default("certs_dir"),
|
||||
help=config_help("cert_dir"))
|
||||
|
||||
add("--le-vhost-ext", default="-le-ssl.conf",
|
||||
add("paths", "--le-vhost-ext", default="-le-ssl.conf",
|
||||
help=config_help("le_vhost_ext"))
|
||||
add("--cert-path", default=flag_default("cert_path"),
|
||||
help=config_help("cert_path"))
|
||||
add("--chain-path", default=flag_default("chain_path"),
|
||||
help=config_help("chain_path"))
|
||||
|
||||
add("--renewer-config-file", default=flag_default("renewer_config_file"),
|
||||
add("paths", "--renewer-config-file", default=flag_default("renewer_config_file"),
|
||||
help=config_help("renewer_config_file"))
|
||||
|
||||
return parser
|
||||
add("paths", "-s", "--server", default=flag_default("server"),
|
||||
help=config_help("server"))
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
"""Command line argument parsing and main script execution."""
|
||||
# note: arg parser internally handles --help (and exits afterwards)
|
||||
plugins = plugins_disco.PluginsRegistry.find_all()
|
||||
args = create_parser(plugins).parse_args(args)
|
||||
args = create_parser(plugins, args).parse_args(args)
|
||||
config = configuration.NamespaceConfig(args)
|
||||
|
||||
# Displayer
|
||||
|
|
|
|||
|
|
@ -12,12 +12,14 @@ from acme.jose import jwk
|
|||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import auth_handler
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import continuity_auth
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt import network2
|
||||
from letsencrypt import network
|
||||
from letsencrypt import reverter
|
||||
from letsencrypt import revoker
|
||||
from letsencrypt import storage
|
||||
|
|
@ -30,7 +32,7 @@ class Client(object):
|
|||
"""ACME protocol client.
|
||||
|
||||
:ivar network: Network object for sending and receiving messages
|
||||
:type network: :class:`letsencrypt.network2.Network`
|
||||
:type network: :class:`letsencrypt.network.Network`
|
||||
|
||||
:ivar account: Account object used for registration
|
||||
:type account: :class:`letsencrypt.account.Account`
|
||||
|
|
@ -63,7 +65,7 @@ class Client(object):
|
|||
self.installer = installer
|
||||
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
self.network = network2.Network(
|
||||
self.network = network.Network(
|
||||
config.server, jwk.JWKRSA.load(self.account.key.pem),
|
||||
verify_ssl=(not config.no_verify_ssl))
|
||||
|
||||
|
|
@ -97,7 +99,7 @@ class Client(object):
|
|||
self.account.regr = self.network.agree_to_tos(self.account.regr)
|
||||
else:
|
||||
# What is the proper response here...
|
||||
raise errors.LetsEncryptClientError("Must agree to TOS")
|
||||
raise errors.Error("Must agree to TOS")
|
||||
|
||||
self.account.save()
|
||||
self._report_new_account()
|
||||
|
|
@ -144,10 +146,9 @@ class Client(object):
|
|||
msg = ("Unable to obtain certificate because authenticator is "
|
||||
"not set.")
|
||||
logging.warning(msg)
|
||||
raise errors.LetsEncryptClientError(msg)
|
||||
raise errors.Error(msg)
|
||||
if self.account.regr is None:
|
||||
raise errors.LetsEncryptClientError(
|
||||
"Please register with the ACME server first.")
|
||||
raise errors.Error("Please register with the ACME server first.")
|
||||
|
||||
logging.debug("CSR: %s, domains: %s", csr, domains)
|
||||
|
||||
|
|
@ -232,23 +233,29 @@ class Client(object):
|
|||
# ideally should be a ConfigObj, but in this case a dict will be
|
||||
# accepted in practice.)
|
||||
params = vars(self.config.namespace)
|
||||
config = {"renewer_config_file":
|
||||
params["renewer_config_file"]} if "renewer_config_file" in params else None
|
||||
config = {}
|
||||
cli_config = configuration.RenewerConfiguration(self.config.namespace)
|
||||
|
||||
if (cli_config.config_dir != constants.CLI_DEFAULTS["config_dir"] or
|
||||
cli_config.work_dir != constants.CLI_DEFAULTS["work_dir"]):
|
||||
logging.warning(
|
||||
"Non-standard path(s), might not work with crontab installed "
|
||||
"by your operating system package manager")
|
||||
|
||||
# XXX: just to stop RenewableCert from complaining; this is
|
||||
# probably not a good solution
|
||||
chain_pem = "" if chain is None else chain.as_pem()
|
||||
renewable_cert = storage.RenewableCert.new_lineage(
|
||||
domains[0], certr.body.as_pem(), key.pem, chain_pem, params, config)
|
||||
self._report_renewal_status(renewable_cert)
|
||||
return renewable_cert
|
||||
lineage = storage.RenewableCert.new_lineage(
|
||||
domains[0], certr.body.as_pem(), key.pem, chain_pem, params,
|
||||
config, cli_config)
|
||||
self._report_renewal_status(lineage)
|
||||
return lineage
|
||||
|
||||
def _report_renewal_status(self, cert):
|
||||
# pylint: disable=no-self-use
|
||||
"""Informs the user about automatic renewal and deployment.
|
||||
|
||||
:param cert: Newly issued certificate
|
||||
:type cert: :class:`letsencrypt.storage.RenewableCert`
|
||||
:param .RenewableCert cert: Newly issued certificate
|
||||
|
||||
"""
|
||||
if ("autorenew" not in cert.configuration
|
||||
|
|
@ -267,7 +274,7 @@ class Client(object):
|
|||
|
||||
msg += ("been enabled for your certificate. These settings can be "
|
||||
"configured in the directories under {0}.").format(
|
||||
cert.configuration["renewal_configs_dir"])
|
||||
cert.cli_config.renewal_configs_dir)
|
||||
reporter = zope.component.getUtility(interfaces.IReporter)
|
||||
reporter.add_message(msg, reporter.LOW_PRIORITY, True)
|
||||
|
||||
|
|
@ -279,9 +286,8 @@ class Client(object):
|
|||
:type certr: :class:`acme.messages.Certificate`
|
||||
|
||||
:param chain_cert:
|
||||
|
||||
:param str cert_path: Path to attempt to save the cert file
|
||||
:param str chain_path: Path to attempt to save the chain file
|
||||
:param str cert_path: Candidate path to a certificate.
|
||||
:param str chain_path: Candidate path to a certificate chain.
|
||||
|
||||
:returns: cert_path, chain_path (absolute paths to the actual files)
|
||||
:rtype: `tuple` of `str`
|
||||
|
|
@ -334,7 +340,7 @@ class Client(object):
|
|||
if self.installer is None:
|
||||
logging.warning("No installer specified, client is unable to deploy"
|
||||
"the certificate")
|
||||
raise errors.LetsEncryptClientError("No installer available")
|
||||
raise errors.Error("No installer available")
|
||||
|
||||
chain_path = None if chain_path is None else os.path.abspath(chain_path)
|
||||
|
||||
|
|
@ -363,14 +369,14 @@ class Client(object):
|
|||
:param redirect: If traffic should be forwarded from HTTP to HTTPS.
|
||||
:type redirect: bool or None
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptClientError: if
|
||||
no installer is specified in the client.
|
||||
:raises .errors.Error: if no installer is specified in the
|
||||
client.
|
||||
|
||||
"""
|
||||
if self.installer is None:
|
||||
logging.warning("No installer is specified, there isn't any "
|
||||
"configuration to enhance.")
|
||||
raise errors.LetsEncryptClientError("No installer available")
|
||||
raise errors.Error("No installer available")
|
||||
|
||||
if redirect is None:
|
||||
redirect = enhancements.ask("redirect")
|
||||
|
|
@ -388,7 +394,7 @@ class Client(object):
|
|||
for dom in domains:
|
||||
try:
|
||||
self.installer.enhance(dom, "redirect")
|
||||
except errors.LetsEncryptConfiguratorError:
|
||||
except errors.ConfiguratorError:
|
||||
logging.warn("Unable to perform redirect for %s", dom)
|
||||
|
||||
self.installer.save("Add Redirects")
|
||||
|
|
@ -409,8 +415,7 @@ def validate_key_csr(privkey, csr=None):
|
|||
|
||||
:param .le_util.CSR csr: CSR
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptClientError: when
|
||||
validation fails
|
||||
:raises .errors.Error: when validation fails
|
||||
|
||||
"""
|
||||
# TODO: Handle all of these problems appropriately
|
||||
|
|
@ -419,8 +424,7 @@ def validate_key_csr(privkey, csr=None):
|
|||
|
||||
# Key must be readable and valid.
|
||||
if privkey.pem and not crypto_util.valid_privkey(privkey.pem):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The provided key is not a valid key")
|
||||
raise errors.Error("The provided key is not a valid key")
|
||||
|
||||
if csr:
|
||||
if csr.form == "der":
|
||||
|
|
@ -429,16 +433,14 @@ def validate_key_csr(privkey, csr=None):
|
|||
|
||||
# If CSR is provided, it must be readable and valid.
|
||||
if csr.data and not crypto_util.valid_csr(csr.data):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The provided CSR is not a valid CSR")
|
||||
raise errors.Error("The provided CSR is not a valid CSR")
|
||||
|
||||
# If both CSR and key are provided, the key must be the same key used
|
||||
# in the CSR.
|
||||
if csr.data and privkey.pem:
|
||||
if not crypto_util.csr_matches_pubkey(
|
||||
csr.data, privkey.pem):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"The key and CSR do not match")
|
||||
raise errors.Error("The key and CSR do not match")
|
||||
|
||||
|
||||
def determine_account(config):
|
||||
|
|
|
|||
|
|
@ -17,10 +17,15 @@ class NamespaceConfig(object):
|
|||
:attr:`~letsencrypt.interfaces.IConfig.work_dir` and relative
|
||||
paths defined in :py:mod:`letsencrypt.constants`:
|
||||
|
||||
- ``temp_checkpoint_dir``
|
||||
- ``in_progress_dir``
|
||||
- ``cert_key_backup``
|
||||
- ``rec_token_dir``
|
||||
- `accounts_dir`
|
||||
- `account_keys_dir`
|
||||
- `cert_dir`
|
||||
- `cert_key_backup`
|
||||
- `in_progress_dir`
|
||||
- `key_dir`
|
||||
- `rec_token_dir`
|
||||
- `renewer_config_file`
|
||||
- `temp_checkpoint_dir`
|
||||
|
||||
:ivar namespace: Namespace typically produced by
|
||||
:meth:`argparse.ArgumentParser.parse_args`.
|
||||
|
|
@ -35,27 +40,12 @@ class NamespaceConfig(object):
|
|||
def __getattr__(self, name):
|
||||
return getattr(self.namespace, name)
|
||||
|
||||
@property
|
||||
def temp_checkpoint_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
|
||||
|
||||
@property
|
||||
def in_progress_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
|
||||
|
||||
@property
|
||||
def server_path(self):
|
||||
"""File path based on ``server``."""
|
||||
parsed = urlparse.urlparse(self.namespace.server)
|
||||
return (parsed.netloc + parsed.path).replace('/', os.path.sep)
|
||||
|
||||
@property
|
||||
def cert_key_backup(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.work_dir, constants.CERT_KEY_BACKUP_DIR,
|
||||
self.server_path)
|
||||
|
||||
@property
|
||||
def accounts_dir(self): #pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
|
|
@ -63,11 +53,63 @@ class NamespaceConfig(object):
|
|||
|
||||
@property
|
||||
def account_keys_dir(self): #pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.config_dir, constants.ACCOUNTS_DIR,
|
||||
self.server_path, constants.ACCOUNT_KEYS_DIR)
|
||||
return os.path.join(self.accounts_dir, constants.ACCOUNT_KEYS_DIR)
|
||||
|
||||
@property
|
||||
def backup_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)
|
||||
|
||||
@property
|
||||
def cert_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
|
||||
|
||||
@property
|
||||
def cert_key_backup(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.work_dir,
|
||||
constants.CERT_KEY_BACKUP_DIR, self.server_path)
|
||||
|
||||
@property
|
||||
def in_progress_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.work_dir, constants.IN_PROGRESS_DIR)
|
||||
|
||||
@property
|
||||
def key_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.KEY_DIR)
|
||||
|
||||
# TODO: This should probably include the server name
|
||||
@property
|
||||
def rec_token_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.work_dir, constants.REC_TOKEN_DIR)
|
||||
|
||||
@property
|
||||
def temp_checkpoint_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
|
||||
|
||||
|
||||
class RenewerConfiguration(object):
|
||||
"""Configuration wrapper for renewer."""
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.namespace, name)
|
||||
|
||||
@property
|
||||
def archive_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.ARCHIVE_DIR)
|
||||
|
||||
@property
|
||||
def live_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.LIVE_DIR)
|
||||
|
||||
@property
|
||||
def renewal_configs_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.config_dir, constants.RENEWAL_CONFIGS_DIR)
|
||||
|
||||
@property
|
||||
def renewer_config_file(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(
|
||||
self.namespace.config_dir, constants.RENEWER_CONFIG_FILENAME)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from acme import challenges
|
|||
SETUPTOOLS_PLUGINS_ENTRY_POINT = "letsencrypt.plugins"
|
||||
"""Setuptools entry point group name for plugins."""
|
||||
|
||||
|
||||
CLI_DEFAULTS = dict(
|
||||
config_files=["/etc/letsencrypt/cli.ini"],
|
||||
verbose_count=-(logging.WARNING / 10),
|
||||
|
|
@ -16,23 +15,21 @@ CLI_DEFAULTS = dict(
|
|||
rollback_checkpoints=0,
|
||||
config_dir="/etc/letsencrypt",
|
||||
work_dir="/var/lib/letsencrypt",
|
||||
backup_dir="/var/lib/letsencrypt/backups",
|
||||
key_dir="/etc/letsencrypt/keys",
|
||||
certs_dir="/etc/letsencrypt/certs",
|
||||
cert_path="/etc/letsencrypt/certs/cert-letsencrypt.pem",
|
||||
chain_path="/etc/letsencrypt/certs/chain-letsencrypt.pem",
|
||||
renewer_config_file="/etc/letsencrypt/renewer.conf",
|
||||
no_verify_ssl=False,
|
||||
dvsni_port=challenges.DVSNI.PORT,
|
||||
cert_path="./cert.pem",
|
||||
chain_path="./chain.pem",
|
||||
|
||||
# TODO: blocked by #485, values ignored
|
||||
backup_dir="not used",
|
||||
key_dir="not used",
|
||||
certs_dir="not used",
|
||||
renewer_config_file="not used",
|
||||
)
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
|
||||
|
||||
RENEWER_DEFAULTS = dict(
|
||||
renewer_config_file="/etc/letsencrypt/renewer.conf",
|
||||
renewal_configs_dir="/etc/letsencrypt/configs",
|
||||
archive_dir="/etc/letsencrypt/archive",
|
||||
live_dir="/etc/letsencrypt/live",
|
||||
renewer_enabled="yes",
|
||||
renew_before_expiry="30 days",
|
||||
deploy_before_expiry="20 days",
|
||||
|
|
@ -57,31 +54,47 @@ List of expected options parameters:
|
|||
|
||||
"""
|
||||
|
||||
ARCHIVE_DIR = "archive"
|
||||
"""Archive directory, relative to `IConfig.config_dir`."""
|
||||
|
||||
CONFIG_DIRS_MODE = 0o755
|
||||
"""Directory mode for ``.IConfig.config_dir`` et al."""
|
||||
|
||||
TEMP_CHECKPOINT_DIR = "temp_checkpoint"
|
||||
"""Temporary checkpoint directory (relative to IConfig.work_dir)."""
|
||||
|
||||
IN_PROGRESS_DIR = "IN_PROGRESS"
|
||||
"""Directory used before a permanent checkpoint is finalized (relative to
|
||||
IConfig.work_dir)."""
|
||||
|
||||
CERT_KEY_BACKUP_DIR = "keys-certs"
|
||||
"""Directory where all certificates and keys are stored (relative to
|
||||
IConfig.work_dir. Used for easy revocation."""
|
||||
|
||||
ACCOUNTS_DIR = "accounts"
|
||||
"""Directory where all accounts are saved."""
|
||||
|
||||
ACCOUNT_KEYS_DIR = "keys"
|
||||
"""Directory where account keys are saved. Relative to ACCOUNTS_DIR."""
|
||||
"""Directory where account keys are saved. Relative to `ACCOUNTS_DIR`."""
|
||||
|
||||
BACKUP_DIR = "backups"
|
||||
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""
|
||||
|
||||
CERT_DIR = "certs"
|
||||
"""See `.IConfig.cert_dir`."""
|
||||
|
||||
CERT_KEY_BACKUP_DIR = "keys-certs"
|
||||
"""Directory where all certificates and keys are stored (relative to
|
||||
`IConfig.work_dir`). Used for easy revocation."""
|
||||
|
||||
IN_PROGRESS_DIR = "IN_PROGRESS"
|
||||
"""Directory used before a permanent checkpoint is finalized (relative to
|
||||
`IConfig.work_dir`)."""
|
||||
|
||||
KEY_DIR = "keys"
|
||||
"""Directory (relative to `IConfig.config_dir`) where keys are saved."""
|
||||
|
||||
LIVE_DIR = "live"
|
||||
"""Live directory, relative to `IConfig.config_dir`."""
|
||||
|
||||
TEMP_CHECKPOINT_DIR = "temp_checkpoint"
|
||||
"""Temporary checkpoint directory (relative to `IConfig.work_dir`)."""
|
||||
|
||||
REC_TOKEN_DIR = "recovery_tokens"
|
||||
"""Directory where all recovery tokens are saved (relative to
|
||||
IConfig.work_dir)."""
|
||||
`IConfig.work_dir`)."""
|
||||
|
||||
NETSTAT = "/bin/netstat"
|
||||
"""Location of netstat binary for checking whether a listener is already
|
||||
running on the specified port (Linux-specific)."""
|
||||
RENEWAL_CONFIGS_DIR = "configs"
|
||||
"""Renewal configs directory, relative to `IConfig.config_dir`."""
|
||||
|
||||
RENEWER_CONFIG_FILENAME = "renewer.conf"
|
||||
"""Renewer config file name (relative to `IConfig.config_dir`)."""
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class ContinuityAuthenticator(object):
|
|||
elif isinstance(achall, achallenges.RecoveryToken):
|
||||
responses.append(self.rec_token.perform(achall))
|
||||
else:
|
||||
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
|
||||
raise errors.ContAuthError("Unexpected Challenge")
|
||||
return responses
|
||||
|
||||
def cleanup(self, achalls):
|
||||
|
|
@ -61,4 +61,4 @@ class ContinuityAuthenticator(object):
|
|||
if isinstance(achall, achallenges.RecoveryToken):
|
||||
self.rec_token.cleanup(achall)
|
||||
elif not isinstance(achall, achallenges.ProofOfPossession):
|
||||
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
|
||||
raise errors.ContAuthError("Unexpected Challenge")
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"):
|
|||
try:
|
||||
key_pem = make_key(key_size)
|
||||
except ValueError as err:
|
||||
logging.fatal(str(err))
|
||||
logging.exception(err)
|
||||
raise err
|
||||
|
||||
# Save file
|
||||
|
|
@ -55,7 +55,7 @@ def init_save_key(key_size, key_dir, keyname="key-letsencrypt.pem"):
|
|||
return le_util.Key(key_path, key_pem)
|
||||
|
||||
|
||||
def init_save_csr(privkey, names, cert_dir, csrname="csr-letsencrypt.pem"):
|
||||
def init_save_csr(privkey, names, path, csrname="csr-letsencrypt.pem"):
|
||||
"""Initialize a CSR with the given private key.
|
||||
|
||||
:param privkey: Key to include in the CSR
|
||||
|
|
@ -63,7 +63,7 @@ def init_save_csr(privkey, names, cert_dir, csrname="csr-letsencrypt.pem"):
|
|||
|
||||
:param set names: `str` names to include in the CSR
|
||||
|
||||
:param str cert_dir: Certificate save directory.
|
||||
:param str path: Certificate save directory.
|
||||
|
||||
:returns: CSR
|
||||
:rtype: :class:`letsencrypt.le_util.CSR`
|
||||
|
|
@ -72,9 +72,9 @@ def init_save_csr(privkey, names, cert_dir, csrname="csr-letsencrypt.pem"):
|
|||
csr_pem, csr_der = make_csr(privkey.pem, names)
|
||||
|
||||
# Save CSR
|
||||
le_util.make_or_verify_dir(cert_dir, 0o755, os.geteuid())
|
||||
le_util.make_or_verify_dir(path, 0o755, os.geteuid())
|
||||
csr_f, csr_filename = le_util.unique_file(
|
||||
os.path.join(cert_dir, csrname), 0o644)
|
||||
os.path.join(path, csrname), 0o644)
|
||||
csr_f.write(csr_pem)
|
||||
csr_f.close()
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ def ask(enhancement):
|
|||
:returns: True if feature is desired, False otherwise
|
||||
:rtype: bool
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptClientError: If
|
||||
the enhancement provided is not supported.
|
||||
:raises .errors.Error: if the enhancement provided is not supported
|
||||
|
||||
"""
|
||||
try:
|
||||
|
|
@ -30,7 +29,7 @@ def ask(enhancement):
|
|||
return DISPATCH[enhancement]()
|
||||
except KeyError:
|
||||
logging.error("Unsupported enhancement given to ask(): %s", enhancement)
|
||||
raise errors.LetsEncryptClientError("Unsupported Enhancement")
|
||||
raise errors.Error("Unsupported Enhancement")
|
||||
|
||||
|
||||
def redirect_by_default():
|
||||
|
|
|
|||
|
|
@ -1,52 +1,63 @@
|
|||
"""Let's Encrypt client errors."""
|
||||
|
||||
|
||||
class LetsEncryptClientError(Exception):
|
||||
class Error(Exception):
|
||||
"""Generic Let's Encrypt client error."""
|
||||
LetsEncryptClientError = Error # TODO: blocked by #485
|
||||
|
||||
|
||||
class NetworkError(LetsEncryptClientError):
|
||||
"""Network error."""
|
||||
|
||||
|
||||
class UnexpectedUpdate(NetworkError):
|
||||
"""Unexpected update."""
|
||||
|
||||
|
||||
class LetsEncryptReverterError(LetsEncryptClientError):
|
||||
class ReverterError(Error):
|
||||
"""Let's Encrypt Reverter error."""
|
||||
|
||||
|
||||
# Auth Handler Errors
|
||||
class AuthorizationError(LetsEncryptClientError):
|
||||
class AuthorizationError(Error):
|
||||
"""Authorization error."""
|
||||
|
||||
|
||||
class LetsEncryptContAuthError(AuthorizationError):
|
||||
class FailedChallenges(AuthorizationError):
|
||||
"""Failed challenges error.
|
||||
|
||||
:ivar set failed_achalls: Failed `.AnnotatedChallenge` instances.
|
||||
|
||||
"""
|
||||
def __init__(self, failed_achalls):
|
||||
assert failed_achalls
|
||||
self.failed_achalls = failed_achalls
|
||||
super(FailedChallenges, self).__init__()
|
||||
|
||||
def __str__(self):
|
||||
return "Failed authorization procedure. {0}".format(
|
||||
", ".join(
|
||||
"{0} ({1}): {2}".format(achall.domain, achall.typ, achall.error)
|
||||
for achall in self.failed_achalls if achall.error is not None))
|
||||
|
||||
|
||||
class ContAuthError(AuthorizationError):
|
||||
"""Let's Encrypt Continuity Authenticator error."""
|
||||
|
||||
|
||||
class LetsEncryptDvAuthError(AuthorizationError):
|
||||
class DvAuthError(AuthorizationError):
|
||||
"""Let's Encrypt DV Authenticator error."""
|
||||
|
||||
|
||||
# Authenticator - Challenge specific errors
|
||||
class LetsEncryptDvsniError(LetsEncryptDvAuthError):
|
||||
class DvsniError(DvAuthError):
|
||||
"""Let's Encrypt DVSNI error."""
|
||||
|
||||
|
||||
# Configurator Errors
|
||||
class LetsEncryptConfiguratorError(LetsEncryptClientError):
|
||||
class ConfiguratorError(Error):
|
||||
"""Let's Encrypt Configurator error."""
|
||||
|
||||
|
||||
class LetsEncryptNoInstallationError(LetsEncryptConfiguratorError):
|
||||
class NoInstallationError(ConfiguratorError):
|
||||
"""Let's Encrypt No Installation error."""
|
||||
|
||||
|
||||
class LetsEncryptMisconfigurationError(LetsEncryptConfiguratorError):
|
||||
class MisconfigurationError(ConfiguratorError):
|
||||
"""Let's Encrypt Misconfiguration error."""
|
||||
|
||||
|
||||
class LetsEncryptRevokerError(LetsEncryptClientError):
|
||||
class RevokerError(Error):
|
||||
"""Let's Encrypt Revoker error."""
|
||||
|
|
|
|||
|
|
@ -68,10 +68,10 @@ class IPlugin(zope.interface.Interface):
|
|||
|
||||
Finish up any additional initialization.
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptMisconfigurationError:
|
||||
when full initialization cannot be completed. Plugin will be
|
||||
displayed on a list of available plugins.
|
||||
:raises letsencrypt.errors.LetsEncryptNoInstallationError:
|
||||
:raises .MisconfigurationError:
|
||||
when full initialization cannot be completed. Plugin will
|
||||
be displayed on a list of available plugins.
|
||||
:raises .NoInstallationError:
|
||||
when the necessary programs/files cannot be located. Plugin
|
||||
will NOT be displayed on a list of available plugins.
|
||||
|
||||
|
|
@ -148,40 +148,36 @@ class IConfig(zope.interface.Interface):
|
|||
|
||||
"""
|
||||
server = zope.interface.Attribute(
|
||||
"CA hostname (and optionally :port). The server certificate must "
|
||||
"be trusted in order to avoid further modifications to the client.")
|
||||
"ACME new registration URI (including /acme/new-reg).")
|
||||
email = zope.interface.Attribute(
|
||||
"Email used for registration and recovery contact.")
|
||||
rsa_key_size = zope.interface.Attribute("Size of the RSA key.")
|
||||
|
||||
config_dir = zope.interface.Attribute("Configuration directory.")
|
||||
work_dir = zope.interface.Attribute("Working directory.")
|
||||
backup_dir = zope.interface.Attribute("Configuration backups directory.")
|
||||
temp_checkpoint_dir = zope.interface.Attribute(
|
||||
"Temporary checkpoint directory.")
|
||||
in_progress_dir = zope.interface.Attribute(
|
||||
"Directory used before a permanent checkpoint is finalized.")
|
||||
cert_key_backup = zope.interface.Attribute(
|
||||
"Directory where all certificates and keys are stored. "
|
||||
"Used for easy revocation.")
|
||||
|
||||
accounts_dir = zope.interface.Attribute(
|
||||
"Directory where all account information is stored.")
|
||||
account_keys_dir = zope.interface.Attribute(
|
||||
"Directory where all account keys are stored.")
|
||||
backup_dir = zope.interface.Attribute("Configuration backups directory.")
|
||||
cert_dir = zope.interface.Attribute(
|
||||
"Directory where newly generated Certificate Signing Requests "
|
||||
"(CSRs) and certificates not enrolled in the renewer are saved.")
|
||||
cert_key_backup = zope.interface.Attribute(
|
||||
"Directory where all certificates and keys are stored. "
|
||||
"Used for easy revocation.")
|
||||
in_progress_dir = zope.interface.Attribute(
|
||||
"Directory used before a permanent checkpoint is finalized.")
|
||||
key_dir = zope.interface.Attribute("Keys storage.")
|
||||
rec_token_dir = zope.interface.Attribute(
|
||||
"Directory where all recovery tokens are saved.")
|
||||
key_dir = zope.interface.Attribute("Keys storage.")
|
||||
cert_dir = zope.interface.Attribute("Certificates storage.")
|
||||
|
||||
le_vhost_ext = zope.interface.Attribute(
|
||||
"SSL vhost configuration extension.")
|
||||
temp_checkpoint_dir = zope.interface.Attribute(
|
||||
"Temporary checkpoint directory.")
|
||||
|
||||
renewer_config_file = zope.interface.Attribute(
|
||||
"Location of renewal configuration file.")
|
||||
|
||||
cert_path = zope.interface.Attribute("Let's Encrypt certificate file path.")
|
||||
chain_path = zope.interface.Attribute("Let's Encrypt chain file path.")
|
||||
|
||||
no_verify_ssl = zope.interface.Attribute(
|
||||
"Disable SSL certificate verification.")
|
||||
dvsni_port = zope.interface.Attribute(
|
||||
|
|
@ -192,6 +188,9 @@ class IConfig(zope.interface.Interface):
|
|||
no_simple_http_tls = zope.interface.Attribute(
|
||||
"Do not use TLS when solving SimpleHTTP challenges.")
|
||||
|
||||
# TODO: the following are not used, but blocked by #485
|
||||
le_vhost_ext = zope.interface.Attribute("not used")
|
||||
|
||||
|
||||
class IInstaller(IPlugin):
|
||||
"""Generic Let's Encrypt Installer Interface.
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ def make_or_verify_dir(directory, mode=0o755, uid=0):
|
|||
:param int mode: Directory mode.
|
||||
:param int uid: Directory owner.
|
||||
|
||||
:raises LetsEncryptClientError: if a directory already exists,
|
||||
:raises .errors.Error: if a directory already exists,
|
||||
but has wrong permissions or owner
|
||||
|
||||
:raises OSError: if invalid or inaccessible file names and
|
||||
|
|
@ -32,9 +32,8 @@ def make_or_verify_dir(directory, mode=0o755, uid=0):
|
|||
except OSError as exception:
|
||||
if exception.errno == errno.EEXIST:
|
||||
if not check_permissions(directory, mode, uid):
|
||||
raise errors.LetsEncryptClientError(
|
||||
"%s exists, but does not have the proper "
|
||||
"permissions or owner" % directory)
|
||||
raise errors.Error(
|
||||
"%s exists, this client can't access it" % directory)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
|
|
|||
|
|
@ -1,121 +1,26 @@
|
|||
"""Network Module."""
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import requests
|
||||
|
||||
from acme import jose
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import errors
|
||||
"""Networking for ACME protocol."""
|
||||
from acme import client
|
||||
|
||||
|
||||
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
|
||||
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
|
||||
class Network(client.Client):
|
||||
"""ACME networking."""
|
||||
|
||||
logging.getLogger("requests").setLevel(logging.WARNING)
|
||||
def register_from_account(self, account):
|
||||
"""Register with server.
|
||||
|
||||
.. todo:: this should probably not be a part of network...
|
||||
|
||||
class Network(object):
|
||||
"""Class for communicating with ACME servers.
|
||||
:param account: Account
|
||||
:type account: :class:`letsencrypt.account.Account`
|
||||
|
||||
:ivar str server_url: Full URL of the ACME service
|
||||
|
||||
"""
|
||||
def __init__(self, server):
|
||||
"""Initialize Network instance.
|
||||
|
||||
:param str server: ACME (CA) server[:port]
|
||||
:returns: Updated account
|
||||
:rtype: :class:`letsencrypt.account.Account`
|
||||
|
||||
"""
|
||||
self.server_url = "https://%s/acme/" % server
|
||||
|
||||
def send(self, msg):
|
||||
"""Send ACME message to server.
|
||||
|
||||
:param msg: ACME message.
|
||||
:type msg: :class:`acme.messages.Message`
|
||||
|
||||
:returns: Server response message.
|
||||
:rtype: :class:`acme.messages.Message`
|
||||
|
||||
:raises acme.errors.ValidationError: if `msg` is not
|
||||
valid serializable ACME JSON message.
|
||||
:raises errors.LetsEncryptClientError: in case of connection error
|
||||
or if response from server is not a valid ACME message.
|
||||
|
||||
"""
|
||||
try:
|
||||
response = requests.post(
|
||||
self.server_url,
|
||||
data=msg.json_dumps(),
|
||||
headers={"Content-Type": "application/json"},
|
||||
verify=True
|
||||
)
|
||||
except requests.exceptions.RequestException as error:
|
||||
raise errors.LetsEncryptClientError(
|
||||
'Sending ACME message to server has failed: %s' % error)
|
||||
|
||||
json_string = response.json()
|
||||
try:
|
||||
return messages.Message.from_json(json_string)
|
||||
except jose.DeserializationError as error:
|
||||
logging.error(json_string)
|
||||
raise # TODO
|
||||
|
||||
def send_and_receive_expected(self, msg, expected):
|
||||
"""Send ACME message to server and return expected message.
|
||||
|
||||
:param msg: ACME message.
|
||||
:type msg: :class:`acme.Message`
|
||||
|
||||
:returns: ACME response message of expected type.
|
||||
:rtype: :class:`acme.messages.Message`
|
||||
|
||||
:raises errors.LetsEncryptClientError: An exception is thrown
|
||||
|
||||
"""
|
||||
response = self.send(msg)
|
||||
return self.is_expected_msg(response, expected)
|
||||
|
||||
|
||||
def is_expected_msg(self, response, expected, delay=3, rounds=20):
|
||||
"""Is response expected ACME message?
|
||||
|
||||
:param response: ACME response message from server.
|
||||
:type response: :class:`acme.messages.Message`
|
||||
|
||||
:param expected: Expected response type.
|
||||
:type expected: subclass of :class:`acme.messages.Message`
|
||||
|
||||
:param int delay: Number of seconds to delay before next round
|
||||
in case of ACME "defer" response message.
|
||||
:param int rounds: Number of resend attempts in case of ACME "defer"
|
||||
response message.
|
||||
|
||||
:returns: ACME response message from server.
|
||||
:rtype: :class:`acme.messages.Message`
|
||||
|
||||
:raises LetsEncryptClientError: if server sent ACME "error" message
|
||||
|
||||
"""
|
||||
for _ in xrange(rounds):
|
||||
if isinstance(response, expected):
|
||||
return response
|
||||
elif isinstance(response, messages.Error):
|
||||
logging.error("%s", response)
|
||||
raise errors.LetsEncryptClientError(response.error)
|
||||
elif isinstance(response, messages.Defer):
|
||||
logging.info("Waiting for %d seconds...", delay)
|
||||
time.sleep(delay)
|
||||
response = self.send(
|
||||
messages.StatusRequest(token=response.token))
|
||||
else:
|
||||
logging.fatal("Received unexpected message")
|
||||
logging.fatal("Expected: %s", expected)
|
||||
logging.fatal("Received: %s", response)
|
||||
sys.exit(33)
|
||||
|
||||
logging.error(
|
||||
"Server has deferred past the max of %d seconds", rounds * delay)
|
||||
details = (
|
||||
"mailto:" + account.email if account.email is not None else None,
|
||||
"tel:" + account.phone if account.phone is not None else None,
|
||||
)
|
||||
account.regr = self.register(contact=tuple(
|
||||
det for det in details if det is not None))
|
||||
return account
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
"""Plugin common functions."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
import zope.interface
|
||||
|
||||
from acme.jose import util as jose_util
|
||||
|
||||
from letsencrypt import constants
|
||||
from letsencrypt import interfaces
|
||||
|
||||
|
||||
|
|
@ -69,3 +75,127 @@ class Plugin(object):
|
|||
with unique plugin name prefix.
|
||||
|
||||
"""
|
||||
|
||||
# other
|
||||
|
||||
class Addr(object):
|
||||
r"""Represents an virtual host address.
|
||||
|
||||
:param str addr: addr part of vhost address
|
||||
:param str port: port number or \*, or ""
|
||||
|
||||
"""
|
||||
def __init__(self, tup):
|
||||
self.tup = tup
|
||||
|
||||
@classmethod
|
||||
def fromstring(cls, str_addr):
|
||||
"""Initialize Addr from string."""
|
||||
tup = str_addr.partition(':')
|
||||
return cls((tup[0], tup[2]))
|
||||
|
||||
def __str__(self):
|
||||
if self.tup[1]:
|
||||
return "%s:%s" % self.tup
|
||||
return self.tup[0]
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.tup == other.tup
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.tup)
|
||||
|
||||
def get_addr(self):
|
||||
"""Return addr part of Addr object."""
|
||||
return self.tup[0]
|
||||
|
||||
def get_port(self):
|
||||
"""Return port."""
|
||||
return self.tup[1]
|
||||
|
||||
def get_addr_obj(self, port):
|
||||
"""Return new address object with same addr and new port."""
|
||||
return self.__class__((self.tup[0], port))
|
||||
|
||||
|
||||
class Dvsni(object):
|
||||
"""Class that perform DVSNI challenges."""
|
||||
|
||||
def __init__(self, configurator):
|
||||
self.configurator = configurator
|
||||
self.achalls = []
|
||||
self.indices = []
|
||||
self.challenge_conf = os.path.join(
|
||||
configurator.config.config_dir, "le_dvsni_cert_challenge.conf")
|
||||
# self.completed = 0
|
||||
|
||||
def add_chall(self, achall, idx=None):
|
||||
"""Add challenge to DVSNI object to perform at once.
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
|
||||
:param int idx: index to challenge in a larger array
|
||||
|
||||
"""
|
||||
self.achalls.append(achall)
|
||||
if idx is not None:
|
||||
self.indices.append(idx)
|
||||
|
||||
def get_cert_file(self, achall):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
|
||||
:returns: certificate file name
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return os.path.join(
|
||||
self.configurator.config.work_dir, achall.nonce_domain + ".crt")
|
||||
|
||||
def _setup_challenge_cert(self, achall, s=None):
|
||||
# pylint: disable=invalid-name
|
||||
"""Generate and write out challenge certificate."""
|
||||
cert_path = self.get_cert_file(achall)
|
||||
# Register the path before you write out the file
|
||||
self.configurator.reverter.register_file_creation(True, cert_path)
|
||||
|
||||
cert_pem, response = achall.gen_cert_and_response(s)
|
||||
|
||||
# Write out challenge cert
|
||||
with open(cert_path, "w") as cert_chall_fd:
|
||||
cert_chall_fd.write(cert_pem)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
# test utils
|
||||
|
||||
def setup_ssl_options(config_dir, src, dest):
|
||||
"""Move the ssl_options into position and return the path."""
|
||||
option_path = os.path.join(config_dir, dest)
|
||||
shutil.copyfile(src, option_path)
|
||||
return option_path
|
||||
|
||||
|
||||
def dir_setup(test_dir, pkg):
|
||||
"""Setup the directories necessary for the configurator."""
|
||||
temp_dir = tempfile.mkdtemp("temp")
|
||||
config_dir = tempfile.mkdtemp("config")
|
||||
work_dir = tempfile.mkdtemp("work")
|
||||
|
||||
os.chmod(temp_dir, constants.CONFIG_DIRS_MODE)
|
||||
os.chmod(config_dir, constants.CONFIG_DIRS_MODE)
|
||||
os.chmod(work_dir, constants.CONFIG_DIRS_MODE)
|
||||
|
||||
test_configs = pkg_resources.resource_filename(
|
||||
pkg, os.path.join("testdata", test_dir))
|
||||
|
||||
shutil.copytree(
|
||||
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
|
||||
|
||||
return temp_dir, config_dir, work_dir
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
"""Tests for letsencrypt.plugins.common."""
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
|
||||
class NamespaceFunctionsTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.common.*_namespace functions."""
|
||||
|
|
@ -57,5 +65,103 @@ class PluginTest(unittest.TestCase):
|
|||
"--mock-foo-bar", dest="different_to_foo_bar", x=1, y=None)
|
||||
|
||||
|
||||
class AddrTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.plugins.common.Addr."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.common import Addr
|
||||
self.addr1 = Addr.fromstring("192.168.1.1")
|
||||
self.addr2 = Addr.fromstring("192.168.1.1:*")
|
||||
self.addr3 = Addr.fromstring("192.168.1.1:80")
|
||||
|
||||
def test_fromstring(self):
|
||||
self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr1.get_port(), "")
|
||||
self.assertEqual(self.addr2.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr2.get_port(), "*")
|
||||
self.assertEqual(self.addr3.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr3.get_port(), "80")
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.addr1), "192.168.1.1")
|
||||
self.assertEqual(str(self.addr2), "192.168.1.1:*")
|
||||
self.assertEqual(str(self.addr3), "192.168.1.1:80")
|
||||
|
||||
def test_get_addr_obj(self):
|
||||
self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443")
|
||||
self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1")
|
||||
self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*")
|
||||
|
||||
def test_eq(self):
|
||||
self.assertEqual(self.addr1, self.addr2.get_addr_obj(""))
|
||||
self.assertNotEqual(self.addr1, self.addr2)
|
||||
self.assertFalse(self.addr1 == 3333)
|
||||
|
||||
def test_set_inclusion(self):
|
||||
from letsencrypt.plugins.common import Addr
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
addr1b = Addr.fromstring("192.168.1.1")
|
||||
addr2b = Addr.fromstring("192.168.1.1:*")
|
||||
set_b = set([addr1b, addr2b])
|
||||
|
||||
self.assertEqual(set_a, set_b)
|
||||
|
||||
|
||||
class DvsniTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.common.DvsniTest."""
|
||||
|
||||
rsa256_file = pkg_resources.resource_filename(
|
||||
"acme.jose", "testdata/rsa256_key.pem")
|
||||
rsa256_pem = pkg_resources.resource_string(
|
||||
"acme.jose", "testdata/rsa256_key.pem")
|
||||
|
||||
auth_key = le_util.Key(rsa256_file, rsa256_pem)
|
||||
achalls = [
|
||||
achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9"
|
||||
"\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4",
|
||||
nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18",
|
||||
), "pending"),
|
||||
domain="encryption-example.demo", key=auth_key),
|
||||
achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
r="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
|
||||
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
|
||||
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7\xa1\xb2\xc5"
|
||||
"\x96\xba",
|
||||
), "pending"),
|
||||
domain="letsencrypt.demo", key=auth_key),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.common import Dvsni
|
||||
self.sni = Dvsni(configurator=mock.MagicMock())
|
||||
|
||||
def test_setup_challenge_cert(self):
|
||||
# This is a helper function that can be used for handling
|
||||
# open context managers more elegantly. It avoids dealing with
|
||||
# __enter__ and __exit__ calls.
|
||||
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
|
||||
m_open = mock.mock_open()
|
||||
|
||||
response = challenges.DVSNIResponse(s="randomS1")
|
||||
achall = mock.MagicMock(nonce=self.achalls[0].nonce,
|
||||
nonce_domain=self.achalls[0].nonce_domain)
|
||||
achall.gen_cert_and_response.return_value = ("pem", response)
|
||||
|
||||
with mock.patch("letsencrypt.plugins.common.open", m_open, create=True):
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(response, self.sni._setup_challenge_cert(
|
||||
achall, "randomS1"))
|
||||
|
||||
self.assertTrue(m_open.called)
|
||||
self.assertEqual(
|
||||
m_open.call_args[0], (self.sni.get_cert_file(achall), "w"))
|
||||
self.assertEqual(m_open().write.call_args[0][0], "pem")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
|
|||
|
|
@ -89,10 +89,10 @@ class PluginEntryPoint(object):
|
|||
if self._prepared is None:
|
||||
try:
|
||||
self._initialized.prepare()
|
||||
except errors.LetsEncryptMisconfigurationError as error:
|
||||
except errors.MisconfigurationError as error:
|
||||
logging.debug("Misconfigured %r: %s", self, error)
|
||||
self._prepared = error
|
||||
except errors.LetsEncryptNoInstallationError as error:
|
||||
except errors.NoInstallationError as error:
|
||||
logging.debug("No installation (%r): %s", self, error)
|
||||
self._prepared = error
|
||||
else:
|
||||
|
|
@ -102,8 +102,7 @@ class PluginEntryPoint(object):
|
|||
@property
|
||||
def misconfigured(self):
|
||||
"""Is plugin misconfigured?"""
|
||||
return isinstance(
|
||||
self._prepared, errors.LetsEncryptMisconfigurationError)
|
||||
return isinstance(self._prepared, errors.MisconfigurationError)
|
||||
|
||||
@property
|
||||
def available(self):
|
||||
|
|
|
|||
|
|
@ -124,22 +124,22 @@ class PluginEntryPointTest(unittest.TestCase):
|
|||
|
||||
def test_prepare_misconfigured(self):
|
||||
plugin = mock.MagicMock()
|
||||
plugin.prepare.side_effect = errors.LetsEncryptMisconfigurationError
|
||||
plugin.prepare.side_effect = errors.MisconfigurationError
|
||||
# pylint: disable=protected-access
|
||||
self.plugin_ep._initialized = plugin
|
||||
self.assertTrue(isinstance(self.plugin_ep.prepare(),
|
||||
errors.LetsEncryptMisconfigurationError))
|
||||
errors.MisconfigurationError))
|
||||
self.assertTrue(self.plugin_ep.prepared)
|
||||
self.assertTrue(self.plugin_ep.misconfigured)
|
||||
self.assertTrue(self.plugin_ep.available)
|
||||
|
||||
def test_prepare_no_installation(self):
|
||||
plugin = mock.MagicMock()
|
||||
plugin.prepare.side_effect = errors.LetsEncryptNoInstallationError
|
||||
plugin.prepare.side_effect = errors.NoInstallationError
|
||||
# pylint: disable=protected-access
|
||||
self.plugin_ep._initialized = plugin
|
||||
self.assertTrue(isinstance(self.plugin_ep.prepare(),
|
||||
errors.LetsEncryptNoInstallationError))
|
||||
errors.NoInstallationError))
|
||||
self.assertTrue(self.plugin_ep.prepared)
|
||||
self.assertFalse(self.plugin_ep.misconfigured)
|
||||
self.assertFalse(self.plugin_ep.available)
|
||||
|
|
|
|||
138
letsencrypt/plugins/manual.py
Normal file
138
letsencrypt/plugins/manual.py
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
"""Manual plugin."""
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import requests
|
||||
import zope.component
|
||||
import zope.interface
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt import interfaces
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
|
||||
class ManualAuthenticator(common.Plugin):
|
||||
"""Manual Authenticator.
|
||||
|
||||
.. todo:: Support for `~.challenges.DVSNI`.
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Manual Authenticator"
|
||||
|
||||
MESSAGE_TEMPLATE = """\
|
||||
Make sure your web server displays the following content at
|
||||
{uri} before continuing:
|
||||
|
||||
{achall.token}
|
||||
|
||||
If you don't have HTTP server configured, you can run the following
|
||||
command on the target server (as root):
|
||||
|
||||
{command}
|
||||
"""
|
||||
|
||||
HTTP_TEMPLATE = """\
|
||||
mkdir -p {response.URI_ROOT_PATH}
|
||||
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
|
||||
# run only once per server:
|
||||
python -m SimpleHTTPServer 80"""
|
||||
"""Non-TLS command template."""
|
||||
|
||||
# https://www.piware.de/2011/01/creating-an-https-server-in-python/
|
||||
HTTPS_TEMPLATE = """\
|
||||
mkdir -p {response.URI_ROOT_PATH} # run only once per server
|
||||
echo -n {achall.token} > {response.URI_ROOT_PATH}/{response.path}
|
||||
# run only once per server:
|
||||
openssl req -new -newkey rsa:4096 -subj "/" -days 1 -nodes -x509 -keyout key.pem -out cert.pem
|
||||
python -c "import BaseHTTPServer, SimpleHTTPServer, ssl; \\
|
||||
s = BaseHTTPServer.HTTPServer(('', 443), SimpleHTTPServer.SimpleHTTPRequestHandler); \\
|
||||
s.socket = ssl.wrap_socket(s.socket, keyfile='key.pem', certfile='cert.pem'); \\
|
||||
s.serve_forever()" """
|
||||
"""TLS command template.
|
||||
|
||||
According to the ACME specification, "the ACME server MUST ignore
|
||||
the certificate provided by the HTTPS server", so the first command
|
||||
generates temporary self-signed certificate. For the same reason
|
||||
``requests.get`` in `_verify` sets ``verify=False``. Python HTTPS
|
||||
server command serves the ``token`` on all URIs.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ManualAuthenticator, self).__init__(*args, **kwargs)
|
||||
self.template = (self.HTTP_TEMPLATE if self.config.no_simple_http_tls
|
||||
else self.HTTPS_TEMPLATE)
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring,no-self-use
|
||||
pass # pragma: no cover
|
||||
|
||||
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. Alternatively shows instructions on how
|
||||
to use Python's built-in HTTP server and, in case of HTTPS, openssl
|
||||
binary for temporary key/certificate generation.""".replace("\n", "")
|
||||
|
||||
def get_chall_pref(self, domain):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return [challenges.SimpleHTTP]
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
responses = []
|
||||
# TODO: group achalls by the same socket.gethostbyname(_ex)
|
||||
# and prompt only once per server (one "echo -n" per domain)
|
||||
for achall in achalls:
|
||||
responses.append(self._perform_single(achall))
|
||||
return responses
|
||||
|
||||
def _perform_single(self, achall):
|
||||
# same path for each challenge response would be easier for
|
||||
# users, but will not work if multiple domains point at the
|
||||
# same server: default command doesn't support virtual hosts
|
||||
response = challenges.SimpleHTTPResponse(
|
||||
path=jose.b64encode(os.urandom(18)),
|
||||
tls=(not self.config.no_simple_http_tls))
|
||||
assert response.good_path # is encoded os.urandom(18) good?
|
||||
|
||||
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
|
||||
achall=achall, response=response,
|
||||
uri=response.uri(achall.domain),
|
||||
command=self.template.format(achall=achall, response=response)))
|
||||
|
||||
if self._verify(achall, response):
|
||||
return response
|
||||
else:
|
||||
return None
|
||||
|
||||
def _notify_and_wait(self, message): # pylint: disable=no-self-use
|
||||
# TODO: IDisplay wraps messages, breaking the command
|
||||
#answer = zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
# message=message, height=25, pause=True)
|
||||
sys.stdout.write(message)
|
||||
raw_input("Press ENTER to continue")
|
||||
|
||||
def _verify(self, achall, chall_response): # pylint: disable=no-self-use
|
||||
uri = chall_response.uri(achall.domain)
|
||||
logging.debug("Verifying %s...", uri)
|
||||
try:
|
||||
response = requests.get(uri, verify=False)
|
||||
except requests.exceptions.ConnectionError as error:
|
||||
logging.exception(error)
|
||||
return False
|
||||
|
||||
ret = response.text == achall.token
|
||||
if not ret:
|
||||
logging.error("Unable to verify %s! Expected: %r, returned: %r.",
|
||||
uri, achall.token, response.text)
|
||||
|
||||
return ret
|
||||
|
||||
def cleanup(self, achalls): # pylint: disable=missing-docstring,no-self-use
|
||||
pass # pragma: no cover
|
||||
59
letsencrypt/plugins/manual_test.py
Normal file
59
letsencrypt/plugins/manual_test.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""Tests for letsencrypt.plugins.manual."""
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
|
||||
class ManualAuthenticatorTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.manual.ManualAuthenticator."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.manual import ManualAuthenticator
|
||||
self.config = mock.MagicMock(no_simple_http_tls=True)
|
||||
self.auth = ManualAuthenticator(config=self.config, name="manual")
|
||||
self.achalls = [achallenges.SimpleHTTP(
|
||||
challb=acme_util.SIMPLE_HTTP, domain="foo.com", key=None)]
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue(isinstance(self.auth.more_info(), str))
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertTrue(all(issubclass(pref, challenges.Challenge)
|
||||
for pref in self.auth.get_chall_pref("foo.com")))
|
||||
|
||||
def test_perform_empty(self):
|
||||
self.assertEqual([], self.auth.perform([]))
|
||||
|
||||
@mock.patch("letsencrypt.plugins.manual.sys.stdout")
|
||||
@mock.patch("letsencrypt.plugins.manual.os.urandom")
|
||||
@mock.patch("letsencrypt.plugins.manual.requests.get")
|
||||
@mock.patch("__builtin__.raw_input")
|
||||
def test_perform(self, mock_raw_input, mock_get, mock_urandom, mock_stdout):
|
||||
mock_urandom.return_value = "foo"
|
||||
mock_get().text = self.achalls[0].token
|
||||
|
||||
self.assertEqual(
|
||||
[challenges.SimpleHTTPResponse(tls=False, path='Zm9v')],
|
||||
self.auth.perform(self.achalls))
|
||||
mock_raw_input.assert_called_once()
|
||||
mock_get.assert_called_with(
|
||||
"http://foo.com/.well-known/acme-challenge/Zm9v", verify=False)
|
||||
|
||||
message = mock_stdout.write.mock_calls[0][1][0]
|
||||
self.assertTrue(self.achalls[0].token in message)
|
||||
self.assertTrue('Zm9v' in message)
|
||||
|
||||
mock_get().text = self.achalls[0].token + '!'
|
||||
self.assertEqual([None], self.auth.perform(self.achalls))
|
||||
|
||||
mock_get.side_effect = requests.exceptions.ConnectionError
|
||||
self.assertEqual([None], self.auth.perform(self.achalls))
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -7,15 +7,19 @@ within lineages of successor certificates, according to configuration.
|
|||
.. todo:: Call new installer API to restart servers after deployment
|
||||
|
||||
"""
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
|
||||
import configobj
|
||||
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import cli
|
||||
from letsencrypt import client
|
||||
from letsencrypt import crypto_util
|
||||
from letsencrypt import notify
|
||||
from letsencrypt import storage
|
||||
|
||||
from letsencrypt.plugins import disco as plugins_disco
|
||||
|
||||
|
||||
|
|
@ -92,7 +96,23 @@ def renew(cert, old_version):
|
|||
# (where fewer than all names were renewed)
|
||||
|
||||
|
||||
def main(config=None):
|
||||
def _paths_parser(parser):
|
||||
add = parser.add_argument_group("paths").add_argument
|
||||
add("--config-dir", default=cli.flag_default("config_dir"),
|
||||
help=cli.config_help("config_dir"))
|
||||
add("--work-dir", default=cli.flag_default("work_dir"),
|
||||
help=cli.config_help("work_dir"))
|
||||
return parser
|
||||
|
||||
|
||||
def _create_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
#parser.add_argument("--cron", action="store_true", help="Run as cronjob.")
|
||||
# pylint: disable=protected-access
|
||||
return _paths_parser(parser)
|
||||
|
||||
|
||||
def main(config=None, args=sys.argv[1:]):
|
||||
"""Main function for autorenewer script."""
|
||||
# TODO: Distinguish automated invocation from manual invocation,
|
||||
# perhaps by looking at sys.argv[0] and inhibiting automated
|
||||
|
|
@ -100,6 +120,9 @@ def main(config=None):
|
|||
# turned it off. (The boolean parameter should probably be
|
||||
# called renewer_enabled.)
|
||||
|
||||
cli_config = configuration.RenewerConfiguration(
|
||||
_create_parser().parse_args(args))
|
||||
|
||||
config = storage.config_with_defaults(config)
|
||||
# Now attempt to read the renewer config file and augment or replace
|
||||
# the renewer defaults with any options contained in that file. If
|
||||
|
|
@ -108,14 +131,15 @@ def main(config=None):
|
|||
# elaborate renewer command line, we will presumably also be able to
|
||||
# specify a config file on the command line, which, if provided, should
|
||||
# take precedence over this one.
|
||||
config.merge(configobj.ConfigObj(config.get("renewer_config_file", "")))
|
||||
config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
|
||||
|
||||
for i in os.listdir(config["renewal_configs_dir"]):
|
||||
for i in os.listdir(cli_config.renewal_configs_dir):
|
||||
print "Processing", i
|
||||
if not i.endswith(".conf"):
|
||||
continue
|
||||
rc_config = configobj.ConfigObj(
|
||||
os.path.join(config["renewal_configs_dir"], i))
|
||||
rc_config = configobj.ConfigObj(cli_config.renewer_config_file)
|
||||
rc_config.merge(configobj.ConfigObj(
|
||||
os.path.join(cli_config.renewal_configs_dir, i)))
|
||||
try:
|
||||
# TODO: Before trying to initialize the RenewableCert object,
|
||||
# we could check here whether the combination of the config
|
||||
|
|
@ -125,7 +149,7 @@ def main(config=None):
|
|||
# RenewableCert object for this cert at all, which could
|
||||
# dramatically improve performance for large deployments
|
||||
# where autorenewal is widely turned off.
|
||||
cert = storage.RenewableCert(rc_config)
|
||||
cert = storage.RenewableCert(rc_config, cli_config=cli_config)
|
||||
except ValueError:
|
||||
# This indicates an invalid renewal configuration file, such
|
||||
# as one missing a required parameter (in the future, perhaps
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ class Reporter(object):
|
|||
|
||||
If there is an unhandled exception, only messages for which
|
||||
``on_crash`` is ``True`` are printed.
|
||||
"""
|
||||
|
||||
"""
|
||||
bold_on = False
|
||||
if not self.messages.empty():
|
||||
no_exception = sys.exc_info()[0] is None
|
||||
|
|
@ -74,14 +75,21 @@ class Reporter(object):
|
|||
if bold_on:
|
||||
print self._BOLD
|
||||
print 'IMPORTANT NOTES:'
|
||||
wrapper = textwrap.TextWrapper(initial_indent=' - ',
|
||||
subsequent_indent=(' ' * 3))
|
||||
first_wrapper = textwrap.TextWrapper(
|
||||
initial_indent=' - ', subsequent_indent=(' ' * 3))
|
||||
next_wrapper = textwrap.TextWrapper(
|
||||
initial_indent=first_wrapper.subsequent_indent,
|
||||
subsequent_indent=first_wrapper.subsequent_indent)
|
||||
while not self.messages.empty():
|
||||
msg = self.messages.get()
|
||||
if no_exception or msg.on_crash:
|
||||
if bold_on and msg.priority > self.HIGH_PRIORITY:
|
||||
sys.stdout.write(self._RESET)
|
||||
bold_on = False
|
||||
print wrapper.fill(msg.text)
|
||||
lines = msg.text.splitlines()
|
||||
print first_wrapper.fill(lines[0])
|
||||
if len(lines) > 1:
|
||||
print "\n".join(
|
||||
next_wrapper.fill(line) for line in lines[1:])
|
||||
if bold_on:
|
||||
sys.stdout.write(self._RESET)
|
||||
|
|
|
|||
|
|
@ -30,19 +30,17 @@ class Reverter(object):
|
|||
This function should reinstall the users original configuration files
|
||||
for all saves with temporary=True
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError: when
|
||||
unable to revert config
|
||||
:raises .ReverterError: when unable to revert config
|
||||
|
||||
"""
|
||||
if os.path.isdir(self.config.temp_checkpoint_dir):
|
||||
try:
|
||||
self._recover_checkpoint(self.config.temp_checkpoint_dir)
|
||||
except errors.LetsEncryptReverterError:
|
||||
except errors.ReverterError:
|
||||
# We have a partial or incomplete recovery
|
||||
logging.fatal("Incomplete or failed recovery for %s",
|
||||
self.config.temp_checkpoint_dir)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
"Unable to revert temporary config")
|
||||
raise errors.ReverterError("Unable to revert temporary config")
|
||||
|
||||
def rollback_checkpoints(self, rollback=1):
|
||||
"""Revert 'rollback' number of configuration checkpoints.
|
||||
|
|
@ -50,20 +48,20 @@ class Reverter(object):
|
|||
:param int rollback: Number of checkpoints to reverse. A str num will be
|
||||
cast to an integer. So "2" is also acceptable.
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError: If
|
||||
there is a problem with the input or if the function is unable to
|
||||
correctly revert the configuration checkpoints.
|
||||
:raises .ReverterError:
|
||||
if there is a problem with the input or if the function is
|
||||
unable to correctly revert the configuration checkpoints
|
||||
|
||||
"""
|
||||
try:
|
||||
rollback = int(rollback)
|
||||
except ValueError:
|
||||
logging.error("Rollback argument must be a positive integer")
|
||||
raise errors.LetsEncryptReverterError("Invalid Input")
|
||||
raise errors.ReverterError("Invalid Input")
|
||||
# Sanity check input
|
||||
if rollback < 0:
|
||||
logging.error("Rollback argument must be a positive integer")
|
||||
raise errors.LetsEncryptReverterError("Invalid Input")
|
||||
raise errors.ReverterError("Invalid Input")
|
||||
|
||||
backups = os.listdir(self.config.backup_dir)
|
||||
backups.sort()
|
||||
|
|
@ -76,9 +74,9 @@ class Reverter(object):
|
|||
cp_dir = os.path.join(self.config.backup_dir, backups.pop())
|
||||
try:
|
||||
self._recover_checkpoint(cp_dir)
|
||||
except errors.LetsEncryptReverterError:
|
||||
except errors.ReverterError:
|
||||
logging.fatal("Failed to load checkpoint during rollback")
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to load checkpoint during rollback")
|
||||
rollback -= 1
|
||||
|
||||
|
|
@ -104,7 +102,7 @@ class Reverter(object):
|
|||
for bkup in backups:
|
||||
float(bkup)
|
||||
except ValueError:
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Invalid directories in {0}".format(self.config.backup_dir))
|
||||
|
||||
output = []
|
||||
|
|
@ -161,9 +159,8 @@ class Reverter(object):
|
|||
:param set save_files: set of files to save
|
||||
:param str save_notes: notes about changes made during the save
|
||||
|
||||
:raises IOError: If unable to open cp_dir + FILEPATHS file
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError: If
|
||||
unable to add checkpoint
|
||||
:raises IOError: if unable to open cp_dir + FILEPATHS file
|
||||
:raises .ReverterError: if unable to add checkpoint
|
||||
|
||||
"""
|
||||
le_util.make_or_verify_dir(
|
||||
|
|
@ -191,7 +188,7 @@ class Reverter(object):
|
|||
logging.error(
|
||||
"Unable to add file %s to checkpoint %s",
|
||||
filename, cp_dir)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to add file {0} to checkpoint "
|
||||
"{1}".format(filename, cp_dir))
|
||||
idx += 1
|
||||
|
|
@ -224,7 +221,7 @@ class Reverter(object):
|
|||
|
||||
:param str cp_dir: checkpoint directory file path
|
||||
|
||||
:raises errors.LetsEncryptReverterError: If unable to recover checkpoint
|
||||
:raises errors.ReverterError: If unable to recover checkpoint
|
||||
|
||||
"""
|
||||
if os.path.isfile(os.path.join(cp_dir, "FILEPATHS")):
|
||||
|
|
@ -238,7 +235,7 @@ class Reverter(object):
|
|||
except (IOError, OSError):
|
||||
# This file is required in all checkpoints.
|
||||
logging.error("Unable to recover files from %s", cp_dir)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to recover files from %s" % cp_dir)
|
||||
|
||||
# Remove any newly added files if they exist
|
||||
|
|
@ -248,7 +245,7 @@ class Reverter(object):
|
|||
shutil.rmtree(cp_dir)
|
||||
except OSError:
|
||||
logging.error("Unable to remove directory: %s", cp_dir)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to remove directory: %s" % cp_dir)
|
||||
|
||||
def _check_tempfile_saves(self, save_files):
|
||||
|
|
@ -256,7 +253,7 @@ class Reverter(object):
|
|||
|
||||
:param set save_files: Set of files about to be saved.
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError:
|
||||
:raises letsencrypt.errors.ReverterError:
|
||||
when save is attempting to overwrite a temporary file.
|
||||
|
||||
"""
|
||||
|
|
@ -277,7 +274,7 @@ class Reverter(object):
|
|||
# Verify no save_file is in protected_files
|
||||
for filename in protected_files:
|
||||
if filename in save_files:
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Attempting to overwrite challenge "
|
||||
"file - %s" % filename)
|
||||
|
||||
|
|
@ -292,7 +289,7 @@ class Reverter(object):
|
|||
a temp or permanent save.
|
||||
:param \*files: file paths (str) to be registered
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError: If
|
||||
:raises letsencrypt.errors.ReverterError: If
|
||||
call does not contain necessary parameters or if the file creation
|
||||
is unable to be registered.
|
||||
|
||||
|
|
@ -300,7 +297,7 @@ class Reverter(object):
|
|||
# Make sure some files are provided... as this is an error
|
||||
# Made this mistake in my initial implementation of apache.dvsni.py
|
||||
if not files:
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Forgot to provide files to registration call")
|
||||
|
||||
if temporary:
|
||||
|
|
@ -322,7 +319,7 @@ class Reverter(object):
|
|||
new_fd.write("{0}{1}".format(path, os.linesep))
|
||||
except (IOError, OSError):
|
||||
logging.error("Unable to register file creation(s) - %s", files)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to register file creation(s) - {0}".format(files))
|
||||
finally:
|
||||
if new_fd is not None:
|
||||
|
|
@ -345,12 +342,12 @@ class Reverter(object):
|
|||
if os.path.isdir(self.config.in_progress_dir):
|
||||
try:
|
||||
self._recover_checkpoint(self.config.in_progress_dir)
|
||||
except errors.LetsEncryptReverterError:
|
||||
except errors.ReverterError:
|
||||
# We have a partial or incomplete recovery
|
||||
logging.fatal("Incomplete or failed recovery for IN_PROGRESS "
|
||||
"checkpoint - %s",
|
||||
self.config.in_progress_dir)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Incomplete or failed recovery for IN_PROGRESS checkpoint "
|
||||
"- %s" % self.config.in_progress_dir)
|
||||
|
||||
|
|
@ -362,7 +359,7 @@ class Reverter(object):
|
|||
:returns: Success
|
||||
:rtype: bool
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError: If
|
||||
:raises letsencrypt.errors.ReverterError: If
|
||||
all files within file_list cannot be removed
|
||||
|
||||
"""
|
||||
|
|
@ -386,7 +383,7 @@ class Reverter(object):
|
|||
except (IOError, OSError):
|
||||
logging.fatal(
|
||||
"Unable to remove filepaths contained within %s", file_list)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to remove filepaths contained within "
|
||||
"{0}".format(file_list))
|
||||
|
||||
|
|
@ -400,7 +397,7 @@ class Reverter(object):
|
|||
|
||||
:param str title: Title describing checkpoint
|
||||
|
||||
:raises letsencrypt.errors.LetsEncryptReverterError: when the
|
||||
:raises letsencrypt.errors.ReverterError: when the
|
||||
checkpoint is not able to be finalized.
|
||||
|
||||
"""
|
||||
|
|
@ -426,7 +423,7 @@ class Reverter(object):
|
|||
shutil.move(changes_since_tmp_path, changes_since_path)
|
||||
except (IOError, OSError):
|
||||
logging.error("Unable to finalize checkpoint - adding title")
|
||||
raise errors.LetsEncryptReverterError("Unable to add title")
|
||||
raise errors.ReverterError("Unable to add title")
|
||||
|
||||
self._timestamp_progress_dir()
|
||||
|
||||
|
|
@ -451,5 +448,5 @@ class Reverter(object):
|
|||
logging.error(
|
||||
"Unable to finalize checkpoint, %s -> %s",
|
||||
self.config.in_progress_dir, final_dir)
|
||||
raise errors.LetsEncryptReverterError(
|
||||
raise errors.ReverterError(
|
||||
"Unable to finalize checkpoint renaming")
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import tempfile
|
|||
import Crypto.PublicKey.RSA
|
||||
import M2Crypto
|
||||
|
||||
from acme import messages
|
||||
from acme.jose import util as jose_util
|
||||
|
||||
from letsencrypt import errors
|
||||
|
|
@ -45,7 +44,9 @@ class Revoker(object):
|
|||
|
||||
"""
|
||||
def __init__(self, installer, config, no_confirm=False):
|
||||
self.network = network.Network(config.server)
|
||||
# XXX
|
||||
self.network = network.Network(new_reg_uri=None, key=None, alg=None)
|
||||
|
||||
self.installer = installer
|
||||
self.config = config
|
||||
self.no_confirm = no_confirm
|
||||
|
|
@ -70,7 +71,7 @@ class Revoker(object):
|
|||
authkey.pem).exportKey("PEM")
|
||||
# https://www.dlitz.net/software/pycrypto/api/current/Crypto.PublicKey.RSA-module.html
|
||||
except (IndexError, ValueError, TypeError):
|
||||
raise errors.LetsEncryptRevokerError(
|
||||
raise errors.RevokerError(
|
||||
"Invalid key file specified to revoke_from_key")
|
||||
|
||||
with open(self.list_path, "rb") as csvfile:
|
||||
|
|
@ -88,8 +89,7 @@ class Revoker(object):
|
|||
# This should never happen given the assumptions of the
|
||||
# module. If it does, it is probably best to delete the
|
||||
# the offending key/cert. For now... just raise an exception
|
||||
raise errors.LetsEncryptRevokerError(
|
||||
"%s - backup file is corrupted.")
|
||||
raise errors.RevokerError("%s - backup file is corrupted.")
|
||||
|
||||
if clean_pem == test_pem:
|
||||
certs.append(
|
||||
|
|
@ -217,7 +217,7 @@ class Revoker(object):
|
|||
if self.no_confirm or revocation.confirm_revocation(cert):
|
||||
try:
|
||||
self._acme_revoke(cert)
|
||||
except errors.LetsEncryptClientError:
|
||||
except errors.Error:
|
||||
# TODO: Improve error handling when networking is set...
|
||||
logging.error(
|
||||
"Unable to revoke cert:%s%s", os.linesep, str(cert))
|
||||
|
|
@ -238,6 +238,8 @@ class Revoker(object):
|
|||
:returns: TODO
|
||||
|
||||
"""
|
||||
# XXX | pylint: disable=unused-variable
|
||||
|
||||
# These will both have to change in the future away from M2Crypto
|
||||
# pylint: disable=protected-access
|
||||
certificate = jose_util.ComparableX509(cert._cert)
|
||||
|
|
@ -247,13 +249,10 @@ class Revoker(object):
|
|||
|
||||
# If the key file doesn't exist... or is corrupted
|
||||
except (IndexError, ValueError, TypeError):
|
||||
raise errors.LetsEncryptRevokerError(
|
||||
raise errors.RevokerError(
|
||||
"Corrupted backup key file: %s" % cert.backup_key_path)
|
||||
|
||||
# TODO: Catch error associated with already revoked and proceed.
|
||||
return self.network.send_and_receive_expected(
|
||||
messages.RevocationRequest.create(certificate=certificate, key=key),
|
||||
messages.Revocation)
|
||||
return self.network.revoke(cert=None) # XXX
|
||||
|
||||
def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use
|
||||
"""Remove certificate and key.
|
||||
|
|
@ -293,7 +292,7 @@ class Revoker(object):
|
|||
|
||||
# This should never happen...
|
||||
if idx != len(cert_list):
|
||||
raise errors.LetsEncryptRevokerError(
|
||||
raise errors.RevokerError(
|
||||
"Did not find all cert_list items to remove from LIST")
|
||||
|
||||
shutil.copy2(list_path2, self.list_path)
|
||||
|
|
@ -398,7 +397,7 @@ class Cert(object):
|
|||
try:
|
||||
self._cert = M2Crypto.X509.load_cert(cert_path)
|
||||
except (IOError, M2Crypto.X509.X509Error):
|
||||
raise errors.LetsEncryptRevokerError(
|
||||
raise errors.RevokerError(
|
||||
"Error loading certificate: %s" % cert_path)
|
||||
|
||||
self.idx = -1
|
||||
|
|
|
|||
|
|
@ -78,14 +78,17 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
renewal configuration file and/or systemwide defaults.
|
||||
|
||||
"""
|
||||
def __init__(self, configfile, config_opts=None):
|
||||
def __init__(self, configfile, config_opts=None, cli_config=None):
|
||||
"""Instantiate a RenewableCert object from an existing lineage.
|
||||
|
||||
:param configobj.ConfigObj configfile: an already-parsed
|
||||
ConfigObj object made from reading the renewal config file
|
||||
that defines this lineage. :param configobj.ConfigObj
|
||||
config_opts: systemwide defaults for renewal properties not
|
||||
otherwise specified in the individual renewal config file.
|
||||
ConfigObj object made from reading the renewal config file
|
||||
that defines this lineage.
|
||||
|
||||
:param configobj.ConfigObj config_opts: systemwide defaults for
|
||||
renewal properties not otherwise specified in the individual
|
||||
renewal config file.
|
||||
:param .RenewerConfiguration cli_config:
|
||||
|
||||
:raises ValueError: if the configuration file's name didn't end
|
||||
in ".conf", or the file is missing or broken.
|
||||
|
|
@ -93,6 +96,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
ConfigObj object.
|
||||
|
||||
"""
|
||||
self.cli_config = cli_config
|
||||
if isinstance(configfile, configobj.ConfigObj):
|
||||
if not os.path.basename(configfile.filename).endswith(".conf"):
|
||||
raise ValueError("renewal config file name must end in .conf")
|
||||
|
|
@ -149,7 +153,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
# Each element's link must point within the cert lineage's
|
||||
# directory within the official archive directory
|
||||
desired_directory = os.path.join(
|
||||
self.configuration["archive_dir"], self.lineagename)
|
||||
self.cli_config.archive_dir, self.lineagename)
|
||||
if not os.path.samefile(os.path.dirname(target),
|
||||
desired_directory):
|
||||
return False
|
||||
|
|
@ -499,7 +503,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
|
||||
@classmethod
|
||||
def new_lineage(cls, lineagename, cert, privkey, chain,
|
||||
renewalparams=None, config=None):
|
||||
renewalparams=None, config=None, cli_config=None):
|
||||
# pylint: disable=too-many-locals,too-many-arguments
|
||||
"""Create a new certificate lineage.
|
||||
|
||||
|
|
@ -536,17 +540,15 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
# the renewer defaults with any options contained in that file. If
|
||||
# renewer_config_file is undefined or if the file is nonexistent or
|
||||
# empty, this .merge() will have no effect.
|
||||
config.merge(configobj.ConfigObj(config.get("renewer_config_file", "")))
|
||||
config.merge(configobj.ConfigObj(cli_config.renewer_config_file))
|
||||
|
||||
# Examine the configuration and find the new lineage's name
|
||||
configs_dir = config["renewal_configs_dir"]
|
||||
archive_dir = config["archive_dir"]
|
||||
live_dir = config["live_dir"]
|
||||
for i in (configs_dir, archive_dir, live_dir):
|
||||
for i in (cli_config.renewal_configs_dir, cli_config.archive_dir,
|
||||
cli_config.live_dir):
|
||||
if not os.path.exists(i):
|
||||
os.makedirs(i, 0700)
|
||||
config_file, config_filename = le_util.unique_lineage_name(configs_dir,
|
||||
lineagename)
|
||||
config_file, config_filename = le_util.unique_lineage_name(
|
||||
cli_config.renewal_configs_dir, lineagename)
|
||||
if not config_filename.endswith(".conf"):
|
||||
raise ValueError("renewal config file name must end in .conf")
|
||||
|
||||
|
|
@ -554,8 +556,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
# lineagename will now potentially be modified based on which
|
||||
# renewal configuration file could actually be created
|
||||
lineagename = os.path.basename(config_filename)[:-len(".conf")]
|
||||
archive = os.path.join(archive_dir, lineagename)
|
||||
live_dir = os.path.join(live_dir, lineagename)
|
||||
archive = os.path.join(cli_config.archive_dir, lineagename)
|
||||
live_dir = os.path.join(cli_config.live_dir, lineagename)
|
||||
if os.path.exists(archive):
|
||||
raise ValueError("archive directory exists for " + lineagename)
|
||||
if os.path.exists(live_dir):
|
||||
|
|
@ -593,7 +595,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
# TODO: add human-readable comments explaining other available
|
||||
# parameters
|
||||
new_config.write()
|
||||
return cls(new_config, config)
|
||||
return cls(new_config, config, cli_config)
|
||||
|
||||
|
||||
def save_successor(self, prior_version, new_cert, new_privkey, new_chain):
|
||||
|
|
@ -624,7 +626,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
|||
# Figure out what the new version is and hence where to save things
|
||||
|
||||
target_version = self.next_free_version()
|
||||
archive = self.configuration["archive_dir"]
|
||||
archive = self.cli_config.archive_dir
|
||||
prefix = os.path.join(archive, self.lineagename)
|
||||
target = dict(
|
||||
[(kind,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import shutil
|
|||
import tempfile
|
||||
import unittest
|
||||
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt import errors
|
||||
|
|
@ -40,11 +40,11 @@ class AccountTest(unittest.TestCase):
|
|||
|
||||
self.key = le_util.Key(key_file, key_pem)
|
||||
self.email = "client@letsencrypt.org"
|
||||
self.regr = messages2.RegistrationResource(
|
||||
self.regr = messages.RegistrationResource(
|
||||
uri="uri",
|
||||
new_authzr_uri="new_authzr_uri",
|
||||
terms_of_service="terms_of_service",
|
||||
body=messages2.Registration(
|
||||
body=messages.Registration(
|
||||
recovery_token="recovery_token", agreement="agreement")
|
||||
)
|
||||
|
||||
|
|
@ -73,7 +73,7 @@ class AccountTest(unittest.TestCase):
|
|||
def test_prompts_bad_email(self, mock_from_email, mock_util):
|
||||
from letsencrypt.account import Account
|
||||
|
||||
mock_from_email.side_effect = (errors.LetsEncryptClientError, "acc")
|
||||
mock_from_email.side_effect = (errors.Error, "acc")
|
||||
mock_util().input.return_value = (display_util.OK, self.email)
|
||||
|
||||
self.assertEqual(Account.from_prompts(self.config), "acc")
|
||||
|
|
@ -102,8 +102,8 @@ class AccountTest(unittest.TestCase):
|
|||
def test_from_email(self):
|
||||
from letsencrypt.account import Account
|
||||
|
||||
self.assertRaises(errors.LetsEncryptClientError,
|
||||
Account.from_email, self.config, "not_valid...email")
|
||||
self.assertRaises(
|
||||
errors.Error, Account.from_email, self.config, "not_valid...email")
|
||||
|
||||
def test_save_from_existing_account(self):
|
||||
from letsencrypt.account import Account
|
||||
|
|
@ -170,10 +170,8 @@ class AccountTest(unittest.TestCase):
|
|||
def test_failed_existing_account(self):
|
||||
from letsencrypt.account import Account
|
||||
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptClientError,
|
||||
Account.from_existing_account,
|
||||
self.config, "non-existant@email.org")
|
||||
self.assertRaises(errors.Error, Account.from_existing_account,
|
||||
self.config, "non-existant@email.org")
|
||||
|
||||
class SafeEmailTest(unittest.TestCase):
|
||||
"""Test safe_email."""
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import Crypto.PublicKey.RSA
|
|||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
|
||||
KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
|
|
@ -16,7 +16,7 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
|||
"acme.jose", os.path.join("testdata", "rsa512_key.pem"))))
|
||||
|
||||
# Challenges
|
||||
SIMPLE_HTTPS = challenges.SimpleHTTP(
|
||||
SIMPLE_HTTP = challenges.SimpleHTTP(
|
||||
token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
|
||||
DVSNI = challenges.DVSNI(
|
||||
r="O*\xb4-\xad\xec\x95>\xed\xa9\r0\x94\xe8\x97\x9c&6\xbf'\xb3"
|
||||
|
|
@ -47,7 +47,7 @@ POP = challenges.ProofOfPossession(
|
|||
)
|
||||
)
|
||||
|
||||
CHALLENGES = [SIMPLE_HTTPS, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP]
|
||||
CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, RECOVERY_TOKEN, POP]
|
||||
DV_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.DVChallenge)]
|
||||
CONT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
|
|
@ -78,21 +78,21 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
|
|||
"status": status,
|
||||
}
|
||||
|
||||
if status == messages2.STATUS_VALID:
|
||||
if status == messages.STATUS_VALID:
|
||||
kwargs.update({"validated": datetime.datetime.now()})
|
||||
|
||||
return messages2.ChallengeBody(**kwargs) # pylint: disable=star-args
|
||||
return messages.ChallengeBody(**kwargs) # pylint: disable=star-args
|
||||
|
||||
|
||||
# Pending ChallengeBody objects
|
||||
DVSNI_P = chall_to_challb(DVSNI, messages2.STATUS_PENDING)
|
||||
SIMPLE_HTTPS_P = chall_to_challb(SIMPLE_HTTPS, messages2.STATUS_PENDING)
|
||||
DNS_P = chall_to_challb(DNS, messages2.STATUS_PENDING)
|
||||
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages2.STATUS_PENDING)
|
||||
RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages2.STATUS_PENDING)
|
||||
POP_P = chall_to_challb(POP, messages2.STATUS_PENDING)
|
||||
DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING)
|
||||
SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages.STATUS_PENDING)
|
||||
DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING)
|
||||
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING)
|
||||
RECOVERY_TOKEN_P = chall_to_challb(RECOVERY_TOKEN, messages.STATUS_PENDING)
|
||||
POP_P = chall_to_challb(POP, messages.STATUS_PENDING)
|
||||
|
||||
CHALLENGES_P = [SIMPLE_HTTPS_P, DVSNI_P, DNS_P,
|
||||
CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P,
|
||||
RECOVERY_CONTACT_P, RECOVERY_TOKEN_P, POP_P]
|
||||
DV_CHALLENGES_P = [challb for challb in CHALLENGES_P
|
||||
if isinstance(challb.chall, challenges.DVChallenge)]
|
||||
|
|
@ -106,7 +106,7 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
|||
"""Generate an authorization resource.
|
||||
|
||||
:param authz_status: Status object
|
||||
:type authz_status: :class:`acme.messages2.Status`
|
||||
:type authz_status: :class:`acme.messages.Status`
|
||||
:param list challs: Challenge objects
|
||||
:param list statuses: status of each challenge object
|
||||
:param bool combos: Whether or not to add combinations
|
||||
|
|
@ -118,13 +118,13 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
|||
for chall, status in itertools.izip(challs, statuses)
|
||||
)
|
||||
authz_kwargs = {
|
||||
"identifier": messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value=domain),
|
||||
"identifier": messages.Identifier(
|
||||
typ=messages.IDENTIFIER_FQDN, value=domain),
|
||||
"challenges": challbs,
|
||||
}
|
||||
if combos:
|
||||
authz_kwargs.update({"combinations": gen_combos(challbs)})
|
||||
if authz_status == messages2.STATUS_VALID:
|
||||
if authz_status == messages.STATUS_VALID:
|
||||
authz_kwargs.update({
|
||||
"status": authz_status,
|
||||
"expires": datetime.datetime.now() + datetime.timedelta(days=31),
|
||||
|
|
@ -135,8 +135,8 @@ def gen_authzr(authz_status, domain, challs, statuses, combos=True):
|
|||
})
|
||||
|
||||
# pylint: disable=star-args
|
||||
return messages2.AuthorizationResource(
|
||||
return messages.AuthorizationResource(
|
||||
uri="https://trusted.ca/new-authz-resource",
|
||||
new_cert_uri="https://trusted.ca/new-cert",
|
||||
body=messages2.Authorization(**authz_kwargs)
|
||||
body=messages.Authorization(**authz_kwargs)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,11 +6,11 @@ import unittest
|
|||
import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt import network2
|
||||
from letsencrypt import network
|
||||
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
|
|
@ -37,8 +37,8 @@ class ChallengeFactoryTest(unittest.TestCase):
|
|||
|
||||
self.dom = "test"
|
||||
self.handler.authzr[self.dom] = acme_util.gen_authzr(
|
||||
messages2.STATUS_PENDING, self.dom, acme_util.CHALLENGES,
|
||||
[messages2.STATUS_PENDING]*6, False)
|
||||
messages.STATUS_PENDING, self.dom, acme_util.CHALLENGES,
|
||||
[messages.STATUS_PENDING]*6, False)
|
||||
|
||||
def test_all(self):
|
||||
cont_c, dv_c = self.handler._challenge_factory(self.dom, range(0, 6))
|
||||
|
|
@ -57,12 +57,12 @@ class ChallengeFactoryTest(unittest.TestCase):
|
|||
|
||||
def test_unrecognized(self):
|
||||
self.handler.authzr["failure.com"] = acme_util.gen_authzr(
|
||||
messages2.STATUS_PENDING, "failure.com",
|
||||
messages.STATUS_PENDING, "failure.com",
|
||||
[mock.Mock(chall="chall", typ="unrecognized")],
|
||||
[messages2.STATUS_PENDING])
|
||||
[messages.STATUS_PENDING])
|
||||
|
||||
self.assertRaises(errors.LetsEncryptClientError,
|
||||
self.handler._challenge_factory, "failure.com", [0])
|
||||
self.assertRaises(
|
||||
errors.Error, self.handler._challenge_factory, "failure.com", [0])
|
||||
|
||||
|
||||
class GetAuthorizationsTest(unittest.TestCase):
|
||||
|
|
@ -86,7 +86,7 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
self.mock_dv_auth.perform.side_effect = gen_auth_resp
|
||||
|
||||
self.mock_account = mock.Mock(key=le_util.Key("file_path", "PEM"))
|
||||
self.mock_net = mock.MagicMock(spec=network2.Network)
|
||||
self.mock_net = mock.MagicMock(spec=network.Network)
|
||||
|
||||
self.handler = AuthHandler(
|
||||
self.mock_dv_auth, self.mock_cont_auth,
|
||||
|
|
@ -153,17 +153,17 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
gen_dom_authzr, challs=acme_util.CHALLENGES)
|
||||
self.mock_dv_auth.perform.side_effect = errors.AuthorizationError
|
||||
|
||||
self.assertRaises(errors.AuthorizationError,
|
||||
self.handler.get_authorizations, ["0"])
|
||||
self.assertRaises(
|
||||
errors.AuthorizationError, self.handler.get_authorizations, ["0"])
|
||||
|
||||
def _validate_all(self, unused_1, unused_2):
|
||||
for dom in self.handler.authzr.keys():
|
||||
azr = self.handler.authzr[dom]
|
||||
self.handler.authzr[dom] = acme_util.gen_authzr(
|
||||
messages2.STATUS_VALID,
|
||||
messages.STATUS_VALID,
|
||||
dom,
|
||||
[challb.chall for challb in azr.body.challenges],
|
||||
[messages2.STATUS_VALID]*len(azr.body.challenges),
|
||||
[messages.STATUS_VALID]*len(azr.body.challenges),
|
||||
azr.body.combinations)
|
||||
|
||||
|
||||
|
|
@ -182,16 +182,16 @@ class PollChallengesTest(unittest.TestCase):
|
|||
|
||||
self.doms = ["0", "1", "2"]
|
||||
self.handler.authzr[self.doms[0]] = acme_util.gen_authzr(
|
||||
messages2.STATUS_PENDING, self.doms[0],
|
||||
acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False)
|
||||
messages.STATUS_PENDING, self.doms[0],
|
||||
acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False)
|
||||
|
||||
self.handler.authzr[self.doms[1]] = acme_util.gen_authzr(
|
||||
messages2.STATUS_PENDING, self.doms[1],
|
||||
acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False)
|
||||
messages.STATUS_PENDING, self.doms[1],
|
||||
acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False)
|
||||
|
||||
self.handler.authzr[self.doms[2]] = acme_util.gen_authzr(
|
||||
messages2.STATUS_PENDING, self.doms[2],
|
||||
acme_util.DV_CHALLENGES, [messages2.STATUS_PENDING]*3, False)
|
||||
messages.STATUS_PENDING, self.doms[2],
|
||||
acme_util.DV_CHALLENGES, [messages.STATUS_PENDING]*3, False)
|
||||
|
||||
self.chall_update = {}
|
||||
for dom in self.doms:
|
||||
|
|
@ -205,7 +205,7 @@ class PollChallengesTest(unittest.TestCase):
|
|||
self.handler._poll_challenges(self.chall_update, False)
|
||||
|
||||
for authzr in self.handler.authzr.values():
|
||||
self.assertEqual(authzr.body.status, messages2.STATUS_VALID)
|
||||
self.assertEqual(authzr.body.status, messages.STATUS_VALID)
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.time")
|
||||
def test_poll_challenges_failure_best_effort(self, unused_mock_time):
|
||||
|
|
@ -213,14 +213,15 @@ class PollChallengesTest(unittest.TestCase):
|
|||
self.handler._poll_challenges(self.chall_update, True)
|
||||
|
||||
for authzr in self.handler.authzr.values():
|
||||
self.assertEqual(authzr.body.status, messages2.STATUS_PENDING)
|
||||
self.assertEqual(authzr.body.status, messages.STATUS_PENDING)
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.time")
|
||||
def test_poll_challenges_failure(self, unused_mock_time):
|
||||
@mock.patch("letsencrypt.auth_handler.zope.component.getUtility")
|
||||
def test_poll_challenges_failure(self, unused_mock_time, unused_mock_zope):
|
||||
self.mock_net.poll.side_effect = self._mock_poll_solve_one_invalid
|
||||
self.assertRaises(errors.AuthorizationError,
|
||||
self.handler._poll_challenges,
|
||||
self.chall_update, False)
|
||||
self.assertRaises(
|
||||
errors.AuthorizationError, self.handler._poll_challenges,
|
||||
self.chall_update, False)
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.time")
|
||||
def test_unable_to_find_challenge_status(self, unused_mock_time):
|
||||
|
|
@ -229,8 +230,8 @@ class PollChallengesTest(unittest.TestCase):
|
|||
self.chall_update[self.doms[0]].append(
|
||||
challb_to_achall(acme_util.RECOVERY_CONTACT_P, "key", self.doms[0]))
|
||||
self.assertRaises(
|
||||
errors.AuthorizationError,
|
||||
self.handler._poll_challenges, self.chall_update, False)
|
||||
errors.AuthorizationError, self.handler._poll_challenges,
|
||||
self.chall_update, False)
|
||||
|
||||
def test_verify_authzr_failure(self):
|
||||
self.assertRaises(
|
||||
|
|
@ -241,10 +242,10 @@ class PollChallengesTest(unittest.TestCase):
|
|||
# Basically it didn't raise an error and it stopped earlier than
|
||||
# Making all challenges invalid which would make mock_poll_solve_one
|
||||
# change authzr to invalid
|
||||
return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_VALID)
|
||||
return self._mock_poll_solve_one_chall(authzr, messages.STATUS_VALID)
|
||||
|
||||
def _mock_poll_solve_one_invalid(self, authzr):
|
||||
return self._mock_poll_solve_one_chall(authzr, messages2.STATUS_INVALID)
|
||||
return self._mock_poll_solve_one_chall(authzr, messages.STATUS_INVALID)
|
||||
|
||||
def _mock_poll_solve_one_chall(self, authzr, desired_status):
|
||||
# pylint: disable=no-self-use
|
||||
|
|
@ -269,10 +270,10 @@ class PollChallengesTest(unittest.TestCase):
|
|||
else:
|
||||
status_ = authzr.body.status
|
||||
|
||||
new_authzr = messages2.AuthorizationResource(
|
||||
new_authzr = messages.AuthorizationResource(
|
||||
uri=authzr.uri,
|
||||
new_cert_uri=authzr.new_cert_uri,
|
||||
body=messages2.Authorization(
|
||||
body=messages.Authorization(
|
||||
identifier=authzr.body.identifier,
|
||||
challenges=new_challbs,
|
||||
combinations=authzr.body.combinations,
|
||||
|
|
@ -300,7 +301,7 @@ class GenChallengePathTest(unittest.TestCase):
|
|||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and SimpleHTTP with appropriate combos."""
|
||||
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P)
|
||||
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
|
|
@ -315,7 +316,7 @@ class GenChallengePathTest(unittest.TestCase):
|
|||
challbs = (acme_util.RECOVERY_TOKEN_P,
|
||||
acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.SIMPLE_HTTPS_P)
|
||||
acme_util.SIMPLE_HTTP_P)
|
||||
prefs = [challenges.RecoveryToken, challenges.DVSNI]
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
|
||||
|
|
@ -328,7 +329,7 @@ class GenChallengePathTest(unittest.TestCase):
|
|||
acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.POP_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.SIMPLE_HTTPS_P,
|
||||
acme_util.SIMPLE_HTTP_P,
|
||||
acme_util.DNS_P)
|
||||
# Typical webserver client that can do everything except DNS
|
||||
# Attempted to make the order realistic
|
||||
|
|
@ -348,8 +349,8 @@ class GenChallengePathTest(unittest.TestCase):
|
|||
prefs = [challenges.DVSNI]
|
||||
combos = ((0, 1),)
|
||||
|
||||
self.assertRaises(errors.AuthorizationError,
|
||||
self._call, challbs, prefs, combos)
|
||||
self.assertRaises(
|
||||
errors.AuthorizationError, self._call, challbs, prefs, combos)
|
||||
|
||||
|
||||
class MutuallyExclusiveTest(unittest.TestCase):
|
||||
|
|
@ -413,13 +414,61 @@ class IsPreferredTest(unittest.TestCase):
|
|||
def test_mutually_exclusvie(self):
|
||||
self.assertFalse(
|
||||
self._call(
|
||||
acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTPS_P])))
|
||||
acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P])))
|
||||
|
||||
def test_mutually_exclusive_same_type(self):
|
||||
self.assertTrue(
|
||||
self._call(acme_util.DVSNI_P, frozenset([acme_util.DVSNI_P])))
|
||||
|
||||
|
||||
class ReportFailedChallsTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.auth_handler._report_failed_challs."""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt import achallenges
|
||||
|
||||
kwargs = {
|
||||
"chall" : acme_util.SIMPLE_HTTP,
|
||||
"uri": "uri",
|
||||
"status": messages.STATUS_INVALID,
|
||||
"error": messages.Error(typ="tls", detail="detail"),
|
||||
}
|
||||
|
||||
self.simple_http = achallenges.SimpleHTTP(
|
||||
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
|
||||
domain="example.com",
|
||||
key=acme_util.KEY)
|
||||
|
||||
kwargs["chall"] = acme_util.DVSNI
|
||||
self.dvsni_same = achallenges.DVSNI(
|
||||
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
|
||||
domain="example.com",
|
||||
key=acme_util.KEY)
|
||||
|
||||
kwargs["error"] = messages.Error(typ="dnssec", detail="detail")
|
||||
self.dvsni_diff = achallenges.DVSNI(
|
||||
challb=messages.ChallengeBody(**kwargs),# pylint: disable=star-args
|
||||
domain="foo.bar",
|
||||
key=acme_util.KEY)
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.zope.component.getUtility")
|
||||
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])
|
||||
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])
|
||||
|
||||
@mock.patch("letsencrypt.auth_handler.zope.component.getUtility")
|
||||
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])
|
||||
self.assertTrue(mock_zope().add_message.call_count == 2)
|
||||
|
||||
|
||||
def gen_auth_resp(chall_list):
|
||||
"""Generate a dummy authorization response."""
|
||||
return ["%s%s" % (chall.__class__.__name__, chall.domain)
|
||||
|
|
@ -429,8 +478,8 @@ def gen_auth_resp(chall_list):
|
|||
def gen_dom_authzr(domain, unused_new_authzr_uri, challs):
|
||||
"""Generates new authzr for domains."""
|
||||
return acme_util.gen_authzr(
|
||||
messages2.STATUS_PENDING, domain, challs,
|
||||
[messages2.STATUS_PENDING]*len(challs))
|
||||
messages.STATUS_PENDING, domain, challs,
|
||||
[messages.STATUS_PENDING]*len(challs))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -26,12 +26,13 @@ class ClientTest(unittest.TestCase):
|
|||
"""Tests for letsencrypt.client.Client."""
|
||||
|
||||
def setUp(self):
|
||||
self.config = mock.MagicMock(no_verify_ssl=False)
|
||||
self.config = mock.MagicMock(
|
||||
no_verify_ssl=False, config_dir="/etc/letsencrypt")
|
||||
# pylint: disable=star-args
|
||||
self.account = mock.MagicMock(**{"key.pem": KEY})
|
||||
|
||||
from letsencrypt.client import Client
|
||||
with mock.patch("letsencrypt.client.network2.Network") as network:
|
||||
with mock.patch("letsencrypt.client.network.Network") as network:
|
||||
self.client = Client(
|
||||
config=self.config, account_=self.account,
|
||||
dv_auth=None, installer=None)
|
||||
|
|
@ -85,7 +86,6 @@ class ClientTest(unittest.TestCase):
|
|||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
def test_report_new_account(self, mock_zope):
|
||||
# pylint: disable=protected-access
|
||||
self.config.config_dir = "/usr/bin/coffee"
|
||||
self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS"
|
||||
self.account.email = "rhino@jungle.io"
|
||||
|
||||
|
|
@ -100,32 +100,33 @@ class ClientTest(unittest.TestCase):
|
|||
# pylint: disable=protected-access
|
||||
cert = mock.MagicMock()
|
||||
cert.configuration = configobj.ConfigObj()
|
||||
cert.configuration["renewal_configs_dir"] = "/etc/letsencrypt/configs"
|
||||
cert.cli_config = configuration.RenewerConfiguration(self.config)
|
||||
|
||||
cert.configuration["autorenew"] = "True"
|
||||
cert.configuration["autodeploy"] = "True"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal and deployment has been" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
cert.configuration["autorenew"] = "False"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("deployment but not automatic renewal" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
cert.configuration["autodeploy"] = "False"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal and deployment has not" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
cert.configuration["autorenew"] = "True"
|
||||
self.client._report_renewal_status(cert)
|
||||
msg = mock_zope().add_message.call_args[0][0]
|
||||
self.assertTrue("renewal but not automatic deployment" in msg)
|
||||
self.assertTrue(cert.configuration["renewal_configs_dir"] in msg)
|
||||
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
|
||||
|
||||
|
||||
class DetermineAccountTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.client.determine_authenticator."""
|
||||
|
|
|
|||
|
|
@ -9,10 +9,10 @@ class NamespaceConfigTest(unittest.TestCase):
|
|||
"""Tests for letsencrypt.configuration.NamespaceConfig."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
self.namespace = mock.MagicMock(
|
||||
config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar',
|
||||
server='https://acme-server.org:443/new')
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
self.config = NamespaceConfig(self.namespace)
|
||||
|
||||
def test_proxy_getattr(self):
|
||||
|
|
@ -30,23 +30,51 @@ class NamespaceConfigTest(unittest.TestCase):
|
|||
|
||||
@mock.patch('letsencrypt.configuration.constants')
|
||||
def test_dynamic_dirs(self, constants):
|
||||
constants.TEMP_CHECKPOINT_DIR = 't'
|
||||
constants.IN_PROGRESS_DIR = '../p'
|
||||
constants.CERT_KEY_BACKUP_DIR = 'c/'
|
||||
constants.REC_TOKEN_DIR = '/r'
|
||||
constants.ACCOUNTS_DIR = 'acc'
|
||||
constants.ACCOUNT_KEYS_DIR = 'keys'
|
||||
constants.BACKUP_DIR = 'backups'
|
||||
constants.CERT_KEY_BACKUP_DIR = 'c/'
|
||||
constants.CERT_DIR = 'certs'
|
||||
constants.IN_PROGRESS_DIR = '../p'
|
||||
constants.KEY_DIR = 'keys'
|
||||
constants.REC_TOKEN_DIR = '/r'
|
||||
constants.TEMP_CHECKPOINT_DIR = 't'
|
||||
|
||||
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
|
||||
self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p')
|
||||
self.assertEqual(
|
||||
self.config.cert_key_backup, '/tmp/foo/c/acme-server.org:443/new')
|
||||
self.assertEqual(self.config.rec_token_dir, '/r')
|
||||
self.assertEqual(
|
||||
self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new')
|
||||
self.assertEqual(
|
||||
self.config.account_keys_dir,
|
||||
'/tmp/config/acc/acme-server.org:443/new/keys')
|
||||
self.assertEqual(self.config.backup_dir, '/tmp/foo/backups')
|
||||
self.assertEqual(self.config.cert_dir, '/tmp/config/certs')
|
||||
self.assertEqual(
|
||||
self.config.cert_key_backup, '/tmp/foo/c/acme-server.org:443/new')
|
||||
self.assertEqual(self.config.in_progress_dir, '/tmp/foo/../p')
|
||||
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
|
||||
self.assertEqual(self.config.rec_token_dir, '/r')
|
||||
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
|
||||
|
||||
|
||||
class RenewerConfigurationTest(unittest.TestCase):
|
||||
"""Test for letsencrypt.configuration.RenewerConfiguration."""
|
||||
|
||||
def setUp(self):
|
||||
self.namespace = mock.MagicMock(config_dir='/tmp/config')
|
||||
from letsencrypt.configuration import RenewerConfiguration
|
||||
self.config = RenewerConfiguration(self.namespace)
|
||||
|
||||
@mock.patch('letsencrypt.configuration.constants')
|
||||
def test_dynamic_dirs(self, constants):
|
||||
constants.ARCHIVE_DIR = "a"
|
||||
constants.LIVE_DIR = 'l'
|
||||
constants.RENEWAL_CONFIGS_DIR = "renewal_configs"
|
||||
constants.RENEWER_CONFIG_FILENAME = 'r.conf'
|
||||
|
||||
self.assertEqual(self.config.archive_dir, '/tmp/config/a')
|
||||
self.assertEqual(self.config.live_dir, '/tmp/config/l')
|
||||
self.assertEqual(
|
||||
self.config.renewal_configs_dir, '/tmp/config/renewal_configs')
|
||||
self.assertEqual(self.config.renewer_config_file, '/tmp/config/r.conf')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class PerformTest(unittest.TestCase):
|
|||
|
||||
def test_unexpected(self):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptContAuthError, self.auth.perform, [
|
||||
errors.ContAuthError, self.auth.perform, [
|
||||
achallenges.DVSNI(challb=None, domain="0", key="invalid_key")])
|
||||
|
||||
def test_chall_pref(self):
|
||||
|
|
@ -91,8 +91,8 @@ class CleanupTest(unittest.TestCase):
|
|||
token = achallenges.RecoveryToken(challb=None, domain="0")
|
||||
unexpected = achallenges.DVSNI(challb=None, domain="0", key="dummy_key")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptContAuthError,
|
||||
self.auth.cleanup, [token, unexpected])
|
||||
self.assertRaises(
|
||||
errors.ContAuthError, self.auth.cleanup, [token, unexpected])
|
||||
|
||||
|
||||
def gen_client_resp(chall):
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ class AskTest(unittest.TestCase):
|
|||
self.assertTrue(self._call("redirect"))
|
||||
|
||||
def test_key_error(self):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptClientError, self._call, "unknown_enhancement")
|
||||
self.assertRaises(errors.Error, self._call, "unknown_enhancement")
|
||||
|
||||
|
||||
class RedirectTest(unittest.TestCase):
|
||||
|
|
|
|||
26
letsencrypt/tests/errors_test.py
Normal file
26
letsencrypt/tests/errors_test.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
"""Tests for letsencrypt.errors."""
|
||||
import unittest
|
||||
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
|
||||
class FaiiledChallengesTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.errors.FailedChallenges."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.errors import FailedChallenges
|
||||
self.error = FailedChallenges(set([achallenges.DNS(
|
||||
domain="example.com", challb=messages.ChallengeBody(
|
||||
chall=acme_util.DNS, uri=None,
|
||||
error=messages.Error(typ="tls", detail="detail")))]))
|
||||
|
||||
def test_str(self):
|
||||
self.assertTrue(str(self.error).startswith(
|
||||
"Failed authorization procedure. example.com (dns): tls"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -44,8 +44,7 @@ class MakeOrVerifyDirTest(unittest.TestCase):
|
|||
self.assertEqual(stat.S_IMODE(os.stat(self.path).st_mode), 0o400)
|
||||
|
||||
def test_existing_wrong_mode_fails(self):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptClientError, self._call, self.path, 0o600)
|
||||
self.assertRaises(errors.Error, self._call, self.path, 0o600)
|
||||
|
||||
def test_reraises_os_error(self):
|
||||
with mock.patch.object(os, 'makedirs') as makedirs:
|
||||
|
|
|
|||
50
letsencrypt/tests/network_test.py
Normal file
50
letsencrypt/tests/network_test.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Tests for letsencrypt.network."""
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt import account
|
||||
|
||||
|
||||
class NetworkTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.network.Network."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.network import Network
|
||||
self.net = Network(
|
||||
new_reg_uri=None, key=None, alg=None, verify_ssl=None)
|
||||
|
||||
self.config = mock.Mock(accounts_dir=tempfile.mkdtemp())
|
||||
self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212')
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.config.accounts_dir)
|
||||
|
||||
def test_register_from_account(self):
|
||||
self.net.register = mock.Mock()
|
||||
acc = account.Account(
|
||||
self.config, 'key', email='cert-admin@example.com',
|
||||
phone='+12025551212')
|
||||
|
||||
self.net.register_from_account(acc)
|
||||
|
||||
self.net.register.assert_called_with(contact=self.contact)
|
||||
|
||||
def test_register_from_account_partial_info(self):
|
||||
self.net.register = mock.Mock()
|
||||
acc = account.Account(
|
||||
self.config, 'key', email='cert-admin@example.com')
|
||||
acc2 = account.Account(self.config, 'key')
|
||||
|
||||
self.net.register_from_account(acc)
|
||||
self.net.register.assert_called_with(
|
||||
contact=('mailto:cert-admin@example.com',))
|
||||
|
||||
self.net.register_from_account(acc2)
|
||||
self.net.register.assert_called_with(contact=())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
|
|
@ -8,7 +8,7 @@ import mock
|
|||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import proof_of_possession
|
||||
|
|
@ -48,8 +48,8 @@ class ProofOfPossessionTest(unittest.TestCase):
|
|||
issuers=(), authorized_for=())
|
||||
chall = challenges.ProofOfPossession(
|
||||
alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints)
|
||||
challb = messages2.ChallengeBody(
|
||||
chall=chall, uri="http://example", status=messages2.STATUS_PENDING)
|
||||
challb = messages.ChallengeBody(
|
||||
chall=chall, uri="http://example", status=messages.STATUS_PENDING)
|
||||
self.achall = achallenges.ProofOfPossession(
|
||||
challb=challb, domain="example.com")
|
||||
|
||||
|
|
@ -60,8 +60,8 @@ class ProofOfPossessionTest(unittest.TestCase):
|
|||
issuers=(), authorized_for=())
|
||||
chall = challenges.ProofOfPossession(
|
||||
alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints)
|
||||
challb = messages2.ChallengeBody(
|
||||
chall=chall, uri="http://example", status=messages2.STATUS_PENDING)
|
||||
challb = messages.ChallengeBody(
|
||||
chall=chall, uri="http://example", status=messages.STATUS_PENDING)
|
||||
self.achall = achallenges.ProofOfPossession(
|
||||
challb=challb, domain="example.com")
|
||||
self.assertEqual(self.proof_of_pos.perform(self.achall), None)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import configobj
|
|||
import mock
|
||||
import pytz
|
||||
|
||||
from letsencrypt import configuration
|
||||
from letsencrypt.storage import ALL_FOUR
|
||||
|
||||
|
||||
|
|
@ -31,22 +32,24 @@ class RenewableCertTests(unittest.TestCase):
|
|||
def setUp(self):
|
||||
from letsencrypt import storage
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
|
||||
self.cli_config = configuration.RenewerConfiguration(
|
||||
namespace=mock.MagicMock(config_dir=self.tempdir))
|
||||
# TODO: maybe provide RenewerConfiguration.make_dirs?
|
||||
os.makedirs(os.path.join(self.tempdir, "live", "example.org"))
|
||||
os.makedirs(os.path.join(self.tempdir, "archive", "example.org"))
|
||||
os.makedirs(os.path.join(self.tempdir, "configs"))
|
||||
defaults = configobj.ConfigObj()
|
||||
defaults["live_dir"] = os.path.join(self.tempdir, "live")
|
||||
defaults["archive_dir"] = os.path.join(self.tempdir, "archive")
|
||||
defaults["renewal_configs_dir"] = os.path.join(self.tempdir,
|
||||
"configs")
|
||||
|
||||
config = configobj.ConfigObj()
|
||||
for kind in ALL_FOUR:
|
||||
config[kind] = os.path.join(self.tempdir, "live", "example.org",
|
||||
kind + ".pem")
|
||||
config.filename = os.path.join(self.tempdir, "configs",
|
||||
"example.org.conf")
|
||||
self.defaults = defaults # for main() test
|
||||
self.test_rc = storage.RenewableCert(config, defaults)
|
||||
|
||||
self.defaults = configobj.ConfigObj()
|
||||
self.test_rc = storage.RenewableCert(
|
||||
config, self.defaults, self.cli_config)
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
|
@ -457,60 +460,57 @@ class RenewableCertTests(unittest.TestCase):
|
|||
def test_new_lineage(self):
|
||||
"""Test for new_lineage() class method."""
|
||||
from letsencrypt import storage
|
||||
config_dir = self.defaults["renewal_configs_dir"]
|
||||
archive_dir = self.defaults["archive_dir"]
|
||||
live_dir = self.defaults["live_dir"]
|
||||
result = storage.RenewableCert.new_lineage("the-lineage.com", "cert",
|
||||
"privkey", "chain", None,
|
||||
self.defaults)
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert", "privkey", "chain", None,
|
||||
self.defaults, self.cli_config)
|
||||
# This consistency check tests most relevant properties about the
|
||||
# newly created cert lineage.
|
||||
self.assertTrue(result.consistent())
|
||||
self.assertTrue(os.path.exists(os.path.join(config_dir,
|
||||
"the-lineage.com.conf")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
|
||||
with open(result.fullchain) as f:
|
||||
self.assertEqual(f.read(), "cert" + "chain")
|
||||
# Let's do it again and make sure it makes a different lineage
|
||||
result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2",
|
||||
"privkey2", "chain2", None,
|
||||
self.defaults)
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(config_dir, "the-lineage.com-0001.conf")))
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2", None,
|
||||
self.defaults, self.cli_config)
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com-0001.conf")))
|
||||
# Now trigger the detection of already existing files
|
||||
os.mkdir(os.path.join(live_dir, "the-lineage.com-0002"))
|
||||
os.mkdir(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com-0002"))
|
||||
self.assertRaises(ValueError, storage.RenewableCert.new_lineage,
|
||||
"the-lineage.com", "cert3", "privkey3", "chain3",
|
||||
None, self.defaults)
|
||||
os.mkdir(os.path.join(archive_dir, "other-example.com"))
|
||||
None, self.defaults, self.cli_config)
|
||||
os.mkdir(os.path.join(self.cli_config.archive_dir, "other-example.com"))
|
||||
self.assertRaises(ValueError, storage.RenewableCert.new_lineage,
|
||||
"other-example.com", "cert4", "privkey4", "chain4",
|
||||
None, self.defaults)
|
||||
None, self.defaults, self.cli_config)
|
||||
# Make sure it can accept renewal parameters
|
||||
params = {"stuff": "properties of stuff", "great": "awesome"}
|
||||
result = storage.RenewableCert.new_lineage("the-lineage.com", "cert2",
|
||||
"privkey2", "chain2",
|
||||
params, self.defaults)
|
||||
result = storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2",
|
||||
params, self.defaults, self.cli_config)
|
||||
# TODO: Conceivably we could test that the renewal parameters actually
|
||||
# got saved
|
||||
|
||||
def test_new_lineage_nonexistent_dirs(self):
|
||||
"""Test that directories can be created if they don't exist."""
|
||||
from letsencrypt import storage
|
||||
config_dir = self.defaults["renewal_configs_dir"]
|
||||
archive_dir = self.defaults["archive_dir"]
|
||||
live_dir = self.defaults["live_dir"]
|
||||
shutil.rmtree(config_dir)
|
||||
shutil.rmtree(archive_dir)
|
||||
shutil.rmtree(live_dir)
|
||||
storage.RenewableCert.new_lineage("the-lineage.com", "cert2",
|
||||
"privkey2", "chain2",
|
||||
None, self.defaults)
|
||||
shutil.rmtree(self.cli_config.renewal_configs_dir)
|
||||
shutil.rmtree(self.cli_config.archive_dir)
|
||||
shutil.rmtree(self.cli_config.live_dir)
|
||||
|
||||
storage.RenewableCert.new_lineage(
|
||||
"the-lineage.com", "cert2", "privkey2", "chain2",
|
||||
None, self.defaults, self.cli_config)
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(config_dir, "the-lineage.com.conf")))
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(live_dir, "the-lineage.com", "privkey.pem")))
|
||||
self.assertTrue(os.path.exists(
|
||||
os.path.join(archive_dir, "the-lineage.com", "privkey1.pem")))
|
||||
os.path.join(
|
||||
self.cli_config.renewal_configs_dir, "the-lineage.com.conf")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.live_dir, "the-lineage.com", "privkey.pem")))
|
||||
self.assertTrue(os.path.exists(os.path.join(
|
||||
self.cli_config.archive_dir, "the-lineage.com", "privkey1.pem")))
|
||||
|
||||
@mock.patch("letsencrypt.storage.le_util.unique_lineage_name")
|
||||
def test_invalid_config_filename(self, mock_uln):
|
||||
|
|
@ -518,7 +518,7 @@ class RenewableCertTests(unittest.TestCase):
|
|||
mock_uln.return_value = "this_does_not_end_with_dot_conf", "yikes"
|
||||
self.assertRaises(ValueError, storage.RenewableCert.new_lineage,
|
||||
"example.com", "cert", "privkey", "chain",
|
||||
None, self.defaults)
|
||||
None, self.defaults, self.cli_config)
|
||||
|
||||
def test_bad_kind(self):
|
||||
self.assertRaises(ValueError, self.test_rc.current_target, "elephant")
|
||||
|
|
@ -602,22 +602,23 @@ class RenewableCertTests(unittest.TestCase):
|
|||
mock_rc_instance.should_autorenew.return_value = True
|
||||
mock_rc_instance.latest_common_version.return_value = 10
|
||||
mock_rc.return_value = mock_rc_instance
|
||||
with open(os.path.join(self.defaults["renewal_configs_dir"],
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"README"), "w") as f:
|
||||
f.write("This is a README file to make sure that the renewer is")
|
||||
f.write("able to correctly ignore files that don't end in .conf.")
|
||||
with open(os.path.join(self.defaults["renewal_configs_dir"],
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"example.org.conf"), "w") as f:
|
||||
# This isn't actually parsed in this test; we have a separate
|
||||
# test_initialization that tests the initialization, assuming
|
||||
# that configobj can correctly parse the config file.
|
||||
f.write("cert = cert.pem\nprivkey = privkey.pem\n")
|
||||
f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
|
||||
with open(os.path.join(self.defaults["renewal_configs_dir"],
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"example.com.conf"), "w") as f:
|
||||
f.write("cert = cert.pem\nprivkey = privkey.pem\n")
|
||||
f.write("chain = chain.pem\nfullchain = fullchain.pem\n")
|
||||
renewer.main(self.defaults)
|
||||
renewer.main(self.defaults, args=[
|
||||
'--config-dir', self.cli_config.config_dir])
|
||||
self.assertEqual(mock_rc.call_count, 2)
|
||||
self.assertEqual(mock_rc_instance.update_all_links_to.call_count, 2)
|
||||
self.assertEqual(mock_notify.notify.call_count, 4)
|
||||
|
|
@ -630,7 +631,8 @@ class RenewableCertTests(unittest.TestCase):
|
|||
mock_happy_instance.should_autorenew.return_value = False
|
||||
mock_happy_instance.latest_common_version.return_value = 10
|
||||
mock_rc.return_value = mock_happy_instance
|
||||
renewer.main(self.defaults)
|
||||
renewer.main(self.defaults, args=[
|
||||
'--config-dir', self.cli_config.config_dir])
|
||||
self.assertEqual(mock_rc.call_count, 4)
|
||||
self.assertEqual(mock_happy_instance.update_all_links_to.call_count, 0)
|
||||
self.assertEqual(mock_notify.notify.call_count, 4)
|
||||
|
|
@ -638,10 +640,11 @@ class RenewableCertTests(unittest.TestCase):
|
|||
|
||||
def test_bad_config_file(self):
|
||||
from letsencrypt import renewer
|
||||
with open(os.path.join(self.defaults["renewal_configs_dir"],
|
||||
with open(os.path.join(self.cli_config.renewal_configs_dir,
|
||||
"bad.conf"), "w") as f:
|
||||
f.write("incomplete = configfile\n")
|
||||
renewer.main(self.defaults)
|
||||
renewer.main(self.defaults, args=[
|
||||
'--config-dir', self.cli_config.config_dir])
|
||||
# The ValueError is caught inside and nothing happens.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -50,10 +50,9 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
|
|||
def test_add_to_checkpoint_copy_failure(self):
|
||||
with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2:
|
||||
mock_copy2.side_effect = IOError("bad copy")
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.add_to_checkpoint,
|
||||
self.sets[0],
|
||||
"save1")
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.add_to_checkpoint,
|
||||
self.sets[0], "save1")
|
||||
|
||||
def test_checkpoint_conflict(self):
|
||||
"""Make sure that checkpoint errors are thrown appropriately."""
|
||||
|
|
@ -65,17 +64,14 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
|
|||
# This shouldn't throw an error
|
||||
self.reverter.add_to_temp_checkpoint(self.sets[0], "save2")
|
||||
# Raise error
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptReverterError, self.reverter.add_to_checkpoint,
|
||||
self.sets[2], "save3")
|
||||
self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint,
|
||||
self.sets[2], "save3")
|
||||
# Should not cause an error
|
||||
self.reverter.add_to_checkpoint(self.sets[1], "save4")
|
||||
|
||||
# Check to make sure new files are also checked...
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptReverterError,
|
||||
self.reverter.add_to_checkpoint,
|
||||
set([config3]), "invalid save")
|
||||
self.assertRaises(errors.ReverterError, self.reverter.add_to_checkpoint,
|
||||
set([config3]), "invalid save")
|
||||
|
||||
def test_multiple_saves_and_temp_revert(self):
|
||||
self.reverter.add_to_temp_checkpoint(self.sets[0], "save1")
|
||||
|
|
@ -120,65 +116,64 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
|
|||
m_open = mock.mock_open()
|
||||
with mock.patch("letsencrypt.reverter.open", m_open, create=True):
|
||||
m_open.side_effect = OSError("bad open")
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.register_file_creation,
|
||||
True, self.config1)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.register_file_creation,
|
||||
True, self.config1)
|
||||
|
||||
def test_bad_registration(self):
|
||||
# Made this mistake and want to make sure it doesn't happen again...
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.register_file_creation,
|
||||
"filepath")
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.register_file_creation,
|
||||
"filepath")
|
||||
|
||||
def test_recovery_routine_in_progress_failure(self):
|
||||
self.reverter.add_to_checkpoint(self.sets[0], "perm save")
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.reverter._recover_checkpoint = mock.MagicMock(
|
||||
side_effect=errors.LetsEncryptReverterError)
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.recovery_routine)
|
||||
side_effect=errors.ReverterError)
|
||||
self.assertRaises(errors.ReverterError, self.reverter.recovery_routine)
|
||||
|
||||
def test_recover_checkpoint_revert_temp_failures(self):
|
||||
# pylint: disable=invalid-name
|
||||
mock_recover = mock.MagicMock(
|
||||
side_effect=errors.LetsEncryptReverterError("e"))
|
||||
side_effect=errors.ReverterError("e"))
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.reverter._recover_checkpoint = mock_recover
|
||||
|
||||
self.reverter.add_to_temp_checkpoint(self.sets[0], "config1 save")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.revert_temporary_config)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.revert_temporary_config)
|
||||
|
||||
def test_recover_checkpoint_rollback_failure(self):
|
||||
mock_recover = mock.MagicMock(
|
||||
side_effect=errors.LetsEncryptReverterError("e"))
|
||||
side_effect=errors.ReverterError("e"))
|
||||
# pylint: disable=protected-access
|
||||
self.reverter._recover_checkpoint = mock_recover
|
||||
|
||||
self.reverter.add_to_checkpoint(self.sets[0], "config1 save")
|
||||
self.reverter.finalize_checkpoint("Title")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.rollback_checkpoints, 1)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.rollback_checkpoints, 1)
|
||||
|
||||
def test_recover_checkpoint_copy_failure(self):
|
||||
self.reverter.add_to_temp_checkpoint(self.sets[0], "save1")
|
||||
|
||||
with mock.patch("letsencrypt.reverter.shutil.copy2") as mock_copy2:
|
||||
mock_copy2.side_effect = OSError("bad copy")
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.revert_temporary_config)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.revert_temporary_config)
|
||||
|
||||
def test_recover_checkpoint_rm_failure(self):
|
||||
self.reverter.add_to_temp_checkpoint(self.sets[0], "temp save")
|
||||
|
||||
with mock.patch("letsencrypt.reverter.shutil.rmtree") as mock_rmtree:
|
||||
mock_rmtree.side_effect = OSError("Cannot remove tree")
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.revert_temporary_config)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.revert_temporary_config)
|
||||
|
||||
@mock.patch("letsencrypt.reverter.logging.warning")
|
||||
def test_recover_checkpoint_missing_new_files(self, mock_warn):
|
||||
|
|
@ -191,8 +186,8 @@ class ReverterCheckpointLocalTest(unittest.TestCase):
|
|||
def test_recover_checkpoint_remove_failure(self, mock_remove):
|
||||
self.reverter.register_file_creation(True, self.config1)
|
||||
mock_remove.side_effect = OSError("Can't remove")
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.revert_temporary_config)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.revert_temporary_config)
|
||||
|
||||
def test_recovery_routine_temp_and_perm(self):
|
||||
# Register a new perm checkpoint file
|
||||
|
|
@ -251,14 +246,11 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
|
||||
def test_rollback_improper_inputs(self):
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptReverterError,
|
||||
self.reverter.rollback_checkpoints, "-1")
|
||||
errors.ReverterError, self.reverter.rollback_checkpoints, "-1")
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptReverterError,
|
||||
self.reverter.rollback_checkpoints, -1000)
|
||||
errors.ReverterError, self.reverter.rollback_checkpoints, -1000)
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptReverterError,
|
||||
self.reverter.rollback_checkpoints, "one")
|
||||
errors.ReverterError, self.reverter.rollback_checkpoints, "one")
|
||||
|
||||
def test_rollback_finalize_checkpoint_valid_inputs(self):
|
||||
# pylint: disable=invalid-name
|
||||
|
|
@ -299,9 +291,8 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
self.reverter.add_to_checkpoint(self.sets[0], "perm save")
|
||||
mock_move.side_effect = OSError("cannot move")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.finalize_checkpoint,
|
||||
"Title")
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.finalize_checkpoint, "Title")
|
||||
|
||||
@mock.patch("letsencrypt.reverter.os.rename")
|
||||
def test_finalize_checkpoint_no_rename_directory(self, mock_rename):
|
||||
|
|
@ -309,9 +300,8 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
self.reverter.add_to_checkpoint(self.sets[0], "perm save")
|
||||
mock_rename.side_effect = OSError
|
||||
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.finalize_checkpoint,
|
||||
"Title")
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.finalize_checkpoint, "Title")
|
||||
|
||||
@mock.patch("letsencrypt.reverter.logging")
|
||||
def test_rollback_too_many(self, mock_logging):
|
||||
|
|
@ -347,8 +337,8 @@ class TestFullCheckpointsReverter(unittest.TestCase):
|
|||
# It must just be clean checkpoints
|
||||
os.makedirs(os.path.join(self.config.backup_dir, "in_progress"))
|
||||
|
||||
self.assertRaises(errors.LetsEncryptReverterError,
|
||||
self.reverter.view_config_changes)
|
||||
self.assertRaises(
|
||||
errors.ReverterError, self.reverter.view_config_changes)
|
||||
|
||||
def _setup_three_checkpoints(self):
|
||||
"""Generate some finalized checkpoints."""
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class RevokerTest(RevokerBase):
|
|||
def tearDown(self):
|
||||
shutil.rmtree(self.backup_dir)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected")
|
||||
@mock.patch("letsencrypt.network.Network.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_key_all(self, mock_display, mock_net):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
|
@ -80,16 +80,14 @@ class RevokerTest(RevokerBase):
|
|||
@mock.patch("letsencrypt.revoker.Crypto.PublicKey.RSA.importKey")
|
||||
def test_revoke_by_invalid_keys(self, mock_import):
|
||||
mock_import.side_effect = ValueError
|
||||
self.assertRaises(errors.LetsEncryptRevokerError,
|
||||
self.revoker.revoke_from_key,
|
||||
self.key)
|
||||
self.assertRaises(
|
||||
errors.RevokerError, self.revoker.revoke_from_key, self.key)
|
||||
|
||||
mock_import.side_effect = [mock.Mock(), IndexError]
|
||||
self.assertRaises(errors.LetsEncryptRevokerError,
|
||||
self.revoker.revoke_from_key,
|
||||
self.key)
|
||||
self.assertRaises(
|
||||
errors.RevokerError, self.revoker.revoke_from_key, self.key)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected")
|
||||
@mock.patch("letsencrypt.network.Network.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_wrong_key(self, mock_display, mock_net):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
|
@ -105,7 +103,7 @@ class RevokerTest(RevokerBase):
|
|||
# No revocation went through
|
||||
self.assertEqual(mock_net.call_count, 0)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected")
|
||||
@mock.patch("letsencrypt.network.Network.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_cert(self, mock_display, mock_net):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
|
@ -122,7 +120,7 @@ class RevokerTest(RevokerBase):
|
|||
|
||||
self.assertEqual(mock_net.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected")
|
||||
@mock.patch("letsencrypt.network.Network.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_cert_not_found(self, mock_display, mock_net):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
|
@ -141,7 +139,7 @@ class RevokerTest(RevokerBase):
|
|||
|
||||
self.assertEqual(mock_net.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected")
|
||||
@mock.patch("letsencrypt.network.Network.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_menu(self, mock_display, mock_net):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
|
@ -165,7 +163,7 @@ class RevokerTest(RevokerBase):
|
|||
self.assertEqual(mock_display.more_info_cert.call_count, 1)
|
||||
|
||||
@mock.patch("letsencrypt.revoker.logging")
|
||||
@mock.patch("letsencrypt.revoker.network.Network.send_and_receive_expected")
|
||||
@mock.patch("letsencrypt.network.Network.revoke")
|
||||
@mock.patch("letsencrypt.revoker.revocation")
|
||||
def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log):
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
|
@ -188,7 +186,7 @@ class RevokerTest(RevokerBase):
|
|||
@mock.patch("letsencrypt.revoker.logging")
|
||||
def test_safe_revoke_acme_fail(self, mock_log, mock_revoke, mock_display):
|
||||
# pylint: disable=protected-access
|
||||
mock_revoke.side_effect = errors.LetsEncryptClientError
|
||||
mock_revoke.side_effect = errors.Error
|
||||
mock_display().confirm_revocation.return_value = True
|
||||
|
||||
self.revoker._safe_revoke(self.certs)
|
||||
|
|
@ -198,9 +196,8 @@ class RevokerTest(RevokerBase):
|
|||
def test_acme_revoke_failure(self, mock_crypto):
|
||||
# pylint: disable=protected-access
|
||||
mock_crypto.side_effect = ValueError
|
||||
self.assertRaises(errors.LetsEncryptClientError,
|
||||
self.revoker._acme_revoke,
|
||||
self.certs[0])
|
||||
self.assertRaises(
|
||||
errors.Error, self.revoker._acme_revoke, self.certs[0])
|
||||
|
||||
def test_remove_certs_from_list_bad_certs(self):
|
||||
# pylint: disable=protected-access
|
||||
|
|
@ -215,9 +212,8 @@ class RevokerTest(RevokerBase):
|
|||
new_cert.orig = Cert.PathStatus("false path", "not here")
|
||||
new_cert.orig_key = Cert.PathStatus("false path", "not here")
|
||||
|
||||
self.assertRaises(errors.LetsEncryptRevokerError,
|
||||
self.revoker._remove_certs_from_list,
|
||||
[new_cert])
|
||||
self.assertRaises(errors.RevokerError,
|
||||
self.revoker._remove_certs_from_list, [new_cert])
|
||||
|
||||
def _backups_exist(self, row):
|
||||
# pylint: disable=protected-access
|
||||
|
|
@ -330,7 +326,7 @@ class CertTest(unittest.TestCase):
|
|||
|
||||
def test_failed_load(self):
|
||||
from letsencrypt.revoker import Cert
|
||||
self.assertRaises(errors.LetsEncryptRevokerError, Cert, self.key_path)
|
||||
self.assertRaises(errors.RevokerError, Cert, self.key_path)
|
||||
|
||||
def test_no_row(self):
|
||||
self.assertEqual(self.certs[0].get_row(), None)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ from letsencrypt import errors
|
|||
from letsencrypt import interfaces
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
from letsencrypt_apache import constants
|
||||
from letsencrypt_apache import dvsni
|
||||
from letsencrypt_apache import obj
|
||||
|
|
@ -87,8 +89,6 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
def add_parser_arguments(cls, add):
|
||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||
help="Apache server root directory.")
|
||||
add("mod-ssl-conf", default=constants.CLI_DEFAULTS["mod_ssl_conf"],
|
||||
help="Contains standard Apache SSL directives.")
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"],
|
||||
help="Path to the 'apache2ctl' binary, used for 'configtest' and "
|
||||
"retrieving Apache2 version number.")
|
||||
|
|
@ -97,6 +97,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
add("init-script", default=constants.CLI_DEFAULTS["init_script"],
|
||||
help="Path to the Apache init script (used for server "
|
||||
"reload/restart).")
|
||||
add("le-vhost-ext", default=constants.CLI_DEFAULTS["le_vhost_ext"],
|
||||
help="SSL vhost configuration extension.")
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Initialize an Apache Configurator.
|
||||
|
|
@ -123,10 +126,15 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
self.vhosts = None
|
||||
self._enhance_func = {"redirect": self._enable_redirect}
|
||||
|
||||
@property
|
||||
def mod_ssl_conf(self):
|
||||
"""Full absolute path to SSL configuration file."""
|
||||
return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST)
|
||||
|
||||
def prepare(self):
|
||||
"""Prepare the authenticator/installer."""
|
||||
self.parser = parser.ApacheParser(
|
||||
self.aug, self.conf('server-root'), self.conf('mod-ssl-conf'))
|
||||
self.aug, self.conf('server-root'), self.mod_ssl_conf)
|
||||
# Check for errors in parsing files with Augeas
|
||||
self.check_parsing_errors("httpd.aug")
|
||||
|
||||
|
|
@ -144,7 +152,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
# on initialization
|
||||
self._prepare_server_https()
|
||||
|
||||
temp_install(self.conf('mod-ssl-conf'))
|
||||
temp_install(self.mod_ssl_conf)
|
||||
|
||||
def deploy_cert(self, domain, cert_path, key_path, chain_path=None):
|
||||
"""Deploys certificate to specified virtual host.
|
||||
|
|
@ -179,8 +187,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
if not path["cert_path"] or not path["cert_key"]:
|
||||
# Throw some can't find all of the directives error"
|
||||
logging.warn(
|
||||
"Cannot find a cert or key directive in %s", vhost.path)
|
||||
logging.warn("VirtualHost was not modified")
|
||||
"Cannot find a cert or key directive in %s. "
|
||||
"VirtualHost was not modified", vhost.path)
|
||||
# Presumably break here so that the virtualhost is not modified
|
||||
return False
|
||||
|
||||
|
|
@ -230,7 +238,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
return vhost
|
||||
# Checking for domain name in vhost address
|
||||
# This technique is not recommended by Apache but is technically valid
|
||||
target_addr = obj.Addr((target_name, "443"))
|
||||
target_addr = common.Addr((target_name, "443"))
|
||||
for vhost in self.vhosts:
|
||||
if target_addr in vhost.addrs:
|
||||
self.assoc[target_name] = vhost
|
||||
|
|
@ -321,7 +329,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
addrs = set()
|
||||
args = self.aug.match(path + "/arg")
|
||||
for arg in args:
|
||||
addrs.add(obj.Addr.fromstring(self.aug.get(arg)))
|
||||
addrs.add(common.Addr.fromstring(self.aug.get(arg)))
|
||||
is_ssl = False
|
||||
|
||||
if self.parser.find_dir(
|
||||
|
|
@ -406,8 +414,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
# Note: This could be made to also look for ip:443 combo
|
||||
# TODO: Need to search only open directives and IfMod mod_ssl.c
|
||||
if len(self.parser.find_dir(parser.case_i("Listen"), "443")) == 0:
|
||||
logging.debug("No Listen 443 directive found")
|
||||
logging.debug("Setting the Apache Server to Listen on port 443")
|
||||
logging.debug("No Listen 443 directive found. Setting the "
|
||||
"Apache Server to Listen on port 443")
|
||||
path = self.parser.add_dir_to_ifmodssl(
|
||||
parser.get_aug_path(self.parser.loc["listen"]), "Listen", "443")
|
||||
self.save_notes += "Added Listen 443 directive to %s\n" % path
|
||||
|
|
@ -443,7 +451,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
"""Makes an ssl_vhost version of a nonssl_vhost.
|
||||
|
||||
Duplicates vhost and adds default ssl options
|
||||
New vhost will reside as (nonssl_vhost.path) + ``IConfig.le_vhost_ext``
|
||||
New vhost will reside as (nonssl_vhost.path) +
|
||||
``letsencrypt_apache.constants.CLI_DEFAULTS["le_vhost_ext"]``
|
||||
|
||||
.. note:: This function saves the configuration
|
||||
|
||||
|
|
@ -457,9 +466,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
avail_fp = nonssl_vhost.filep
|
||||
# Get filepath of new ssl_vhost
|
||||
if avail_fp.endswith(".conf"):
|
||||
ssl_fp = avail_fp[:-(len(".conf"))] + self.config.le_vhost_ext
|
||||
ssl_fp = avail_fp[:-(len(".conf"))] + self.conf("le_vhost_ext")
|
||||
else:
|
||||
ssl_fp = avail_fp + self.config.le_vhost_ext
|
||||
ssl_fp = avail_fp + self.conf("le_vhost_ext")
|
||||
|
||||
# First register the creation so that it is properly removed if
|
||||
# configuration is rolled back
|
||||
|
|
@ -486,7 +495,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
addr_match % (ssl_fp, parser.case_i("VirtualHost")))
|
||||
|
||||
for addr in ssl_addr_p:
|
||||
old_addr = obj.Addr.fromstring(
|
||||
old_addr = common.Addr.fromstring(
|
||||
str(self.aug.get(addr)))
|
||||
ssl_addr = old_addr.get_addr_obj("443")
|
||||
self.aug.set(addr, str(ssl_addr))
|
||||
|
|
@ -552,9 +561,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
return self._enhance_func[enhancement](
|
||||
self.choose_vhost(domain), options)
|
||||
except ValueError:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
raise errors.ConfiguratorError(
|
||||
"Unsupported enhancement: {}".format(enhancement))
|
||||
except errors.LetsEncryptConfiguratorError:
|
||||
except errors.ConfiguratorError:
|
||||
logging.warn("Failed %s for %s", enhancement, domain)
|
||||
|
||||
def _enable_redirect(self, ssl_vhost, unused_options):
|
||||
|
|
@ -600,7 +609,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
return
|
||||
else:
|
||||
logging.info("Unknown redirect exists for this vhost")
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
raise errors.ConfiguratorError(
|
||||
"Unknown redirect already exists "
|
||||
"in {}".format(general_v.filep))
|
||||
# Add directives to server
|
||||
|
|
@ -671,9 +680,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
# Make sure adding the vhost will be safe
|
||||
conflict, host_or_addrs = self._conflicting_host(ssl_vhost)
|
||||
if conflict:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to create a redirection vhost "
|
||||
"- {}".format(host_or_addrs))
|
||||
raise errors.ConfiguratorError(
|
||||
"Unable to create a redirection vhost - {}".format(
|
||||
host_or_addrs))
|
||||
|
||||
redirect_addrs = host_or_addrs
|
||||
|
||||
|
|
@ -789,8 +798,8 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
# Instead... should look for vhost of the form *:80
|
||||
# Should we prompt the user?
|
||||
ssl_addrs = ssl_vhost.addrs
|
||||
if ssl_addrs == obj.Addr.fromstring("_default_:443"):
|
||||
ssl_addrs = [obj.Addr.fromstring("*:443")]
|
||||
if ssl_addrs == common.Addr.fromstring("_default_:443"):
|
||||
ssl_addrs = [common.Addr.fromstring("*:443")]
|
||||
|
||||
for vhost in self.vhosts:
|
||||
found = 0
|
||||
|
|
@ -920,9 +929,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
|
||||
if proc.returncode != 0:
|
||||
# Enter recovery routine...
|
||||
logging.error("Configtest failed")
|
||||
logging.error(stdout)
|
||||
logging.error(stderr)
|
||||
logging.error("Configtest failed\n%s\n%s", stdout, stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -951,8 +958,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptConfiguratorError:
|
||||
Unable to find Apache version
|
||||
:raises .ConfiguratorError: if unable to find Apache version
|
||||
|
||||
"""
|
||||
try:
|
||||
|
|
@ -962,15 +968,14 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
|||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[0]
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
raise errors.ConfiguratorError(
|
||||
"Unable to run %s -v" % self.conf('ctl'))
|
||||
|
||||
regex = re.compile(r"Apache/([0-9\.]*)", re.IGNORECASE)
|
||||
matches = regex.findall(text)
|
||||
|
||||
if len(matches) != 1:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to find Apache version")
|
||||
raise errors.ConfiguratorError("Unable to find Apache version")
|
||||
|
||||
return tuple([int(i) for i in matches[0].split(".")])
|
||||
|
||||
|
|
@ -1052,9 +1057,8 @@ def enable_mod(mod_name, apache_init_script, apache_enmod):
|
|||
stdout=open("/dev/null", "w"),
|
||||
stderr=open("/dev/null", "w"))
|
||||
apache_restart(apache_init_script)
|
||||
except (OSError, subprocess.CalledProcessError) as err:
|
||||
logging.error("Error enabling mod_%s", mod_name)
|
||||
logging.error("Exception: %s", err)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
logging.exception("Error enabling mod_%s", mod_name)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
|
@ -1080,12 +1084,11 @@ def mod_loaded(module, apache_ctl):
|
|||
except (OSError, ValueError):
|
||||
logging.error(
|
||||
"Error accessing %s for loaded modules!", apache_ctl)
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Error accessing loaded modules")
|
||||
raise errors.ConfiguratorError("Error accessing loaded modules")
|
||||
# Small errors that do not impede
|
||||
if proc.returncode != 0:
|
||||
logging.warn("Error in checking loaded module list: %s", stderr)
|
||||
raise errors.LetsEncryptMisconfigurationError(
|
||||
raise errors.MisconfigurationError(
|
||||
"Apache is unable to check whether or not the module is "
|
||||
"loaded because Apache is misconfigured.")
|
||||
|
||||
|
|
@ -1117,9 +1120,7 @@ def apache_restart(apache_init_script):
|
|||
|
||||
if proc.returncode != 0:
|
||||
# Enter recovery routine...
|
||||
logging.error("Apache Restart Failed!")
|
||||
logging.error(stdout)
|
||||
logging.error(stderr)
|
||||
logging.error("Apache Restart Failed!\n%s\n%s", stdout, stderr)
|
||||
return False
|
||||
|
||||
except (OSError, ValueError):
|
||||
|
|
@ -1167,4 +1168,4 @@ def temp_install(options_ssl):
|
|||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
if not os.path.isfile(options_ssl):
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, options_ssl)
|
||||
shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl)
|
||||
|
|
|
|||
|
|
@ -4,15 +4,17 @@ import pkg_resources
|
|||
|
||||
CLI_DEFAULTS = dict(
|
||||
server_root="/etc/apache2",
|
||||
mod_ssl_conf="/etc/letsencrypt/options-ssl-apache.conf",
|
||||
ctl="apache2ctl",
|
||||
enmod="a2enmod",
|
||||
init_script="/etc/init.d/apache2",
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
)
|
||||
"""CLI defaults."""
|
||||
|
||||
MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
|
||||
"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
|
||||
|
||||
MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
|
||||
"letsencrypt_apache", "options-ssl-apache.conf")
|
||||
"""Path to the Apache mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
|
|
|||
|
|
@ -2,10 +2,12 @@
|
|||
import logging
|
||||
import os
|
||||
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
from letsencrypt_apache import parser
|
||||
|
||||
|
||||
class ApacheDvsni(object):
|
||||
class ApacheDvsni(common.Dvsni):
|
||||
"""Class performs DVSNI challenges within the Apache configurator.
|
||||
|
||||
:ivar configurator: ApacheConfigurator object
|
||||
|
|
@ -42,26 +44,6 @@ class ApacheDvsni(object):
|
|||
</VirtualHost>
|
||||
|
||||
"""
|
||||
def __init__(self, configurator):
|
||||
self.configurator = configurator
|
||||
self.achalls = []
|
||||
self.indices = []
|
||||
self.challenge_conf = os.path.join(
|
||||
configurator.config.config_dir, "le_dvsni_cert_challenge.conf")
|
||||
# self.completed = 0
|
||||
|
||||
def add_chall(self, achall, idx=None):
|
||||
"""Add challenge to DVSNI object to perform at once.
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
|
||||
:param int idx: index to challenge in a larger array
|
||||
|
||||
"""
|
||||
self.achalls.append(achall)
|
||||
if idx is not None:
|
||||
self.indices.append(idx)
|
||||
|
||||
def perform(self):
|
||||
"""Peform a DVSNI challenge."""
|
||||
|
|
@ -77,10 +59,9 @@ class ApacheDvsni(object):
|
|||
vhost = self.configurator.choose_vhost(achall.domain)
|
||||
if vhost is None:
|
||||
logging.error(
|
||||
"No vhost exists with servername or alias of: %s",
|
||||
achall.domain)
|
||||
logging.error("No _default_:443 vhost exists")
|
||||
logging.error("Please specify servernames in the Apache config")
|
||||
"No vhost exists with servername or alias of: %s. "
|
||||
"No _default_:443 vhost exists. Please specify servernames "
|
||||
"in the Apache config", achall.domain)
|
||||
return None
|
||||
|
||||
# TODO - @jdkasten review this code to make sure it makes sense
|
||||
|
|
@ -107,28 +88,12 @@ class ApacheDvsni(object):
|
|||
|
||||
return responses
|
||||
|
||||
def _setup_challenge_cert(self, achall, s=None):
|
||||
# pylint: disable=invalid-name
|
||||
"""Generate and write out challenge certificate."""
|
||||
cert_path = self.get_cert_file(achall)
|
||||
# Register the path before you write out the file
|
||||
self.configurator.reverter.register_file_creation(True, cert_path)
|
||||
|
||||
cert_pem, response = achall.gen_cert_and_response(s)
|
||||
|
||||
# Write out challenge cert
|
||||
with open(cert_path, "w") as cert_chall_fd:
|
||||
cert_chall_fd.write(cert_pem)
|
||||
|
||||
return response
|
||||
|
||||
def _mod_config(self, ll_addrs):
|
||||
"""Modifies Apache config files to include challenge vhosts.
|
||||
|
||||
Result: Apache config includes virtual servers for issued challs
|
||||
|
||||
:param list ll_addrs: list of list of
|
||||
:class:`letsencrypt.plugins.apache.obj.Addr` to apply
|
||||
:param list ll_addrs: list of list of `~.common.Addr` to apply
|
||||
|
||||
"""
|
||||
# TODO: Use ip address of existing vhost instead of relying on FQDN
|
||||
|
|
@ -167,7 +132,7 @@ class ApacheDvsni(object):
|
|||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
|
||||
:param list ip_addrs: addresses of challenged domain
|
||||
:class:`list` of type :class:`~apache.obj.Addr`
|
||||
:class:`list` of type `~.common.Addr`
|
||||
|
||||
:returns: virtual host configuration text
|
||||
:rtype: str
|
||||
|
|
@ -175,7 +140,7 @@ class ApacheDvsni(object):
|
|||
"""
|
||||
ips = " ".join(str(i) for i in ip_addrs)
|
||||
document_root = os.path.join(
|
||||
self.configurator.config.config_dir, "dvsni_page/")
|
||||
self.configurator.config.work_dir, "dvsni_page/")
|
||||
# TODO: Python docs is not clear how mutliline string literal
|
||||
# newlines are parsed on different platforms. At least on
|
||||
# Linux (Debian sid), when source file uses CRLF, Python still
|
||||
|
|
@ -186,16 +151,3 @@ class ApacheDvsni(object):
|
|||
ssl_options_conf_path=self.configurator.parser.loc["ssl_options"],
|
||||
cert_path=self.get_cert_file(achall), key_path=achall.key.file,
|
||||
document_root=document_root).replace("\n", os.linesep)
|
||||
|
||||
def get_cert_file(self, achall):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
|
||||
:param achall: Annotated DVSNI challenge.
|
||||
:type achall: :class:`letsencrypt.achallenges.DVSNI`
|
||||
|
||||
:returns: certificate file name
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return os.path.join(
|
||||
self.configurator.config.work_dir, achall.nonce_domain + ".crt")
|
||||
|
|
|
|||
|
|
@ -1,54 +1,13 @@
|
|||
"""Module contains classes used by the Apache Configurator."""
|
||||
|
||||
|
||||
class Addr(object):
|
||||
r"""Represents an Apache VirtualHost address.
|
||||
|
||||
:param str addr: addr part of vhost address
|
||||
:param str port: port number or \*, or ""
|
||||
|
||||
"""
|
||||
def __init__(self, tup):
|
||||
self.tup = tup
|
||||
|
||||
@classmethod
|
||||
def fromstring(cls, str_addr):
|
||||
"""Initialize Addr from string."""
|
||||
tup = str_addr.partition(':')
|
||||
return cls((tup[0], tup[2]))
|
||||
|
||||
def __str__(self):
|
||||
if self.tup[1]:
|
||||
return "%s:%s" % self.tup
|
||||
return self.tup[0]
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, self.__class__):
|
||||
return self.tup == other.tup
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.tup)
|
||||
|
||||
def get_addr(self):
|
||||
"""Return addr part of Addr object."""
|
||||
return self.tup[0]
|
||||
|
||||
def get_port(self):
|
||||
"""Return port."""
|
||||
return self.tup[1]
|
||||
|
||||
def get_addr_obj(self, port):
|
||||
"""Return new address object with same addr and new port."""
|
||||
return self.__class__((self.tup[0], port))
|
||||
|
||||
|
||||
class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
"""Represents an Apache Virtualhost.
|
||||
|
||||
:ivar str filep: file path of VH
|
||||
:ivar str path: Augeas path to virtual host
|
||||
:ivar set addrs: Virtual Host addresses (:class:`set` of :class:`Addr`)
|
||||
:ivar set addrs: Virtual Host addresses (:class:`set` of
|
||||
:class:`common.Addr`)
|
||||
:ivar set names: Server names/aliases of vhost
|
||||
(:class:`list` of :class:`str`)
|
||||
|
||||
|
|
|
|||
|
|
@ -347,8 +347,7 @@ class ApacheParser(object):
|
|||
if os.path.isfile(os.path.join(self.root, name)):
|
||||
return os.path.join(self.root, name)
|
||||
|
||||
raise errors.LetsEncryptNoInstallationError(
|
||||
"Could not find configuration root")
|
||||
raise errors.NoInstallationError("Could not find configuration root")
|
||||
|
||||
def _set_user_config_file(self, root):
|
||||
"""Set the appropriate user configuration file
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ from letsencrypt import achallenges
|
|||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
from letsencrypt_apache import configurator
|
||||
from letsencrypt_apache import obj
|
||||
from letsencrypt_apache import parser
|
||||
|
||||
from letsencrypt_apache.tests import util
|
||||
|
|
@ -31,8 +32,7 @@ class TwoVhost80Test(util.ApacheTest):
|
|||
"mod_loaded") as mock_load:
|
||||
mock_load.return_value = True
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir,
|
||||
self.ssl_options)
|
||||
self.config_path, self.config_dir, self.work_dir)
|
||||
|
||||
self.vh_truth = util.get_vh_truth(
|
||||
self.temp_dir, "debian_apache_2_4/two_vhost_80")
|
||||
|
|
@ -112,7 +112,7 @@ class TwoVhost80Test(util.ApacheTest):
|
|||
self.vh_truth[1].filep)
|
||||
|
||||
def test_is_name_vhost(self):
|
||||
addr = obj.Addr.fromstring("*:80")
|
||||
addr = common.Addr.fromstring("*:80")
|
||||
self.assertTrue(self.config.is_name_vhost(addr))
|
||||
self.config.version = (2, 2)
|
||||
self.assertFalse(self.config.is_name_vhost(addr))
|
||||
|
|
@ -133,7 +133,7 @@ class TwoVhost80Test(util.ApacheTest):
|
|||
self.assertEqual(ssl_vhost.path,
|
||||
"/files" + ssl_vhost.filep + "/IfModule/VirtualHost")
|
||||
self.assertEqual(len(ssl_vhost.addrs), 1)
|
||||
self.assertEqual(set([obj.Addr.fromstring("*:443")]), ssl_vhost.addrs)
|
||||
self.assertEqual(set([common.Addr.fromstring("*:443")]), ssl_vhost.addrs)
|
||||
self.assertEqual(ssl_vhost.names, set(["encryption-example.demo"]))
|
||||
self.assertTrue(ssl_vhost.ssl)
|
||||
self.assertFalse(ssl_vhost.enabled)
|
||||
|
|
@ -196,17 +196,14 @@ class TwoVhost80Test(util.ApacheTest):
|
|||
|
||||
mock_popen().communicate.return_value = (
|
||||
"Server Version: Apache (Debian)", "")
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
self.assertRaises(errors.ConfiguratorError, self.config.get_version)
|
||||
|
||||
mock_popen().communicate.return_value = (
|
||||
"Server Version: Apache/2.3{0} Apache/2.4.7".format(os.linesep), "")
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
self.assertRaises(errors.ConfiguratorError, self.config.get_version)
|
||||
|
||||
mock_popen.side_effect = OSError("Can't find program")
|
||||
self.assertRaises(
|
||||
errors.LetsEncryptConfiguratorError, self.config.get_version)
|
||||
self.assertRaises(errors.ConfiguratorError, self.config.get_version)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"""Test for letsencrypt_apache.dvsni."""
|
||||
import pkg_resources
|
||||
import unittest
|
||||
import shutil
|
||||
|
||||
|
|
@ -7,18 +6,17 @@ import mock
|
|||
|
||||
from acme import challenges
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import le_util
|
||||
from letsencrypt.plugins import common
|
||||
from letsencrypt.plugins import common_test
|
||||
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
from letsencrypt_apache import obj
|
||||
from letsencrypt_apache.tests import util
|
||||
|
||||
|
||||
class DvsniPerformTest(util.ApacheTest):
|
||||
"""Test the ApacheDVSNI challenge."""
|
||||
|
||||
achalls = common_test.DvsniTest.achalls
|
||||
|
||||
def setUp(self):
|
||||
super(DvsniPerformTest, self).setUp()
|
||||
|
||||
|
|
@ -26,38 +24,11 @@ class DvsniPerformTest(util.ApacheTest):
|
|||
"mod_loaded") as mock_load:
|
||||
mock_load.return_value = True
|
||||
config = util.get_apache_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir,
|
||||
self.ssl_options)
|
||||
self.config_path, self.config_dir, self.work_dir)
|
||||
|
||||
from letsencrypt_apache import dvsni
|
||||
self.sni = dvsni.ApacheDvsni(config)
|
||||
|
||||
rsa256_file = pkg_resources.resource_filename(
|
||||
"acme.jose", "testdata/rsa256_key.pem")
|
||||
rsa256_pem = pkg_resources.resource_string(
|
||||
"acme.jose", "testdata/rsa256_key.pem")
|
||||
|
||||
auth_key = le_util.Key(rsa256_file, rsa256_pem)
|
||||
self.achalls = [
|
||||
achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
r="\x8c\x8a\xbf_-f\\cw\xee\xd6\xf8/\xa5\xe3\xfd\xeb9"
|
||||
"\xf1\xf5\xb9\xefVM\xc9w\xa4u\x9c\xe1\x87\xb4",
|
||||
nonce="7\xbc^\xb7]>\x00\xa1\x9bOcU\x84^Z\x18",
|
||||
), "pending"),
|
||||
domain="encryption-example.demo", key=auth_key),
|
||||
achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
r="\xba\xa9\xda?<m\xaewmx\xea\xad\xadv\xf4\x02\xc9y\x80"
|
||||
"\xe2_X\t\xe7\xc7\xa4\t\xca\xf7&\x945",
|
||||
nonce="Y\xed\x01L\xac\x95\xf7pW\xb1\xd7\xa1\xb2\xc5"
|
||||
"\x96\xba",
|
||||
), "pending"),
|
||||
domain="letsencrypt.demo", key=auth_key),
|
||||
]
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
|
|
@ -67,28 +38,6 @@ class DvsniPerformTest(util.ApacheTest):
|
|||
resp = self.sni.perform()
|
||||
self.assertEqual(len(resp), 0)
|
||||
|
||||
def test_setup_challenge_cert(self):
|
||||
# This is a helper function that can be used for handling
|
||||
# open context managers more elegantly. It avoids dealing with
|
||||
# __enter__ and __exit__ calls.
|
||||
# http://www.voidspace.org.uk/python/mock/helpers.html#mock.mock_open
|
||||
m_open = mock.mock_open()
|
||||
|
||||
response = challenges.DVSNIResponse(s="randomS1")
|
||||
achall = mock.MagicMock(nonce=self.achalls[0].nonce,
|
||||
nonce_domain=self.achalls[0].nonce_domain)
|
||||
achall.gen_cert_and_response.return_value = ("pem", response)
|
||||
|
||||
with mock.patch("letsencrypt_apache.dvsni.open", m_open, create=True):
|
||||
# pylint: disable=protected-access
|
||||
self.assertEqual(response, self.sni._setup_challenge_cert(
|
||||
achall, "randomS1"))
|
||||
|
||||
self.assertTrue(m_open.called)
|
||||
self.assertEqual(
|
||||
m_open.call_args[0], (self.sni.get_cert_file(achall), "w"))
|
||||
self.assertEqual(m_open().write.call_args[0][0], "pem")
|
||||
|
||||
def test_perform1(self):
|
||||
achall = self.achalls[0]
|
||||
self.sni.add_chall(achall)
|
||||
|
|
@ -140,8 +89,9 @@ class DvsniPerformTest(util.ApacheTest):
|
|||
def test_mod_config(self):
|
||||
for achall in self.achalls:
|
||||
self.sni.add_chall(achall)
|
||||
v_addr1 = [obj.Addr(("1.2.3.4", "443")), obj.Addr(("5.6.7.8", "443"))]
|
||||
v_addr2 = [obj.Addr(("127.0.0.1", "443"))]
|
||||
v_addr1 = [common.Addr(("1.2.3.4", "443")),
|
||||
common.Addr(("5.6.7.8", "443"))]
|
||||
v_addr2 = [common.Addr(("127.0.0.1", "443"))]
|
||||
ll_addr = []
|
||||
ll_addr.append(v_addr1)
|
||||
ll_addr.append(v_addr2)
|
||||
|
|
|
|||
|
|
@ -1,63 +1,23 @@
|
|||
"""Test the helper objects in letsencrypt_apache.obj."""
|
||||
"""Tests for letsencrypt_apache.obj."""
|
||||
import unittest
|
||||
|
||||
|
||||
class AddrTest(unittest.TestCase):
|
||||
"""Test the Addr class."""
|
||||
def setUp(self):
|
||||
from letsencrypt_apache.obj import Addr
|
||||
self.addr1 = Addr.fromstring("192.168.1.1")
|
||||
self.addr2 = Addr.fromstring("192.168.1.1:*")
|
||||
self.addr3 = Addr.fromstring("192.168.1.1:80")
|
||||
|
||||
def test_fromstring(self):
|
||||
self.assertEqual(self.addr1.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr1.get_port(), "")
|
||||
self.assertEqual(self.addr2.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr2.get_port(), "*")
|
||||
self.assertEqual(self.addr3.get_addr(), "192.168.1.1")
|
||||
self.assertEqual(self.addr3.get_port(), "80")
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.addr1), "192.168.1.1")
|
||||
self.assertEqual(str(self.addr2), "192.168.1.1:*")
|
||||
self.assertEqual(str(self.addr3), "192.168.1.1:80")
|
||||
|
||||
def test_get_addr_obj(self):
|
||||
self.assertEqual(str(self.addr1.get_addr_obj("443")), "192.168.1.1:443")
|
||||
self.assertEqual(str(self.addr2.get_addr_obj("")), "192.168.1.1")
|
||||
self.assertEqual(str(self.addr1.get_addr_obj("*")), "192.168.1.1:*")
|
||||
|
||||
def test_eq(self):
|
||||
self.assertEqual(self.addr1, self.addr2.get_addr_obj(""))
|
||||
self.assertNotEqual(self.addr1, self.addr2)
|
||||
self.assertFalse(self.addr1 == 3333)
|
||||
|
||||
def test_set_inclusion(self):
|
||||
from letsencrypt_apache.obj import Addr
|
||||
set_a = set([self.addr1, self.addr2])
|
||||
addr1b = Addr.fromstring("192.168.1.1")
|
||||
addr2b = Addr.fromstring("192.168.1.1:*")
|
||||
set_b = set([addr1b, addr2b])
|
||||
|
||||
self.assertEqual(set_a, set_b)
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
|
||||
class VirtualHostTest(unittest.TestCase):
|
||||
"""Test the VirtualHost class."""
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt_apache.obj import VirtualHost
|
||||
from letsencrypt_apache.obj import Addr
|
||||
self.vhost1 = VirtualHost(
|
||||
"filep", "vh_path",
|
||||
set([Addr.fromstring("localhost")]), False, False)
|
||||
set([common.Addr.fromstring("localhost")]), False, False)
|
||||
|
||||
def test_eq(self):
|
||||
from letsencrypt_apache.obj import Addr
|
||||
from letsencrypt_apache.obj import VirtualHost
|
||||
vhost1b = VirtualHost(
|
||||
"filep", "vh_path",
|
||||
set([Addr.fromstring("localhost")]), False, False)
|
||||
set([common.Addr.fromstring("localhost")]), False, False)
|
||||
|
||||
self.assertEqual(vhost1b, self.vhost1)
|
||||
self.assertEqual(str(vhost1b), str(self.vhost1))
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ class ApacheParserTest(util.ApacheTest):
|
|||
mock_path.isfile.return_value = False
|
||||
|
||||
# pylint: disable=protected-access
|
||||
self.assertRaises(errors.LetsEncryptConfiguratorError,
|
||||
self.assertRaises(errors.ConfiguratorError,
|
||||
self.parser._set_locations, self.ssl_options)
|
||||
|
||||
mock_path.isfile.side_effect = [True, False, False]
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
"""Common utilities for letsencrypt_apache."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from letsencrypt import constants as core_constants
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
from letsencrypt_apache import configurator
|
||||
from letsencrypt_apache import constants
|
||||
|
|
@ -19,10 +17,13 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
|||
def setUp(self):
|
||||
super(ApacheTest, self).setUp()
|
||||
|
||||
self.temp_dir, self.config_dir, self.work_dir = dir_setup(
|
||||
"debian_apache_2_4/two_vhost_80")
|
||||
self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(
|
||||
test_dir="debian_apache_2_4/two_vhost_80",
|
||||
pkg="letsencrypt_apache.tests")
|
||||
|
||||
self.ssl_options = setup_ssl_options(self.config_dir)
|
||||
self.ssl_options = common.setup_ssl_options(
|
||||
self.config_dir, constants.MOD_SSL_CONF_SRC,
|
||||
constants.MOD_SSL_CONF_DEST)
|
||||
|
||||
self.config_path = os.path.join(
|
||||
self.temp_dir, "debian_apache_2_4/two_vhost_80/apache2")
|
||||
|
|
@ -33,36 +34,8 @@ class ApacheTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
|||
"acme.jose", "testdata/rsa256_key.pem")
|
||||
|
||||
|
||||
def dir_setup(test_dir="debian_apache_2_4/two_vhost_80",
|
||||
pkg="letsencrypt_apache.tests"):
|
||||
"""Setup the directories necessary for the configurator."""
|
||||
temp_dir = tempfile.mkdtemp("temp")
|
||||
config_dir = tempfile.mkdtemp("config")
|
||||
work_dir = tempfile.mkdtemp("work")
|
||||
|
||||
os.chmod(temp_dir, core_constants.CONFIG_DIRS_MODE)
|
||||
os.chmod(config_dir, core_constants.CONFIG_DIRS_MODE)
|
||||
os.chmod(work_dir, core_constants.CONFIG_DIRS_MODE)
|
||||
|
||||
test_configs = pkg_resources.resource_filename(
|
||||
pkg, os.path.join("testdata", test_dir))
|
||||
|
||||
shutil.copytree(
|
||||
test_configs, os.path.join(temp_dir, test_dir), symlinks=True)
|
||||
|
||||
return temp_dir, config_dir, work_dir
|
||||
|
||||
|
||||
def setup_ssl_options(
|
||||
config_dir, mod_ssl_conf=constants.MOD_SSL_CONF):
|
||||
"""Move the ssl_options into position and return the path."""
|
||||
option_path = os.path.join(config_dir, "options-ssl.conf")
|
||||
shutil.copyfile(mod_ssl_conf, option_path)
|
||||
return option_path
|
||||
|
||||
|
||||
def get_apache_configurator(
|
||||
config_path, config_dir, work_dir, ssl_options, version=(2, 4, 7)):
|
||||
config_path, config_dir, work_dir, version=(2, 4, 7)):
|
||||
"""Create an Apache Configurator with the specified options."""
|
||||
|
||||
backups = os.path.join(work_dir, "backups")
|
||||
|
|
@ -74,8 +47,7 @@ def get_apache_configurator(
|
|||
config = configurator.ApacheConfigurator(
|
||||
config=mock.MagicMock(
|
||||
apache_server_root=config_path,
|
||||
apache_mod_ssl_conf=ssl_options,
|
||||
le_vhost_ext="-le-ssl.conf",
|
||||
apache_le_vhost_ext=constants.CLI_DEFAULTS["le_vhost_ext"],
|
||||
backup_dir=backups,
|
||||
config_dir=config_dir,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
|
|
@ -99,21 +71,21 @@ def get_vh_truth(temp_dir, config_name):
|
|||
obj.VirtualHost(
|
||||
os.path.join(prefix, "encryption-example.conf"),
|
||||
os.path.join(aug_pre, "encryption-example.conf/VirtualHost"),
|
||||
set([obj.Addr.fromstring("*:80")]),
|
||||
set([common.Addr.fromstring("*:80")]),
|
||||
False, True, set(["encryption-example.demo"])),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "default-ssl.conf"),
|
||||
os.path.join(aug_pre, "default-ssl.conf/IfModule/VirtualHost"),
|
||||
set([obj.Addr.fromstring("_default_:443")]), True, False),
|
||||
set([common.Addr.fromstring("_default_:443")]), True, False),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "000-default.conf"),
|
||||
os.path.join(aug_pre, "000-default.conf/VirtualHost"),
|
||||
set([obj.Addr.fromstring("*:80")]), False, True,
|
||||
set([common.Addr.fromstring("*:80")]), False, True,
|
||||
set(["ip-172-30-0-17"])),
|
||||
obj.VirtualHost(
|
||||
os.path.join(prefix, "letsencrypt.conf"),
|
||||
os.path.join(aug_pre, "letsencrypt.conf/VirtualHost"),
|
||||
set([obj.Addr.fromstring("*:80")]), False, True,
|
||||
set([common.Addr.fromstring("*:80")]), False, True,
|
||||
set(["letsencrypt.demo"])),
|
||||
]
|
||||
return vh_truth
|
||||
|
|
|
|||
|
|
@ -56,8 +56,6 @@ class NginxConfigurator(common.Plugin):
|
|||
def add_parser_arguments(cls, add):
|
||||
add("server-root", default=constants.CLI_DEFAULTS["server_root"],
|
||||
help="Nginx server root directory.")
|
||||
add("mod-ssl-conf", default=constants.CLI_DEFAULTS["mod_ssl_conf"],
|
||||
help="Contains standard nginx SSL directives.")
|
||||
add("ctl", default=constants.CLI_DEFAULTS["ctl"], help="Path to the "
|
||||
"'nginx' binary, used for 'configtest' and retrieving nginx "
|
||||
"version number.")
|
||||
|
|
@ -91,18 +89,22 @@ class NginxConfigurator(common.Plugin):
|
|||
self.reverter = reverter.Reverter(self.config)
|
||||
self.reverter.recovery_routine()
|
||||
|
||||
@property
|
||||
def mod_ssl_conf(self):
|
||||
"""Full absolute path to SSL configuration file."""
|
||||
return os.path.join(self.config.config_dir, constants.MOD_SSL_CONF_DEST)
|
||||
|
||||
# This is called in determine_authenticator and determine_installer
|
||||
def prepare(self):
|
||||
"""Prepare the authenticator/installer."""
|
||||
self.parser = parser.NginxParser(
|
||||
self.conf('server-root'),
|
||||
self.conf('mod-ssl-conf'))
|
||||
self.conf('server-root'), self.mod_ssl_conf)
|
||||
|
||||
# Set Version
|
||||
if self.version is None:
|
||||
self.version = self.get_version()
|
||||
|
||||
temp_install(self.conf('mod-ssl-conf'))
|
||||
temp_install(self.mod_ssl_conf)
|
||||
|
||||
# Entry point in main.py for installing cert
|
||||
def deploy_cert(self, domain, cert_path, key_path, chain_path=None):
|
||||
|
|
@ -128,11 +130,10 @@ class NginxConfigurator(common.Plugin):
|
|||
directives, True)
|
||||
logging.info("Deployed Certificate to VirtualHost %s for %s",
|
||||
vhost.filep, vhost.names)
|
||||
except errors.LetsEncryptMisconfigurationError:
|
||||
except errors.MisconfigurationError:
|
||||
logging.warn(
|
||||
"Cannot find a cert or key directive in %s for %s",
|
||||
vhost.filep, vhost.names)
|
||||
logging.warn("VirtualHost was not modified")
|
||||
"Cannot find a cert or key directive in %s for %s. "
|
||||
"VirtualHost was not modified.", vhost.filep, vhost.names)
|
||||
# Presumably break here so that the virtualhost is not modified
|
||||
return False
|
||||
|
||||
|
|
@ -316,9 +317,9 @@ class NginxConfigurator(common.Plugin):
|
|||
return self._enhance_func[enhancement](
|
||||
self.choose_vhost(domain), options)
|
||||
except (KeyError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
raise errors.ConfiguratorError(
|
||||
"Unsupported enhancement: {0}".format(enhancement))
|
||||
except errors.LetsEncryptConfiguratorError:
|
||||
except errors.ConfiguratorError:
|
||||
logging.warn("Failed %s for %s", enhancement, domain)
|
||||
|
||||
######################################
|
||||
|
|
@ -352,9 +353,7 @@ class NginxConfigurator(common.Plugin):
|
|||
|
||||
if proc.returncode != 0:
|
||||
# Enter recovery routine...
|
||||
logging.error("Config test failed")
|
||||
logging.error(stdout)
|
||||
logging.error(stderr)
|
||||
logging.error("Config test failed\n%s\n%s", stdout, stderr)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
|
@ -383,7 +382,7 @@ class NginxConfigurator(common.Plugin):
|
|||
:returns: version
|
||||
:rtype: tuple
|
||||
|
||||
:raises errors.LetsEncryptConfiguratorError:
|
||||
:raises .ConfiguratorError:
|
||||
Unable to find Nginx version or version is unsupported
|
||||
|
||||
"""
|
||||
|
|
@ -394,7 +393,7 @@ class NginxConfigurator(common.Plugin):
|
|||
stderr=subprocess.PIPE)
|
||||
text = proc.communicate()[1] # nginx prints output to stderr
|
||||
except (OSError, ValueError):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
raise errors.ConfiguratorError(
|
||||
"Unable to run %s -V" % self.conf('ctl'))
|
||||
|
||||
version_regex = re.compile(r"nginx/([0-9\.]*)", re.IGNORECASE)
|
||||
|
|
@ -407,22 +406,19 @@ class NginxConfigurator(common.Plugin):
|
|||
ssl_matches = ssl_regex.findall(text)
|
||||
|
||||
if not version_matches:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Unable to find Nginx version")
|
||||
raise errors.ConfiguratorError("Unable to find Nginx version")
|
||||
if not ssl_matches:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
raise errors.ConfiguratorError(
|
||||
"Nginx build is missing SSL module (--with-http_ssl_module).")
|
||||
if not sni_matches:
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Nginx build doesn't support SNI")
|
||||
raise errors.ConfiguratorError("Nginx build doesn't support SNI")
|
||||
|
||||
nginx_version = tuple([int(i) for i in version_matches[0].split(".")])
|
||||
|
||||
# nginx < 0.8.48 uses machine hostname as default server_name instead of
|
||||
# the empty string
|
||||
if nginx_version < (0, 8, 48):
|
||||
raise errors.LetsEncryptConfiguratorError(
|
||||
"Nginx version must be 0.8.48+")
|
||||
raise errors.ConfiguratorError("Nginx version must be 0.8.48+")
|
||||
|
||||
return nginx_version
|
||||
|
||||
|
|
@ -570,14 +566,11 @@ def nginx_restart(nginx_ctl):
|
|||
|
||||
if nginx_proc.returncode != 0:
|
||||
# Enter recovery routine...
|
||||
logging.error("Nginx Restart Failed!")
|
||||
logging.error(stdout)
|
||||
logging.error(stderr)
|
||||
logging.error("Nginx Restart Failed!\n%s\n%s", stdout, stderr)
|
||||
return False
|
||||
|
||||
except (OSError, ValueError):
|
||||
logging.fatal(
|
||||
"Nginx Restart Failed - Please Check the Configuration")
|
||||
logging.fatal("Nginx Restart Failed - Please Check the Configuration")
|
||||
sys.exit(1)
|
||||
|
||||
return True
|
||||
|
|
@ -592,4 +585,4 @@ def temp_install(options_ssl):
|
|||
|
||||
# Check to make sure options-ssl.conf is installed
|
||||
if not os.path.isfile(options_ssl):
|
||||
shutil.copyfile(constants.MOD_SSL_CONF, options_ssl)
|
||||
shutil.copyfile(constants.MOD_SSL_CONF_SRC, options_ssl)
|
||||
|
|
|
|||
|
|
@ -4,13 +4,15 @@ import pkg_resources
|
|||
|
||||
CLI_DEFAULTS = dict(
|
||||
server_root="/etc/nginx",
|
||||
mod_ssl_conf="/etc/letsencrypt/options-ssl-nginx.conf",
|
||||
ctl="nginx",
|
||||
)
|
||||
"""CLI defaults."""
|
||||
|
||||
|
||||
MOD_SSL_CONF = pkg_resources.resource_filename(
|
||||
MOD_SSL_CONF_DEST = "options-ssl-nginx.conf"
|
||||
"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
|
||||
|
||||
MOD_SSL_CONF_SRC = pkg_resources.resource_filename(
|
||||
"letsencrypt_nginx", "options-ssl-nginx.conf")
|
||||
"""Path to the Nginx mod_ssl config file found in the Let's Encrypt
|
||||
"""Path to the nginx mod_ssl config file found in the Let's Encrypt
|
||||
distribution."""
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue