mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 00:02:14 -04:00
Merge branch '440-no-cli' into 473-no-cli
Conflicts:
letsencrypt/cli.py
letsencrypt/renewer.py
This commit is contained in:
commit
e82f605c22
93 changed files with 2260 additions and 2846 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
15
README.rst
15
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
|
||||
==============================
|
||||
|
||||
|
|
@ -47,6 +57,9 @@ server automatically!::
|
|||
:target: https://quay.io/repository/letsencrypt/lets-encrypt-preview
|
||||
: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,7 +98,7 @@ Current Features
|
|||
Links
|
||||
-----
|
||||
|
||||
Documentation: https://letsencrypt.readthedocs.org/
|
||||
Documentation: https://letsencrypt.readthedocs.org
|
||||
|
||||
Software project: https://github.com/letsencrypt/lets-encrypt-preview
|
||||
|
||||
|
|
|
|||
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]
|
||||
|
|
|
|||
|
|
@ -42,31 +42,54 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields):
|
|||
|
||||
|
||||
@Challenge.register
|
||||
class SimpleHTTPS(DVChallenge):
|
||||
"""ACME "simpleHttps" challenge."""
|
||||
typ = "simpleHttps"
|
||||
class SimpleHTTP(DVChallenge):
|
||||
"""ACME "simpleHttp" challenge."""
|
||||
typ = "simpleHttp"
|
||||
token = jose.Field("token")
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
class SimpleHTTPSResponse(ChallengeResponse):
|
||||
"""ACME "simpleHttps" challenge response."""
|
||||
typ = "simpleHttps"
|
||||
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:`~SimpleHTTPS.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,14 +18,21 @@ KEY = jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
|||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))))
|
||||
|
||||
|
||||
class SimpleHTTPSTest(unittest.TestCase):
|
||||
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):
|
||||
from acme.challenges import SimpleHTTPS
|
||||
self.msg = SimpleHTTPS(
|
||||
from acme.challenges import SimpleHTTP
|
||||
self.msg = SimpleHTTP(
|
||||
token='evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')
|
||||
self.jmsg = {
|
||||
'type': 'simpleHttps',
|
||||
'type': 'simpleHttp',
|
||||
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA',
|
||||
}
|
||||
|
||||
|
|
@ -33,39 +40,63 @@ class SimpleHTTPSTest(unittest.TestCase):
|
|||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import SimpleHTTPS
|
||||
self.assertEqual(self.msg, SimpleHTTPS.from_json(self.jmsg))
|
||||
from acme.challenges import SimpleHTTP
|
||||
self.assertEqual(self.msg, SimpleHTTP.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import SimpleHTTPS
|
||||
hash(SimpleHTTPS.from_json(self.jmsg))
|
||||
from acme.challenges import SimpleHTTP
|
||||
hash(SimpleHTTP.from_json(self.jmsg))
|
||||
|
||||
|
||||
class SimpleHTTPSResponseTest(unittest.TestCase):
|
||||
class SimpleHTTPResponseTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import SimpleHTTPSResponse
|
||||
self.msg = SimpleHTTPSResponse(path='6tbIMBC5Anhl5bOlWT5ZFA')
|
||||
self.jmsg = {
|
||||
'type': 'simpleHttps',
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
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 SimpleHTTPSResponse
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
self.assertEqual(
|
||||
self.msg, SimpleHTTPSResponse.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 SimpleHTTPSResponse
|
||||
hash(SimpleHTTPSResponse.from_json(self.jmsg))
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
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,22 +9,22 @@ import M2Crypto
|
|||
import requests
|
||||
import werkzeug
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
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)
|
||||
|
|
@ -32,27 +32,31 @@ class Network(object):
|
|||
:ivar bool verify_ssl: Verify SSL certificates?
|
||||
|
||||
"""
|
||||
|
||||
DER_CONTENT_TYPE = 'application/pkix-cert'
|
||||
JSON_CONTENT_TYPE = 'application/json'
|
||||
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
|
||||
REPLAY_NONCE_HEADER = 'Replay-Nonce'
|
||||
|
||||
def __init__(self, new_reg_uri, key, alg=jose.RS256, verify_ssl=True):
|
||||
self.new_reg_uri = new_reg_uri
|
||||
self.key = key
|
||||
self.alg = alg
|
||||
self.verify_ssl = verify_ssl
|
||||
self._nonces = set()
|
||||
|
||||
def _wrap_in_jws(self, obj):
|
||||
def _wrap_in_jws(self, obj, nonce):
|
||||
"""Wrap `JSONDeSerializable` object in JWS.
|
||||
|
||||
.. todo:: Implement ``acmePath``.
|
||||
|
||||
:param JSONDeSerializable obj:
|
||||
:rtype: `.JWS`
|
||||
|
||||
"""
|
||||
dumps = obj.json_dumps()
|
||||
logging.debug('Serialized JSON: %s', dumps)
|
||||
return jose.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg).json_dumps()
|
||||
return jws.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps()
|
||||
|
||||
@classmethod
|
||||
def _check_response(cls, response, content_type=None):
|
||||
|
|
@ -68,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
|
||||
|
|
@ -89,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(
|
||||
|
|
@ -105,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`
|
||||
|
|
@ -122,29 +126,52 @@ 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 _post(self, uri, data, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
def _add_nonce(self, response):
|
||||
if self.REPLAY_NONCE_HEADER in response.headers:
|
||||
nonce = response.headers[self.REPLAY_NONCE_HEADER]
|
||||
error = jws.Header.validate_nonce(nonce)
|
||||
if error is None:
|
||||
logging.debug('Storing nonce: %r', nonce)
|
||||
self._nonces.add(nonce)
|
||||
else:
|
||||
raise errors.ClientError('Invalid nonce ({0}): {1}'.format(
|
||||
nonce, error))
|
||||
else:
|
||||
raise errors.ClientError(
|
||||
'Server {0} response did not include a replay nonce'.format(
|
||||
response.request.method))
|
||||
|
||||
def _get_nonce(self, uri):
|
||||
if not self._nonces:
|
||||
logging.debug('Requesting fresh nonce by sending HEAD to %s', uri)
|
||||
self._add_nonce(requests.head(uri))
|
||||
return self._nonces.pop()
|
||||
|
||||
def _post(self, uri, obj, content_type=JSON_CONTENT_TYPE, **kwargs):
|
||||
"""Send POST data.
|
||||
|
||||
: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`
|
||||
|
||||
"""
|
||||
data = self._wrap_in_jws(obj, self._get_nonce(uri))
|
||||
logging.debug('Sending POST data to %s: %s', uri, data)
|
||||
kwargs.setdefault('verify', self.verify_ssl)
|
||||
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)
|
||||
return response
|
||||
|
||||
|
|
@ -159,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.
|
||||
|
||||
|
|
@ -177,12 +204,12 @@ 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, self._wrap_in_jws(new_reg))
|
||||
response = self._post(self.new_reg_uri, new_reg)
|
||||
assert response.status_code == httplib.CREATED # TODO: handle errors
|
||||
|
||||
regr = self._regr_from_response(response)
|
||||
|
|
@ -191,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.
|
||||
|
||||
|
|
@ -219,7 +228,7 @@ class Network(object):
|
|||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
response = self._post(regr.uri, self._wrap_in_jws(regr.body))
|
||||
response = self._post(regr.uri, regr.body)
|
||||
|
||||
# TODO: Boulder returns httplib.ACCEPTED
|
||||
#assert response.status_code == httplib.OK
|
||||
|
|
@ -231,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
|
||||
|
||||
|
|
@ -257,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:
|
||||
|
|
@ -271,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
|
||||
|
||||
|
|
@ -279,8 +287,8 @@ class Network(object):
|
|||
:rtype: `.AuthorizationResource`
|
||||
|
||||
"""
|
||||
new_authz = messages2.Authorization(identifier=identifier)
|
||||
response = self._post(new_authzr_uri, self._wrap_in_jws(new_authz))
|
||||
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)
|
||||
|
||||
|
|
@ -298,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.
|
||||
|
|
@ -316,14 +324,14 @@ class Network(object):
|
|||
:raises errors.UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
response = self._post(challb.uri, self._wrap_in_jws(response))
|
||||
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)
|
||||
|
|
@ -382,20 +390,20 @@ 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
|
||||
response = self._post(
|
||||
authzrs[0].new_cert_uri, # TODO: acme-spec #90
|
||||
self._wrap_in_jws(req),
|
||||
req,
|
||||
content_type=content_type,
|
||||
headers={'Accept': content_type})
|
||||
|
||||
|
|
@ -404,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)))
|
||||
|
|
@ -429,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
|
||||
|
|
@ -458,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))
|
||||
|
|
@ -496,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)
|
||||
|
|
@ -531,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, self._wrap_in_jws(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,96 +1,87 @@
|
|||
"""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 messages2
|
||||
|
||||
from letsencrypt import account
|
||||
from letsencrypt import errors
|
||||
from acme import jws as acme_jws
|
||||
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
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.network2 import Network
|
||||
self.verify_ssl = mock.MagicMock()
|
||||
self.net = Network(
|
||||
self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped)
|
||||
|
||||
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')
|
||||
self.net._nonces.add(self.nonce) # pylint: disable=protected-access
|
||||
|
||||
self.response = mock.MagicMock(ok=True, status_code=httplib.OK)
|
||||
self.response.headers = {}
|
||||
self.response.links = {}
|
||||
|
||||
self.identifier = messages2.Identifier(
|
||||
typ=messages2.IDENTIFIER_FQDN, value='example.com')
|
||||
self.post = mock.MagicMock(return_value=self.response)
|
||||
self.get = mock.MagicMock(return_value=self.response)
|
||||
|
||||
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 = mock.MagicMock(return_value=self.response)
|
||||
self.net._get = mock.MagicMock(return_value=self.response)
|
||||
self.net._post = self.post
|
||||
self.net._get = self.get
|
||||
|
||||
def test_init(self):
|
||||
self.assertTrue(self.net.verify_ssl is self.verify_ssl)
|
||||
|
|
@ -106,30 +97,34 @@ class NetworkTest(unittest.TestCase):
|
|||
def from_json(cls, value):
|
||||
pass # pragma: no cover
|
||||
# pylint: disable=protected-access
|
||||
jws = self.net._wrap_in_jws(MockJSONDeSerializable('foo'))
|
||||
self.assertEqual(jose.JWS.json_loads(jws).payload, '"foo"')
|
||||
jws_dump = self.net._wrap_in_jws(
|
||||
MockJSONDeSerializable('foo'), nonce='Tg')
|
||||
jws = acme_jws.JWS.json_loads(jws_dump)
|
||||
self.assertEqual(jws.payload, '"foo"')
|
||||
self.assertEqual(jws.signature.combined.nonce, 'Tg')
|
||||
# TODO: check that nonce is in protected header
|
||||
|
||||
def test_check_response_not_ok_jobj_no_error(self):
|
||||
self.response.ok = False
|
||||
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
|
||||
|
|
@ -137,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):
|
||||
|
|
@ -154,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()
|
||||
|
|
@ -169,33 +164,73 @@ class NetworkTest(unittest.TestCase):
|
|||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.get('uri'), content_type='ct')
|
||||
|
||||
@mock.patch('letsencrypt.network2.requests')
|
||||
def _mock_wrap_in_jws(self):
|
||||
# pylint: disable=protected-access
|
||||
self.net._wrap_in_jws = self.wrap_in_jws
|
||||
|
||||
@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.assertRaises(errors.NetworkError, self.net._post, 'uri', 'data')
|
||||
self._mock_wrap_in_jws()
|
||||
self.assertRaises(
|
||||
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()
|
||||
self.net._post('uri', 'data', content_type='ct')
|
||||
self._mock_wrap_in_jws()
|
||||
requests_mock.post().headers = {
|
||||
self.net.REPLAY_NONCE_HEADER: self.nonce}
|
||||
self.net._post('uri', mock.sentinel.obj, content_type='ct')
|
||||
self.net._check_response.assert_called_once_with(
|
||||
requests_mock.post('uri', 'data'), content_type='ct')
|
||||
requests_mock.post('uri', mock.sentinel.wrapped), content_type='ct')
|
||||
|
||||
@mock.patch('letsencrypt.client.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()
|
||||
self._mock_wrap_in_jws()
|
||||
|
||||
self.net._nonces.clear()
|
||||
self.assertRaises(
|
||||
errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
|
||||
nonce2 = jose.b64encode('Nonce2')
|
||||
requests_mock.head('uri').headers = {
|
||||
self.net.REPLAY_NONCE_HEADER: nonce2}
|
||||
requests_mock.post('uri').headers = {
|
||||
self.net.REPLAY_NONCE_HEADER: self.nonce}
|
||||
|
||||
self.net._post('uri', mock.sentinel.obj)
|
||||
|
||||
requests_mock.head.assert_called_with('uri')
|
||||
self.wrap_in_jws.assert_called_once_with(mock.sentinel.obj, nonce2)
|
||||
self.assertEqual(self.net._nonces, set([self.nonce]))
|
||||
|
||||
# wrong nonce
|
||||
requests_mock.post('uri').headers = {self.net.REPLAY_NONCE_HEADER: 'F'}
|
||||
self.assertRaises(
|
||||
errors.ClientError, self.net._post, 'uri', mock.sentinel.obj)
|
||||
|
||||
@mock.patch('acme.client.requests')
|
||||
def test_get_post_verify_ssl(self, requests_mock):
|
||||
# pylint: disable=protected-access
|
||||
self._mock_wrap_in_jws()
|
||||
self.net._check_response = mock.MagicMock()
|
||||
|
||||
for verify_ssl in [True, False]:
|
||||
self.net.verify_ssl = verify_ssl
|
||||
self.net._get('uri')
|
||||
self.net._post('uri', 'data')
|
||||
self.net._nonces.add('N')
|
||||
requests_mock.post().headers = {
|
||||
self.net.REPLAY_NONCE_HEADER: self.nonce}
|
||||
self.net._post('uri', mock.sentinel.obj)
|
||||
requests_mock.get.assert_called_once_with('uri', verify=verify_ssl)
|
||||
requests_mock.post.assert_called_once_with(
|
||||
'uri', data='data', verify=verify_ssl)
|
||||
requests_mock.post.assert_called_with(
|
||||
'uri', data=mock.sentinel.wrapped, verify=verify_ssl)
|
||||
requests_mock.reset_mock()
|
||||
|
||||
def test_register(self):
|
||||
|
|
@ -221,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
|
||||
|
|
@ -286,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):
|
||||
|
|
@ -310,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):
|
||||
|
|
@ -319,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
|
||||
|
|
@ -329,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
|
||||
|
|
@ -339,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
|
||||
|
|
@ -360,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))
|
||||
|
|
@ -409,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
|
||||
|
|
@ -464,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'
|
||||
|
|
@ -475,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()
|
||||
|
|
@ -497,14 +509,14 @@ class NetworkTest(unittest.TestCase):
|
|||
|
||||
def test_revoke(self):
|
||||
self._mock_post_get()
|
||||
self.net.revoke(self.certr, when=messages2.Revocation.NOW)
|
||||
# pylint: disable=protected-access
|
||||
self.net._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."""
|
||||
|
|
|
|||
|
|
@ -66,7 +66,11 @@ from acme.jose.jwk import (
|
|||
JWKRSA,
|
||||
)
|
||||
|
||||
from acme.jose.jws import JWS
|
||||
from acme.jose.jws import (
|
||||
Header,
|
||||
JWS,
|
||||
Signature,
|
||||
)
|
||||
|
||||
from acme.jose.util import (
|
||||
ComparableX509,
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ class Field(object):
|
|||
definition of being empty, e.g. for some more exotic data types.
|
||||
|
||||
"""
|
||||
return not value
|
||||
return not isinstance(value, bool) and not value
|
||||
|
||||
def omit(self, value):
|
||||
"""Omit the value in output?"""
|
||||
|
|
@ -129,7 +129,8 @@ class JSONObjectWithFieldsMeta(abc.ABCMeta):
|
|||
keys are field attribute names and values are fields themselves.
|
||||
|
||||
2. ``cls.__slots__`` is extended by all field attribute names
|
||||
(i.e. not :attr:`Field.json_name`).
|
||||
(i.e. not :attr:`Field.json_name`). Original ``cls.__slots__``
|
||||
are stored in ``cls._orig_slots``.
|
||||
|
||||
In a consequence, for a field attribute name ``some_field``,
|
||||
``cls.some_field`` will be a slot descriptor and not an instance
|
||||
|
|
@ -143,6 +144,7 @@ class JSONObjectWithFieldsMeta(abc.ABCMeta):
|
|||
some_field = some_field
|
||||
|
||||
assert Foo.__slots__ == ('some_field', 'baz')
|
||||
assert Foo._orig_slots == ()
|
||||
assert Foo.some_field is not Field
|
||||
|
||||
assert Foo._fields.keys() == ['some_field']
|
||||
|
|
@ -158,12 +160,16 @@ class JSONObjectWithFieldsMeta(abc.ABCMeta):
|
|||
|
||||
def __new__(mcs, name, bases, dikt):
|
||||
fields = {}
|
||||
|
||||
for base in bases:
|
||||
fields.update(getattr(base, '_fields', {}))
|
||||
# Do not reorder, this class might override fields from base classes!
|
||||
for key, value in dikt.items(): # not iterkeys() (in-place edit!)
|
||||
if isinstance(value, Field):
|
||||
fields[key] = dikt.pop(key)
|
||||
|
||||
dikt['__slots__'] = tuple(
|
||||
list(dikt.get('__slots__', ())) + fields.keys())
|
||||
dikt['_orig_slots'] = dikt.get('__slots__', ())
|
||||
dikt['__slots__'] = tuple(list(dikt['_orig_slots']) + fields.keys())
|
||||
dikt['_fields'] = fields
|
||||
|
||||
return abc.ABCMeta.__new__(mcs, name, bases, dikt)
|
||||
|
|
@ -212,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)
|
||||
|
|
@ -224,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):
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
"""Tests for acme.jose.json_util."""
|
||||
import itertools
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
|
@ -20,6 +21,13 @@ CSR = M2Crypto.X509.load_request(pkg_resources.resource_filename(
|
|||
class FieldTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.json_util.Field."""
|
||||
|
||||
def test_no_omit_boolean(self):
|
||||
from acme.jose.json_util import Field
|
||||
for default, omitempty, value in itertools.product(
|
||||
[True, False], [True, False], [True, False]):
|
||||
self.assertFalse(
|
||||
Field("foo", default=default, omitempty=omitempty).omit(value))
|
||||
|
||||
def test_descriptors(self):
|
||||
mock_value = mock.MagicMock()
|
||||
|
||||
|
|
@ -77,6 +85,47 @@ class FieldTest(unittest.TestCase):
|
|||
self.assertTrue(Field.default_decoder(mock_value) is mock_value)
|
||||
|
||||
|
||||
class JSONObjectWithFieldsMetaTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.json_util.JSONObjectWithFieldsMeta."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.json_util import Field
|
||||
from acme.jose.json_util import JSONObjectWithFieldsMeta
|
||||
self.field = Field('Baz')
|
||||
self.field2 = Field('Baz2')
|
||||
# pylint: disable=invalid-name,missing-docstring,too-few-public-methods
|
||||
# pylint: disable=blacklisted-name
|
||||
class A(object):
|
||||
__metaclass__ = JSONObjectWithFieldsMeta
|
||||
__slots__ = ('bar',)
|
||||
baz = self.field
|
||||
class B(A):
|
||||
pass
|
||||
class C(A):
|
||||
baz = self.field2
|
||||
self.a_cls = A
|
||||
self.b_cls = B
|
||||
self.c_cls = C
|
||||
|
||||
def test_fields(self):
|
||||
# pylint: disable=protected-access,no-member
|
||||
self.assertEqual({'baz': self.field}, self.a_cls._fields)
|
||||
self.assertEqual({'baz': self.field}, self.b_cls._fields)
|
||||
|
||||
def test_fields_inheritance(self):
|
||||
# pylint: disable=protected-access,no-member
|
||||
self.assertEqual({'baz': self.field2}, self.c_cls._fields)
|
||||
|
||||
def test_slots(self):
|
||||
self.assertEqual(('bar', 'baz'), self.a_cls.__slots__)
|
||||
self.assertEqual(('baz',), self.b_cls.__slots__)
|
||||
|
||||
def test_orig_slots(self):
|
||||
# pylint: disable=protected-access,no-member
|
||||
self.assertEqual(('bar',), self.a_cls._orig_slots)
|
||||
self.assertEqual((), self.b_cls._orig_slots)
|
||||
|
||||
|
||||
class JSONObjectWithFieldsTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.json_util.JSONObjectWithFields."""
|
||||
# pylint: disable=protected-access
|
||||
|
|
|
|||
|
|
@ -247,6 +247,8 @@ class JWS(json_util.JSONObjectWithFields):
|
|||
"""
|
||||
__slots__ = ('payload', 'signatures')
|
||||
|
||||
signature_cls = Signature
|
||||
|
||||
def verify(self, key=None):
|
||||
"""Verify."""
|
||||
return all(sig.verify(self.payload, key) for sig in self.signatures)
|
||||
|
|
@ -255,13 +257,13 @@ class JWS(json_util.JSONObjectWithFields):
|
|||
def sign(cls, payload, **kwargs):
|
||||
"""Sign."""
|
||||
return cls(payload=payload, signatures=(
|
||||
Signature.sign(payload=payload, **kwargs),))
|
||||
cls.signature_cls.sign(payload=payload, **kwargs),))
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
"""Get a singleton signature.
|
||||
|
||||
:rtype: :class:`Signature`
|
||||
:rtype: `signature_cls`
|
||||
|
||||
"""
|
||||
assert len(self.signatures) == 1
|
||||
|
|
@ -288,8 +290,8 @@ class JWS(json_util.JSONObjectWithFields):
|
|||
raise errors.DeserializationError(
|
||||
'Compact JWS serialization should comprise of exactly'
|
||||
' 3 dot-separated components')
|
||||
sig = Signature(protected=json_util.decode_b64jose(protected),
|
||||
signature=json_util.decode_b64jose(signature))
|
||||
sig = cls.signature_cls(protected=json_util.decode_b64jose(protected),
|
||||
signature=json_util.decode_b64jose(signature))
|
||||
return cls(payload=json_util.decode_b64jose(payload), signatures=(sig,))
|
||||
|
||||
def to_partial_json(self, flat=True): # pylint: disable=arguments-differ
|
||||
|
|
@ -312,10 +314,10 @@ class JWS(json_util.JSONObjectWithFields):
|
|||
raise errors.DeserializationError('Flat mixed with non-flat')
|
||||
elif 'signature' in jobj: # flat
|
||||
return cls(payload=json_util.decode_b64jose(jobj.pop('payload')),
|
||||
signatures=(Signature.from_json(jobj),))
|
||||
signatures=(cls.signature_cls.from_json(jobj),))
|
||||
else:
|
||||
return cls(payload=json_util.decode_b64jose(jobj['payload']),
|
||||
signatures=tuple(Signature.from_json(sig)
|
||||
signatures=tuple(cls.signature_cls.from_json(sig)
|
||||
for sig in jobj['signatures']))
|
||||
|
||||
class CLI(object):
|
||||
|
|
|
|||
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-----
|
||||
59
acme/jws.py
Normal file
59
acme/jws.py
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
"""ACME JOSE JWS."""
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
|
||||
|
||||
class Header(jose.Header):
|
||||
"""ACME JOSE Header.
|
||||
|
||||
.. todo:: Implement ``acmePath``.
|
||||
|
||||
"""
|
||||
nonce = jose.Field('nonce', omitempty=True)
|
||||
|
||||
@classmethod
|
||||
def validate_nonce(cls, nonce):
|
||||
"""Validate nonce.
|
||||
|
||||
:returns: ``None`` if ``nonce`` is valid, decoding errors otherwise.
|
||||
|
||||
"""
|
||||
try:
|
||||
jose.b64decode(nonce)
|
||||
except (ValueError, TypeError) as error:
|
||||
return error
|
||||
else:
|
||||
return None
|
||||
|
||||
@nonce.decoder
|
||||
def nonce(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
error = Header.validate_nonce(value)
|
||||
if error is not None:
|
||||
# TODO: custom error
|
||||
raise errors.Error("Invalid nonce: {0}".format(error))
|
||||
return value
|
||||
|
||||
|
||||
class Signature(jose.Signature):
|
||||
"""ACME Signature."""
|
||||
__slots__ = jose.Signature._orig_slots # pylint: disable=no-member
|
||||
|
||||
# TODO: decoder/encoder should accept cls? Otherwise, subclassing
|
||||
# JSONObjectWithFields is tricky...
|
||||
header_cls = Header
|
||||
header = jose.Field(
|
||||
'header', omitempty=True, default=header_cls(),
|
||||
decoder=header_cls.from_json)
|
||||
|
||||
# TODO: decoder should check that nonce is in the protected header
|
||||
|
||||
|
||||
class JWS(jose.JWS):
|
||||
"""ACME JWS."""
|
||||
signature_cls = Signature
|
||||
__slots__ = jose.JWS._orig_slots # pylint: disable=no-member
|
||||
|
||||
@classmethod
|
||||
def sign(cls, payload, key, alg, nonce): # pylint: disable=arguments-differ
|
||||
return super(JWS, cls).sign(payload, key=key, alg=alg,
|
||||
protect=frozenset(['nonce']), nonce=nonce)
|
||||
58
acme/jws_test.py
Normal file
58
acme/jws_test.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
"""Tests for acme.jws."""
|
||||
import os
|
||||
import pkg_resources
|
||||
import unittest
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
|
||||
|
||||
RSA512_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
|
||||
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))
|
||||
|
||||
|
||||
class HeaderTest(unittest.TestCase):
|
||||
"""Tests for acme.jws.Header."""
|
||||
|
||||
good_nonce = jose.b64encode('foo')
|
||||
wrong_nonce = 'F'
|
||||
# Following just makes sure wrong_nonce is wrong
|
||||
try:
|
||||
jose.b64decode(wrong_nonce)
|
||||
except (ValueError, TypeError):
|
||||
assert True
|
||||
else:
|
||||
assert False # pragma: no cover
|
||||
|
||||
def test_validate_nonce(self):
|
||||
from acme.jws import Header
|
||||
self.assertTrue(Header.validate_nonce(self.good_nonce) is None)
|
||||
self.assertFalse(Header.validate_nonce(self.wrong_nonce) is None)
|
||||
|
||||
def test_nonce_decoder(self):
|
||||
from acme.jws import Header
|
||||
nonce_field = Header._fields['nonce']
|
||||
|
||||
self.assertRaises(errors.Error, nonce_field.decode, self.wrong_nonce)
|
||||
self.assertEqual(self.good_nonce, nonce_field.decode(self.good_nonce))
|
||||
|
||||
|
||||
class JWSTest(unittest.TestCase):
|
||||
"""Tests for acme.jws.JWS."""
|
||||
|
||||
def setUp(self):
|
||||
self.privkey = jose.JWKRSA(key=RSA512_KEY)
|
||||
self.pubkey = self.privkey.public()
|
||||
self.nonce = jose.b64encode('Nonce')
|
||||
|
||||
def test_it(self):
|
||||
from acme.jws import JWS
|
||||
jws = JWS.sign(payload='foo', key=self.privkey,
|
||||
alg=jose.RS256, nonce=self.nonce)
|
||||
JWS.from_json(jws.to_json())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
625
acme/messages.py
625
acme/messages.py
|
|
@ -1,106 +1,287 @@
|
|||
"""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 = {
|
||||
'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, 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)
|
||||
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 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 +290,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,297 +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)',
|
||||
}
|
||||
|
||||
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.SimpleHTTPS(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,336 @@ 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.SimpleHTTPS(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.SimpleHTTPS(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.SimpleHTTPSResponse(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):
|
||||
from acme.messages import Error
|
||||
self.assertEqual(Error.from_json(self.jmsg), self.msg)
|
||||
self.assertEqual(self.const_a, self.MockConstant.from_json('a'))
|
||||
self.assertRaises(
|
||||
jose.DeserializationError, self.MockConstant.from_json, 'c')
|
||||
|
||||
def test_json_without_optionals(self):
|
||||
del self.jmsg['message']
|
||||
del self.jmsg['moreInfo']
|
||||
def test_from_json_hashable(self):
|
||||
hash(self.MockConstant.from_json('a'))
|
||||
|
||||
from acme.messages import Error
|
||||
msg = Error.from_json(self.jmsg)
|
||||
def test_repr(self):
|
||||
self.assertEqual('MockConstant(a)', repr(self.const_a))
|
||||
self.assertEqual('MockConstant(b)', repr(self.const_b))
|
||||
|
||||
self.assertTrue(msg.message is None)
|
||||
self.assertTrue(msg.more_info is None)
|
||||
self.assertEqual(self.jmsg, msg.to_partial_json())
|
||||
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 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.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" : [ "simpleHttps" ]
|
||||
},
|
||||
"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" : [ "simpleHttps" ]
|
||||
},
|
||||
"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://*
|
||||
|
|
|
|||
|
|
@ -45,5 +45,15 @@ fi
|
|||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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,9 +5,9 @@ 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
|
||||
|
||||
|
|
@ -16,9 +16,31 @@ download docker, and issue the following command
|
|||
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
|
||||
quay.io/letsencrypt/lets-encrypt-preview: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/lets-encrypt-preview
|
||||
cd lets-encrypt-preview
|
||||
|
||||
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/lets-encrypt-preview/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
|
||||
|
|
@ -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`
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -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,7 +186,7 @@ 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:
|
||||
|
|
@ -227,5 +227,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::
|
||||
|
|
@ -62,10 +62,10 @@ class DVSNI(AnnotatedChallenge):
|
|||
return cert_pem, response
|
||||
|
||||
|
||||
class SimpleHTTPS(AnnotatedChallenge):
|
||||
"""Client annotated "simpleHttps" ACME challenge."""
|
||||
class SimpleHTTP(AnnotatedChallenge):
|
||||
"""Client annotated "simpleHttp" ACME challenge."""
|
||||
__slots__ = ('challb', 'domain', 'key')
|
||||
acme_type = challenges.SimpleHTTPS
|
||||
acme_type = challenges.SimpleHTTP
|
||||
|
||||
|
||||
class DNS(AnnotatedChallenge):
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import logging
|
|||
import time
|
||||
|
||||
from acme import challenges
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import constants
|
||||
|
|
@ -24,13 +24,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
|
||||
|
|
@ -82,7 +82,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 +134,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.
|
||||
|
|
@ -196,7 +198,7 @@ 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
|
||||
|
|
@ -205,9 +207,9 @@ class AuthHandler(object):
|
|||
status = self._get_chall_status(self.authzr[domain], achall)
|
||||
|
||||
# This does nothing for challenges that have yet to be decided yet.
|
||||
if status == messages2.STATUS_VALID:
|
||||
if status == messages.STATUS_VALID:
|
||||
completed.append(achall)
|
||||
elif status == messages2.STATUS_INVALID:
|
||||
elif status == messages.STATUS_INVALID:
|
||||
failed.append(achall)
|
||||
|
||||
return completed, failed
|
||||
|
|
@ -219,7 +221,7 @@ class AuthHandler(object):
|
|||
each challenge resource.
|
||||
|
||||
:param authzr: Authorization Resource
|
||||
:type authzr: :class:`acme.messages2.AuthorizationResource`
|
||||
:type authzr: :class:`acme.messages.AuthorizationResource`
|
||||
|
||||
:param achall: Annotated challenge for which to get status
|
||||
:type achall: :class:`letsencrypt.achallenges.AnnotatedChallenge`
|
||||
|
|
@ -277,8 +279,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):
|
||||
|
|
@ -319,7 +321,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,28 +333,22 @@ 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.SimpleHTTPS):
|
||||
logging.info(" SimpleHTTPS challenge for %s.", domain)
|
||||
return achallenges.SimpleHTTPS(
|
||||
elif isinstance(chall, challenges.SimpleHTTP):
|
||||
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)
|
||||
|
||||
|
|
@ -368,8 +364,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.
|
||||
|
||||
|
|
|
|||
|
|
@ -146,21 +146,21 @@ def install(args, config, plugins):
|
|||
return "Installer could not be determined"
|
||||
acme, doms = _common_run(
|
||||
args, config, acc, authenticator=None, installer=installer)
|
||||
assert args.cert_path is not None # required=True in the subparser
|
||||
assert args.cert_path is not None
|
||||
acme.deploy_certificate(doms, acc.key.file, args.cert_path, args.chain_path)
|
||||
acme.enhance_config(doms, args.redirect)
|
||||
|
||||
|
||||
def revoke(args, unused_config, unused_plugins):
|
||||
"""Revoke."""
|
||||
if args.cert_path is None and args.key_path is None:
|
||||
return "At least one of --cert-path or --key-path is required"
|
||||
if args.rev_cert is None and args.rev_key is None:
|
||||
return "At least one of --certificate or --key is required"
|
||||
|
||||
# This depends on the renewal config and cannot be completed yet.
|
||||
zope.component.getUtility(interfaces.IDisplay).notification(
|
||||
"Revocation is not available with the new Boulder server yet.")
|
||||
#client.revoke(args.installer, config, plugins, args.no_confirm,
|
||||
# args.cert_path, args.key_path)
|
||||
# args.rev_cert, args.rev_key)
|
||||
|
||||
|
||||
def rollback(args, config, plugins):
|
||||
|
|
@ -252,6 +252,9 @@ def create_parser(plugins):
|
|||
add("-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(
|
||||
"testing", description="The following flags are meant for "
|
||||
"testing purposes only! Do NOT change them, unless you "
|
||||
|
|
@ -265,6 +268,34 @@ def create_parser(plugins):
|
|||
"--dvsni-port", type=int, help=config_help("dvsni_port"),
|
||||
default=flag_default("dvsni_port"))
|
||||
|
||||
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__)
|
||||
subparser.set_defaults(func=func)
|
||||
return subparser
|
||||
|
||||
add_subparser("run", run)
|
||||
add_subparser("auth", auth)
|
||||
add_subparser("install", install)
|
||||
parser_revoke = add_subparser("revoke", revoke)
|
||||
parser_rollback = add_subparser("rollback", rollback)
|
||||
add_subparser("config_changes", config_changes)
|
||||
|
||||
parser_plugins = add_subparser("plugins", plugins_cmd)
|
||||
parser_plugins.add_argument("--init", action="store_true")
|
||||
parser_plugins.add_argument("--prepare", action="store_true")
|
||||
parser_plugins.add_argument(
|
||||
"--authenticators", action="append_const", dest="ifaces",
|
||||
const=interfaces.IAuthenticator)
|
||||
parser_plugins.add_argument(
|
||||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller)
|
||||
|
||||
parser.add_argument("--configurator")
|
||||
parser.add_argument("-a", "--authenticator")
|
||||
parser.add_argument("-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:
|
||||
|
|
@ -283,56 +314,11 @@ def create_parser(plugins):
|
|||
help="Automatically redirect all HTTP traffic to HTTPS for the newly "
|
||||
"authenticated vhost.")
|
||||
|
||||
_paths_parser(parser)
|
||||
# _plugins_parsing should be the last thing to act upon the main
|
||||
# parser (--help should display plugin-specific options last)
|
||||
_plugins_parsing(parser, plugins)
|
||||
|
||||
_create_subparsers(parser)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def _create_subparsers(parser):
|
||||
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__)
|
||||
subparser.set_defaults(func=func)
|
||||
return subparser
|
||||
|
||||
# the order of add_subparser() calls is important: it defines the
|
||||
# order in which subparser names will be displayed in --help
|
||||
add_subparser("run", run)
|
||||
add_subparser("auth", auth)
|
||||
parser_install = add_subparser("install", install)
|
||||
parser_plugins = add_subparser("plugins", plugins_cmd)
|
||||
parser_revoke = add_subparser("revoke", revoke)
|
||||
parser_rollback = add_subparser("rollback", rollback)
|
||||
add_subparser("config_changes", config_changes)
|
||||
|
||||
parser_install.add_argument(
|
||||
"--cert-path", required=True, help="Path to a certificate that "
|
||||
"is going to be installed.")
|
||||
parser_install.add_argument(
|
||||
"--chain-path", help="Accompanying path to a certificate chain.")
|
||||
|
||||
parser_plugins.add_argument(
|
||||
"--init", action="store_true", help="Initialize plugins.")
|
||||
parser_plugins.add_argument("--prepare", action="store_true",
|
||||
help="Initialize and prepare plugins.")
|
||||
parser_plugins.add_argument(
|
||||
"--authenticators", action="append_const", dest="ifaces",
|
||||
const=interfaces.IAuthenticator,
|
||||
help="Limit to authenticator plugins only.")
|
||||
parser_plugins.add_argument(
|
||||
"--installers", action="append_const", dest="ifaces",
|
||||
const=interfaces.IInstaller, help="Limit to installer plugins only.")
|
||||
|
||||
parser_revoke.add_argument(
|
||||
"--cert-path", type=read_file, help="Revoke a specific certificate.")
|
||||
"--certificate", dest="rev_cert", type=read_file, metavar="CERT_PATH",
|
||||
help="Revoke a specific certificate.")
|
||||
parser_revoke.add_argument(
|
||||
"--key-path", type=read_file,
|
||||
"--key", dest="rev_key", type=read_file, metavar="KEY_PATH",
|
||||
help="Revoke all certs generated by the provided authorized key.")
|
||||
|
||||
parser_rollback.add_argument(
|
||||
|
|
@ -340,43 +326,41 @@ def _create_subparsers(parser):
|
|||
default=flag_default("rollback_checkpoints"),
|
||||
help="Revert configuration N number of checkpoints.")
|
||||
|
||||
_paths_parser(parser.add_argument_group("paths"))
|
||||
|
||||
def _paths_parser(parser):
|
||||
add = parser.add_argument_group("paths").add_argument
|
||||
add("--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
add("--work-dir", default=flag_default("work_dir"),
|
||||
help=config_help("work_dir"))
|
||||
# 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
|
||||
|
||||
|
||||
def _plugins_parsing(parser, plugins):
|
||||
plugins_group = parser.add_argument_group(
|
||||
"plugins", description="Let's Encrypt client supports an extensible "
|
||||
"plugins architecture. See '%(prog)s plugins' for a list of all "
|
||||
"available plugins and their names. You can force a particular "
|
||||
"plugin by setting options provided below. Futher down this help "
|
||||
"message you will find plugin-specific options (prefixed by "
|
||||
"--{plugin_name}.")
|
||||
plugins_group.add_argument(
|
||||
"-a", "--authenticator", help="Authenticator plugin name.")
|
||||
plugins_group.add_argument(
|
||||
"-i", "--installer", help="Installer plugin name.")
|
||||
plugins_group.add_argument(
|
||||
"--configurator", help="Name of the plugin that is both "
|
||||
"an authenticator and an installer. Should not be used together "
|
||||
"with --authenticator or --installer.")
|
||||
def _paths_parser(parser):
|
||||
add = parser.add_argument
|
||||
add("--config-dir", default=flag_default("config_dir"),
|
||||
help=config_help("config_dir"))
|
||||
add("--work-dir", default=flag_default("work_dir"),
|
||||
help=config_help("work_dir"))
|
||||
add("--backup-dir", default=flag_default("backup_dir"),
|
||||
help=config_help("backup_dir"))
|
||||
add("--key-dir", default=flag_default("key_dir"),
|
||||
help=config_help("key_dir"))
|
||||
add("--cert-dir", default=flag_default("certs_dir"),
|
||||
help=config_help("cert_dir"))
|
||||
|
||||
# things should not be reorder past/pre this comment:
|
||||
# plugins_group should be displayed in --help before plugin
|
||||
# specific groups (so that plugins_group.description makes sense)
|
||||
add("--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"))
|
||||
|
||||
for name, plugin_ep in plugins.iteritems():
|
||||
plugin_ep.plugin_cls.inject_parser_options(
|
||||
parser.add_argument_group(
|
||||
"plugins: {0}".format(name),
|
||||
description=plugin_ep.description), name)
|
||||
add("--renewer-config-file", default=flag_default("renewer_config_file"),
|
||||
help=config_help("renewer_config_file"))
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
def main(args=sys.argv[1:]):
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ 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
|
||||
|
|
@ -31,7 +31,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`
|
||||
|
|
@ -64,7 +64,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))
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ class Client(object):
|
|||
cert_key = crypto_util.init_save_key(
|
||||
self.config.rsa_key_size, self.config.key_dir)
|
||||
csr = crypto_util.init_save_csr(
|
||||
cert_key, domains, self.config.csr_dir)
|
||||
cert_key, domains, self.config.cert_dir)
|
||||
|
||||
# Retrieve certificate
|
||||
certr = self.network.request_issuance(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ class NamespaceConfig(object):
|
|||
|
||||
- `accounts_dir`
|
||||
- `account_keys_dir`
|
||||
- `csr_dir`
|
||||
- `cert_dir`
|
||||
- `cert_key_backup`
|
||||
- `in_progress_dir`
|
||||
- `key_dir`
|
||||
|
|
@ -65,8 +65,8 @@ class NamespaceConfig(object):
|
|||
constants.CERT_KEY_BACKUP_DIR, self.server_path)
|
||||
|
||||
@property
|
||||
def csr_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.CSR_DIR)
|
||||
def cert_dir(self): # pylint: disable=missing-docstring
|
||||
return os.path.join(self.namespace.config_dir, constants.CERT_DIR)
|
||||
|
||||
@property
|
||||
def in_progress_dir(self): # pylint: disable=missing-docstring
|
||||
|
|
|
|||
|
|
@ -17,6 +17,14 @@ CLI_DEFAULTS = dict(
|
|||
work_dir="/var/lib/letsencrypt",
|
||||
no_verify_ssl=False,
|
||||
dvsni_port=challenges.DVSNI.PORT,
|
||||
|
||||
# TODO: blocked by #485, values ignored
|
||||
backup_dir="not used",
|
||||
key_dir="not used",
|
||||
certs_dir="not used",
|
||||
cert_path="not used",
|
||||
chain_path="not used",
|
||||
renewer_config_file="not used",
|
||||
)
|
||||
"""Defaults for CLI flags and `.IConfig` attributes."""
|
||||
|
||||
|
|
@ -30,7 +38,7 @@ RENEWER_DEFAULTS = dict(
|
|||
|
||||
|
||||
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
|
||||
challenges.DVSNI, challenges.SimpleHTTPS])])
|
||||
challenges.DVSNI, challenges.SimpleHTTP])])
|
||||
"""Mutually exclusive challenges."""
|
||||
|
||||
|
||||
|
|
@ -65,8 +73,8 @@ CERT_KEY_BACKUP_DIR = "keys-certs"
|
|||
"""Directory where all certificates and keys are stored (relative to
|
||||
`IConfig.work_dir`). Used for easy revocation."""
|
||||
|
||||
CSR_DIR = "csrs"
|
||||
"""Directory (relative to `IConfig.config_dir`) where CSRs are saved."""
|
||||
CERT_DIR = "certs"
|
||||
"""See `.IConfig.cert_dir`."""
|
||||
|
||||
IN_PROGRESS_DIR = "IN_PROGRESS"
|
||||
"""Directory used before a permanent checkpoint is finalized (relative to
|
||||
|
|
@ -90,8 +98,3 @@ RENEWAL_CONFIGS_DIR = "configs"
|
|||
|
||||
RENEWER_CONFIG_FILENAME = "renewer.conf"
|
||||
"""Renewer config file name (relative to `IConfig.config_dir`)."""
|
||||
|
||||
|
||||
NETSTAT = "/bin/netstat"
|
||||
"""Location of netstat binary for checking whether a listener is already
|
||||
running on the specified port (Linux-specific)."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,14 +5,6 @@ class LetsEncryptClientError(Exception):
|
|||
"""Generic Let's Encrypt client error."""
|
||||
|
||||
|
||||
class NetworkError(LetsEncryptClientError):
|
||||
"""Network error."""
|
||||
|
||||
|
||||
class UnexpectedUpdate(NetworkError):
|
||||
"""Unexpected update."""
|
||||
|
||||
|
||||
class LetsEncryptReverterError(LetsEncryptClientError):
|
||||
"""Let's Encrypt Reverter error."""
|
||||
|
||||
|
|
|
|||
|
|
@ -148,8 +148,7 @@ 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.")
|
||||
|
|
@ -162,7 +161,9 @@ class IConfig(zope.interface.Interface):
|
|||
account_keys_dir = zope.interface.Attribute(
|
||||
"Directory where all account keys are stored.")
|
||||
backup_dir = zope.interface.Attribute("Configuration backups directory.")
|
||||
csr_dir = zope.interface.Attribute("CSRs storage.")
|
||||
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.")
|
||||
|
|
@ -183,6 +184,15 @@ class IConfig(zope.interface.Interface):
|
|||
"Port number to perform DVSNI challenge. "
|
||||
"Boulder in testing mode defaults to 5001.")
|
||||
|
||||
# TODO: not implemented
|
||||
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")
|
||||
cert_path = zope.interface.Attribute("not used")
|
||||
chain_path = zope.interface.Attribute("not used")
|
||||
|
||||
|
||||
class IInstaller(IPlugin):
|
||||
"""Generic Let's Encrypt Installer Interface.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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
|
||||
|
|
@ -99,7 +99,9 @@ def renew(cert, old_version):
|
|||
def _create_parser():
|
||||
parser = argparse.ArgumentParser()
|
||||
#parser.add_argument("--cron", action="store_true", help="Run as cronjob.")
|
||||
return cli._paths_parser(parser) # pylint: disable=protected-access
|
||||
# pylint: disable=protected-access
|
||||
cli._paths_parser(parser.add_argument_group("paths"))
|
||||
return parser
|
||||
|
||||
def main(config=None, args=sys.argv[1:]):
|
||||
"""Main function for autorenewer script."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -238,6 +239,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)
|
||||
|
|
@ -250,10 +253,7 @@ class Revoker(object):
|
|||
raise errors.LetsEncryptRevokerError(
|
||||
"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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.SimpleHTTPS(
|
||||
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,18 +6,18 @@ 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
|
||||
|
||||
|
||||
TRANSLATE = {
|
||||
"dvsni": "DVSNI",
|
||||
"simpleHttps": "SimpleHTTPS",
|
||||
"simpleHttp": "SimpleHTTP",
|
||||
"dns": "DNS",
|
||||
"recoveryToken": "RecoveryToken",
|
||||
"recoveryContact": "RecoveryContact",
|
||||
|
|
@ -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,9 +57,9 @@ 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])
|
||||
|
|
@ -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,
|
||||
|
|
@ -160,10 +160,10 @@ class GetAuthorizationsTest(unittest.TestCase):
|
|||
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,7 +213,7 @@ 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):
|
||||
|
|
@ -241,10 +241,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 +269,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,
|
||||
|
|
@ -299,8 +299,8 @@ class GenChallengePathTest(unittest.TestCase):
|
|||
return gen_challenge_path(challbs, preferences, combinations)
|
||||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and SimpleHTTPS with appropriate combos."""
|
||||
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTPS_P)
|
||||
"""Given DVSNI and SimpleHTTP with appropriate combos."""
|
||||
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
|
|
@ -315,7 +315,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,13 +328,13 @@ 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
|
||||
prefs = [challenges.RecoveryToken,
|
||||
challenges.ProofOfPossession,
|
||||
challenges.SimpleHTTPS,
|
||||
challenges.SimpleHTTP,
|
||||
challenges.DVSNI,
|
||||
challenges.RecoveryContact]
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
|
|
@ -403,8 +403,8 @@ class IsPreferredTest(unittest.TestCase):
|
|||
def _call(cls, chall, satisfied):
|
||||
from letsencrypt.auth_handler import is_preferred
|
||||
return is_preferred(chall, satisfied, exclusive_groups=frozenset([
|
||||
frozenset([challenges.DVSNI, challenges.SimpleHTTPS]),
|
||||
frozenset([challenges.DNS, challenges.SimpleHTTPS]),
|
||||
frozenset([challenges.DVSNI, challenges.SimpleHTTP]),
|
||||
frozenset([challenges.DNS, challenges.SimpleHTTP]),
|
||||
]))
|
||||
|
||||
def test_empty_satisfied(self):
|
||||
|
|
@ -413,7 +413,7 @@ 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(
|
||||
|
|
@ -429,8 +429,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__":
|
||||
|
|
|
|||
|
|
@ -27,14 +27,14 @@ class ClientTest(unittest.TestCase):
|
|||
self.account = mock.MagicMock(**{"key.pem": KEY})
|
||||
|
||||
from letsencrypt.client import Client
|
||||
with mock.patch("letsencrypt.client.network2") as network2:
|
||||
with mock.patch("letsencrypt.client.network") as network:
|
||||
self.client = Client(
|
||||
config=self.config, account_=self.account, dv_auth=None,
|
||||
installer=None)
|
||||
self.network2 = network2
|
||||
self.network = network
|
||||
|
||||
def test_init_network_verify_ssl(self):
|
||||
self.network2.Network.assert_called_once_with(
|
||||
self.network.Network.assert_called_once_with(
|
||||
mock.ANY, mock.ANY, verify_ssl=True)
|
||||
|
||||
@mock.patch("letsencrypt.client.zope.component.getUtility")
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class NamespaceConfigTest(unittest.TestCase):
|
|||
constants.ACCOUNT_KEYS_DIR = 'keys'
|
||||
constants.BACKUP_DIR = 'backups'
|
||||
constants.CERT_KEY_BACKUP_DIR = 'c/'
|
||||
constants.CSR_DIR = 'csrs'
|
||||
constants.CERT_DIR = 'certs'
|
||||
constants.IN_PROGRESS_DIR = '../p'
|
||||
constants.KEY_DIR = 'keys'
|
||||
constants.REC_TOKEN_DIR = '/r'
|
||||
|
|
@ -46,7 +46,7 @@ class NamespaceConfigTest(unittest.TestCase):
|
|||
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.csr_dir, '/tmp/config/csrs')
|
||||
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')
|
||||
|
|
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -89,7 +89,7 @@ class RevokerTest(RevokerBase):
|
|||
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 +105,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 +122,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 +141,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 +165,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -185,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
|
||||
|
||||
|
|
@ -236,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
|
||||
|
|
@ -327,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(
|
||||
|
|
@ -412,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
|
||||
|
|
@ -493,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))
|
||||
|
|
@ -796,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
|
||||
|
|
@ -927,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
|
||||
|
|
@ -1059,9 +1059,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)
|
||||
|
||||
|
||||
|
|
@ -1124,9 +1123,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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -18,7 +20,7 @@ class ApacheDvsni(object):
|
|||
larger array. ApacheDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within ApacheConfigurator
|
||||
who must return all responses in order. Imagine ApacheConfigurator
|
||||
maintaining state about where all of the SimpleHTTPS Challenges,
|
||||
maintaining state about where all of the SimpleHTTP Challenges,
|
||||
Dvsni Challenges belong in the response array. This is an optional
|
||||
utility.
|
||||
|
||||
|
|
@ -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`)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -111,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))
|
||||
|
|
@ -132,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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
@ -31,32 +29,6 @@ class DvsniPerformTest(util.ApacheTest):
|
|||
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)
|
||||
|
|
@ -66,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)
|
||||
|
|
@ -139,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))
|
||||
|
|
|
|||
|
|
@ -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,35 +34,6 @@ 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, src=constants.MOD_SSL_CONF_SRC,
|
||||
dest=constants.MOD_SSL_CONF_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 get_apache_configurator(
|
||||
config_path, config_dir, work_dir, version=(2, 4, 7)):
|
||||
"""Create an Apache Configurator with the specified options."""
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -132,9 +132,8 @@ class NginxConfigurator(common.Plugin):
|
|||
vhost.filep, vhost.names)
|
||||
except errors.LetsEncryptMisconfigurationError:
|
||||
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
|
||||
|
||||
|
|
@ -354,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
|
||||
|
|
@ -572,14 +569,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
|
||||
|
|
|
|||
|
|
@ -4,14 +4,13 @@ import logging
|
|||
import os
|
||||
|
||||
from letsencrypt import errors
|
||||
|
||||
from letsencrypt_apache.dvsni import ApacheDvsni
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
from letsencrypt_nginx import obj
|
||||
from letsencrypt_nginx import nginxparser
|
||||
|
||||
|
||||
class NginxDvsni(ApacheDvsni):
|
||||
class NginxDvsni(common.Dvsni):
|
||||
"""Class performs DVSNI challenges within the Nginx configurator.
|
||||
|
||||
:ivar configurator: NginxConfigurator object
|
||||
|
|
@ -24,7 +23,7 @@ class NginxDvsni(ApacheDvsni):
|
|||
larger array. NginxDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within NginxConfigurator
|
||||
who must return all responses in order. Imagine NginxConfigurator
|
||||
maintaining state about where all of the SimpleHTTPS Challenges,
|
||||
maintaining state about where all of the SimpleHTTP Challenges,
|
||||
Dvsni Challenges belong in the response array. This is an optional
|
||||
utility.
|
||||
|
||||
|
|
@ -51,9 +50,9 @@ class NginxDvsni(ApacheDvsni):
|
|||
vhost = self.configurator.choose_vhost(achall.domain)
|
||||
if vhost is None:
|
||||
logging.error(
|
||||
"No nginx vhost exists with server_name matching: %s",
|
||||
"No nginx vhost exists with server_name matching: %s. "
|
||||
"Please specify server_names in the Nginx config.",
|
||||
achall.domain)
|
||||
logging.error("Please specify server_names in the Nginx config")
|
||||
return None
|
||||
|
||||
for addr in vhost.addrs:
|
||||
|
|
@ -125,7 +124,7 @@ class NginxDvsni(ApacheDvsni):
|
|||
|
||||
"""
|
||||
document_root = os.path.join(
|
||||
self.configurator.config.config_dir, "dvsni_page")
|
||||
self.configurator.config.work_dir, "dvsni_page")
|
||||
|
||||
block = [['listen', str(addr)] for addr in addrs]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
"""Module contains classes used by the Nginx Configurator."""
|
||||
import re
|
||||
|
||||
from letsencrypt_apache.obj import Addr as ApacheAddr
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
|
||||
class Addr(ApacheAddr):
|
||||
class Addr(common.Addr):
|
||||
r"""Represents an Nginx address, i.e. what comes after the 'listen'
|
||||
directive.
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import unittest
|
|||
import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import messages2
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import errors
|
||||
|
|
@ -164,20 +164,20 @@ class NginxConfiguratorTest(util.NginxTest):
|
|||
# Note: As more challenges are offered this will have to be expanded
|
||||
auth_key = le_util.Key(self.rsa256_file, self.rsa256_pem)
|
||||
achall1 = achallenges.DVSNI(
|
||||
challb=messages2.ChallengeBody(
|
||||
challb=messages.ChallengeBody(
|
||||
chall=challenges.DVSNI(
|
||||
r="foo",
|
||||
nonce="bar"),
|
||||
uri="https://ca.org/chall0_uri",
|
||||
status=messages2.Status("pending"),
|
||||
status=messages.Status("pending"),
|
||||
), domain="localhost", key=auth_key)
|
||||
achall2 = achallenges.DVSNI(
|
||||
challb=messages2.ChallengeBody(
|
||||
challb=messages.ChallengeBody(
|
||||
chall=challenges.DVSNI(
|
||||
r="abc",
|
||||
nonce="def"),
|
||||
uri="https://ca.org/chall1_uri",
|
||||
status=messages2.Status("pending"),
|
||||
status=messages.Status("pending"),
|
||||
), domain="example.com", key=auth_key)
|
||||
|
||||
dvsni_ret_val = [
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
"""Test for letsencrypt_nginx.dvsni."""
|
||||
import pkg_resources
|
||||
import unittest
|
||||
import shutil
|
||||
|
||||
|
|
@ -9,7 +8,8 @@ from acme import challenges
|
|||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.plugins import common_test
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
from letsencrypt_nginx import obj
|
||||
|
|
@ -19,49 +19,43 @@ from letsencrypt_nginx.tests import util
|
|||
class DvsniPerformTest(util.NginxTest):
|
||||
"""Test the NginxDVSNI challenge."""
|
||||
|
||||
achalls = [
|
||||
achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
r="foo",
|
||||
nonce="bar"
|
||||
), "pending"),
|
||||
domain="www.example.com", key=common_test.DvsniTest.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="blah", key=common_test.DvsniTest.auth_key),
|
||||
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="www.example.org", key=common_test.DvsniTest.auth_key)
|
||||
]
|
||||
|
||||
|
||||
def setUp(self):
|
||||
super(DvsniPerformTest, self).setUp()
|
||||
|
||||
config = util.get_nginx_configurator(
|
||||
self.config_path, self.config_dir, self.work_dir)
|
||||
|
||||
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)
|
||||
|
||||
from letsencrypt_nginx import dvsni
|
||||
self.sni = dvsni.NginxDvsni(config)
|
||||
|
||||
self.achalls = [
|
||||
achallenges.DVSNI(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.DVSNI(
|
||||
r="foo",
|
||||
nonce="bar"
|
||||
), "pending"),
|
||||
domain="www.example.com", 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="blah", key=auth_key),
|
||||
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="www.example.org", key=auth_key)
|
||||
]
|
||||
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
shutil.rmtree(self.config_dir)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import unittest
|
|||
|
||||
import mock
|
||||
|
||||
from letsencrypt_apache.tests import util as apache_util
|
||||
from letsencrypt.plugins import common
|
||||
|
||||
from letsencrypt_nginx import constants
|
||||
from letsencrypt_nginx import configurator
|
||||
|
|
@ -16,10 +16,10 @@ class NginxTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
|||
def setUp(self):
|
||||
super(NginxTest, self).setUp()
|
||||
|
||||
self.temp_dir, self.config_dir, self.work_dir = apache_util.dir_setup(
|
||||
self.temp_dir, self.config_dir, self.work_dir = common.dir_setup(
|
||||
"etc_nginx", "letsencrypt_nginx.tests")
|
||||
|
||||
self.ssl_options = apache_util.setup_ssl_options(
|
||||
self.ssl_options = common.setup_ssl_options(
|
||||
self.config_dir, constants.MOD_SSL_CONF_SRC,
|
||||
constants.MOD_SSL_CONF_DEST)
|
||||
|
||||
|
|
|
|||
67
requirements-swig-3.0.5.txt
Normal file
67
requirements-swig-3.0.5.txt
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Support swig 3.0.5+
|
||||
# https://github.com/M2Crypto/M2Crypto/issues/24
|
||||
# https://github.com/M2Crypto/M2Crypto/pull/30
|
||||
git+https://github.com/M2Crypto/M2Crypto.git@d13a3a46c8934c5f50b31d5f95b23e6e06f845c3#egg=M2Crypto
|
||||
|
||||
# This requirements file will fail on Travis CI 12.04 LTS Ubuntu build
|
||||
# machine under TOX_ENV=py26 with very confusing error (full tracback
|
||||
# at https://api.travis-ci.org/jobs/66529698/log.txt?deansi=true):
|
||||
|
||||
#Traceback (most recent call last):
|
||||
# File "setup.py", line 133, in <module>
|
||||
# include_package_data=True,
|
||||
# File "/opt/python/2.6.9/lib/python2.6/distutils/core.py", line 152, in setup
|
||||
# dist.run_commands()
|
||||
# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 975, in run_commands
|
||||
# self.run_command(cmd)
|
||||
# File "/opt/python/2.6.9/lib/python2.6/distutils/dist.py", line 995, in run_command
|
||||
# cmd_obj.run()
|
||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 142, in run
|
||||
# self.with_project_on_sys_path(self.run_tests)
|
||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 122, in with_project_on_sys_path
|
||||
# func()
|
||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 163, in run_tests
|
||||
# testRunner=self._resolve_as_ep(self.test_runner),
|
||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 816, in __init__
|
||||
# self.parseArgs(argv)
|
||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 843, in parseArgs
|
||||
# self.createTests()
|
||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 849, in createTests
|
||||
# self.module)
|
||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 613, in loadTestsFromNames
|
||||
# suites = [self.loadTestsFromName(name, module) for name in names]
|
||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 587, in loadTestsFromName
|
||||
# return self.loadTestsFromModule(obj)
|
||||
# File "/home/travis/build/letsencrypt/lets-encrypt-preview/.tox/py26/lib/python2.6/site-packages/setuptools/command/test.py", line 37, in loadTestsFromModule
|
||||
# tests.append(self.loadTestsFromName(submodule))
|
||||
# File "/opt/python/2.6.9/lib/python2.6/unittest.py", line 584, in loadTestsFromName
|
||||
# parent, obj = obj, getattr(obj, part)
|
||||
#AttributeError: 'module' object has no attribute 'continuity_auth'
|
||||
|
||||
# the above error happens because letsencrypt.continuity_auth cannot import M2Crypto:
|
||||
|
||||
#>>> import M2Crypto
|
||||
#Traceback (most recent call last):
|
||||
# File "<stdin>", line 1, in <module>
|
||||
# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/__init__.py", line 22, in <module>
|
||||
# import m2crypto
|
||||
# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 26, in <module>
|
||||
# _m2crypto = swig_import_helper()
|
||||
# File "/root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/m2crypto.py", line 22, in swig_import_helper
|
||||
# _mod = imp.load_module('_m2crypto', fp, pathname, description)
|
||||
#ImportError: /root/lets-encrypt-preview/venv/lib/python2.6/site-packages/M2Crypto-0.21.1-py2.6-linux-x86_64.egg/M2Crypto/_m2crypto.so: undefined symbol: SSLv2_method
|
||||
|
||||
# For more info see:
|
||||
|
||||
# - https://github.com/martinpaljak/M2Crypto/commit/84977c532c2444c5487db57146d81bb68dd5431d
|
||||
# - http://stackoverflow.com/questions/10547332/install-m2crypto-on-a-virtualenv-without-system-packages
|
||||
# - http://stackoverflow.com/questions/8206546/undefined-symbol-sslv2-method
|
||||
|
||||
# In short: Python has been built without SSLv2 support, and
|
||||
# github.com/M2Crypto/M2Crypto version doesn't contain necessary
|
||||
# patch, but it's the only one that has a patch for newer versions of
|
||||
# swig...
|
||||
|
||||
# Problem seems not exists on Python 2.7. It's unlikely that the
|
||||
# target distribution has swig 3.0.5+ and doesn't have Python 2.7, so
|
||||
# this file should only be used in conjuction with Python 2.6.
|
||||
|
|
@ -1,3 +1,2 @@
|
|||
# https://github.com/bw2/ConfigArgParse/issues/17
|
||||
-e git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse
|
||||
-e .
|
||||
git+https://github.com/kuba/ConfigArgParse.git@python2.6#egg=ConfigArgParse
|
||||
|
|
|
|||
64
setup.py
64
setup.py
|
|
@ -28,11 +28,65 @@ meta = dict(re.findall(r"""__([a-z]+)__ = "([^"]+)""", read_file(init_fn)))
|
|||
readme = read_file(os.path.join(here, 'README.rst'))
|
||||
changes = read_file(os.path.join(here, 'CHANGES.rst'))
|
||||
|
||||
# #358: acme, letsencrypt, letsencrypt_apache, letsencrypt_nginx, etc.
|
||||
# shall be distributed separately. Please make sure to keep the
|
||||
# dependecy lists up to date: this is being somewhat checked below
|
||||
# using an assert statement! Separate lists are helpful for OS package
|
||||
# maintainers. and will make the future migration a lot easier.
|
||||
acme_install_requires = [
|
||||
'argparse',
|
||||
#'letsencrypt' # TODO: uses testdata vectors
|
||||
'mock',
|
||||
'pycrypto',
|
||||
'pyrfc3339',
|
||||
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
|
||||
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
|
||||
'pytz',
|
||||
'requests',
|
||||
'werkzeug',
|
||||
'M2Crypto',
|
||||
]
|
||||
letsencrypt_install_requires = [
|
||||
#'acme',
|
||||
'argparse',
|
||||
'ConfigArgParse',
|
||||
'configobj',
|
||||
'M2Crypto',
|
||||
'mock',
|
||||
'parsedatetime',
|
||||
'psutil>=2.1.0', # net_connections introduced in 2.1.0
|
||||
'pycrypto',
|
||||
# https://pyopenssl.readthedocs.org/en/latest/api/crypto.html#OpenSSL.crypto.X509Req.get_extensions
|
||||
'PyOpenSSL>=0.15',
|
||||
'pyrfc3339',
|
||||
'python-augeas',
|
||||
'python2-pythondialog>=3.2.2rc1', # Debian squeeze support, cf. #280
|
||||
'pytz',
|
||||
'requests',
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
'M2Crypto',
|
||||
]
|
||||
letsencrypt_apache_install_requires = [
|
||||
#'acme',
|
||||
#'letsencrypt',
|
||||
'mock',
|
||||
'python-augeas',
|
||||
'zope.component',
|
||||
'zope.interface',
|
||||
]
|
||||
letsencrypt_nginx_install_requires = [
|
||||
#'acme',
|
||||
#'letsencrypt',
|
||||
'pyparsing>=1.5.5', # Python3 support; perhaps unnecessary?
|
||||
'mock',
|
||||
'zope.interface',
|
||||
]
|
||||
|
||||
install_requires = [
|
||||
'argparse',
|
||||
'ConfigArgParse',
|
||||
'configobj',
|
||||
'jsonschema',
|
||||
'mock',
|
||||
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
|
||||
'parsedatetime',
|
||||
|
|
@ -55,6 +109,13 @@ install_requires = [
|
|||
'M2Crypto',
|
||||
]
|
||||
|
||||
assert set(install_requires) == set.union(*(set(ireq) for ireq in (
|
||||
acme_install_requires,
|
||||
letsencrypt_install_requires,
|
||||
letsencrypt_apache_install_requires,
|
||||
letsencrypt_nginx_install_requires
|
||||
))), "*install_requires don't match up!"
|
||||
|
||||
dev_extras = [
|
||||
# Pin astroid==1.3.5, pylint==1.4.2 as a workaround for #289
|
||||
'astroid==1.3.5',
|
||||
|
|
@ -120,6 +181,7 @@ setup(
|
|||
'jws = letsencrypt.acme.jose.jws:CLI.run',
|
||||
],
|
||||
'letsencrypt.plugins': [
|
||||
'manual = letsencrypt.plugins.manual:ManualAuthenticator',
|
||||
'standalone = letsencrypt.plugins.standalone.authenticator'
|
||||
':StandaloneAuthenticator',
|
||||
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ cover () {
|
|||
"$1" --cover-min-percentage="$2" "$1"
|
||||
}
|
||||
|
||||
rm -f .coverage # --cover-erase is off, make sure stats are correct
|
||||
|
||||
# don't use sequential composition (;), if letsencrypt_nginx returns
|
||||
# 0, coveralls submit will be triggered (c.f. .travis.yml,
|
||||
# after_success)
|
||||
cover letsencrypt 95 && cover acme 100 && \
|
||||
cover letsencrypt_apache 78 && cover letsencrypt_nginx 96
|
||||
cover letsencrypt_apache 76 && cover letsencrypt_nginx 96
|
||||
|
|
|
|||
4
tox.ini
4
tox.ini
|
|
@ -22,12 +22,12 @@ setenv =
|
|||
[testenv:cover]
|
||||
basepython = python2.7
|
||||
commands =
|
||||
pip install -e .[testing]
|
||||
pip install -r requirements.txt -e .[testing]
|
||||
./tox.cover.sh
|
||||
|
||||
[testenv:lint]
|
||||
# recent versions of pylint do not support Python 2.6 (#97, #187)
|
||||
basepython = python2.7
|
||||
commands =
|
||||
pip install -e .[dev]
|
||||
pip install -r requirements.txt -e .[dev]
|
||||
pylint --rcfile=.pylintrc letsencrypt acme letsencrypt_apache letsencrypt_nginx
|
||||
|
|
|
|||
Loading…
Reference in a new issue