Merge branch 'master' into apache_modules

This commit is contained in:
James Kasten 2015-07-10 15:18:35 -07:00
commit 1b308cd530
193 changed files with 1393 additions and 1216 deletions

View file

@ -1,11 +1,11 @@
# this file uses slightly different syntax than .gitignore,
# e.g. ".tox/" will not ignore .tox directory
# e.g. "tox.cover/" will not ignore tox.cover directory
# well, official docker build should be done on clean git checkout
# anyway, so .tox should be empty... But I'm sure people will try to
# test docker on their git working directories.
.git
.tox
tox.cover
venv
docs

2
.gitignore vendored
View file

@ -4,7 +4,7 @@
build/
dist/
/venv/
/.tox/
/tox.venv/
letsencrypt.log
# coverage

View file

@ -20,7 +20,7 @@ env:
install: "travis_retry pip install tox coveralls"
before_script: '[ "${TOXENV:0:2}" != "py" ] || ./tests/boulder-start.sh'
# TODO: eliminate substring slice bashism
script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source .tox/$TOXENV/bin/activate && ./tests/boulder-integration.sh))'
script: 'travis_retry tox && ([ "${TOXENV:0:2}" != "py" ] || (source tox.venv/bin/activate && ./tests/boulder-integration.sh))'
after_success: '[ "$TOXENV" == "cover" ] && coveralls'

View file

@ -50,7 +50,11 @@ 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
/opt/letsencrypt/venv/bin/pip install \
-e /opt/letsencrypt/src/acme \
-e /opt/letsencrypt/src \
-e /opt/letsencrypt/src/letsencrypt_apache \
-e /opt/letsencrypt/src/letsencrypt_nginx
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image);

View file

@ -1,15 +1,7 @@
include requirements.txt
include README.rst
include CHANGES.rst
include CONTRIBUTING.md
include linter_plugin.py
include letsencrypt/EULA
recursive-include letsencrypt/tests/testdata *
recursive-include acme/schemata *.json
recursive-include acme/jose/testdata *
recursive-include letsencrypt_apache/tests/testdata *
include letsencrypt_apache/options-ssl-apache.conf
recursive-include letsencrypt_nginx/tests/testdata *
include letsencrypt_nginx/options-ssl-nginx.conf

2
Vagrantfile vendored
View file

@ -10,7 +10,7 @@ cd /vagrant
sudo ./bootstrap/ubuntu.sh
if [ ! -d "venv" ]; then
virtualenv --no-site-packages -p python2 venv
./venv/bin/pip install -r requirements.txt -e .[dev,docs,testing]
./venv/bin/pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt_apache -e letsencrypt_nginx
fi
SETUP_SCRIPT

1
acme/MANIFEST.in Normal file
View file

@ -0,0 +1 @@
recursive-include acme/testdata *

View file

@ -7,6 +7,7 @@ import os
import requests
from acme import interfaces
from acme import jose
from acme import other
@ -31,10 +32,17 @@ class DVChallenge(Challenge): # pylint: disable=abstract-method
"""Domain validation challenges."""
class ChallengeResponse(jose.TypedJSONObjectWithFields):
class ChallengeResponse(interfaces.ClientRequestableResource,
jose.TypedJSONObjectWithFields):
# _fields_to_partial_json | pylint: disable=abstract-method
"""ACME challenge response."""
"""ACME challenge response.
:ivar str mitm_resource: ACME resource identifier used in client
HTTPS requests in order to protect against MITM.
"""
TYPES = {}
resource_type = 'challenge'
@classmethod
def from_json(cls, jobj):

View file

@ -1,10 +1,6 @@
"""Tests for acme.challenges."""
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import mock
import OpenSSL
import requests
@ -12,15 +8,11 @@ import urlparse
from acme import jose
from acme import other
from acme import test_util
CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'cert.pem'))))
KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
CERT = test_util.load_cert('cert.pem')
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class ChallengeResponseTest(unittest.TestCase):

View file

@ -2,6 +2,7 @@
import datetime
import heapq
import httplib
import json
import logging
import time
@ -64,8 +65,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
new_authzr_uri=new_authzr_uri,
terms_of_service=terms_of_service)
def register(self, contact=messages.Registration._fields[
'contact'].default):
def register(self, new_reg=None):
"""Register.
:param contact: Contact list, as accepted by `.Registration`
@ -77,14 +77,16 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:raises .UnexpectedUpdate:
"""
new_reg = messages.Registration(contact=contact)
new_reg = messages.Registration() if new_reg is None else new_reg
response = self.net.post(self.new_reg_uri, new_reg)
assert response.status_code == httplib.CREATED # TODO: handle errors
# "Instance of 'Field' has no key/contact member" bug:
# pylint: disable=no-member
regr = self._regr_from_response(response)
if (regr.body.key != self.key.public_key()
or regr.body.contact != contact):
if (regr.body.key != self.key.public_key() or
regr.body.contact != new_reg.contact):
raise errors.UnexpectedUpdate(regr)
return regr
@ -444,11 +446,13 @@ class ClientNetwork(object):
.. todo:: Implement ``acmePath``.
:param JSONDeSerializable obj:
:param .ClientRequestableResource obj:
:rtype: `.JWS`
"""
dumps = obj.json_dumps()
jobj = obj.to_json()
jobj['resource'] = obj.resource_type
dumps = json.dumps(jobj)
logger.debug('Serialized JSON: %s', dumps)
return jws.JWS.sign(
payload=dumps, key=self.key, alg=self.alg, nonce=nonce).json_dumps()

View file

@ -1,8 +1,7 @@
"""Tests for acme.client."""
import datetime
import httplib
import os
import pkg_resources
import json
import unittest
import mock
@ -14,14 +13,12 @@ from acme import jose
from acme import jws as acme_jws
from acme import messages
from acme import messages_test
from acme import test_util
CERT_DER = pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'cert.der'))
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')))
CERT_DER = test_util.load_vector('cert.der')
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
class ClientTest(unittest.TestCase):
@ -74,6 +71,8 @@ class ClientTest(unittest.TestCase):
cert_chain_uri='https://www.letsencrypt-demo.org/ca')
def test_register(self):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
self.response.status_code = httplib.CREATED
self.response.json.return_value = self.regr.body.to_json()
self.response.headers['Location'] = self.regr.uri
@ -82,14 +81,14 @@ class ClientTest(unittest.TestCase):
'terms-of-service': {'url': self.regr.terms_of_service},
})
self.assertEqual(self.regr, self.client.register(self.contact))
self.assertEqual(self.regr, self.client.register(self.regr.body))
# TODO: test POST call arguments
# TODO: split here and separate test
reg_wrong_key = self.regr.body.update(key=KEY2.public_key())
self.response.json.return_value = reg_wrong_key.to_json()
self.assertRaises(
errors.UnexpectedUpdate, self.client.register, self.contact)
errors.UnexpectedUpdate, self.client.register, self.regr.body)
def test_register_missing_next(self):
self.response.status_code = httplib.CREATED
@ -97,6 +96,8 @@ class ClientTest(unittest.TestCase):
errors.ClientError, self.client.register, self.regr.body)
def test_update_registration(self):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
self.response.headers['Location'] = self.regr.uri
self.response.json.return_value = self.regr.body.to_json()
self.assertEqual(self.regr, self.client.update_registration(self.regr))
@ -367,20 +368,22 @@ class ClientNetworkTest(unittest.TestCase):
self.assertTrue(self.net.verify_ssl is self.verify_ssl)
def test_wrap_in_jws(self):
class MockJSONDeSerializable(jose.JSONDeSerializable):
class MockClientRequestableResource(jose.JSONDeSerializable):
# pylint: disable=missing-docstring
resource_type = 'mock'
def __init__(self, value):
self.value = value
def to_partial_json(self):
return self.value
return {'foo': self.value}
@classmethod
def from_json(cls, value):
pass # pragma: no cover
# pylint: disable=protected-access
jws_dump = self.net._wrap_in_jws(
MockJSONDeSerializable('foo'), nonce='Tg')
MockClientRequestableResource('foo'), nonce='Tg')
jws = acme_jws.JWS.json_loads(jws_dump)
self.assertEqual(jws.payload, '"foo"')
self.assertEqual(json.loads(jws.payload),
{'foo': 'foo', 'resource': 'mock'})
self.assertEqual(jws.signature.combined.nonce, 'Tg')
# TODO: check that nonce is in protected header
@ -503,7 +506,8 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
def test_head(self):
self.assertEqual(self.response, self.net.head('url', 'foo', bar='baz'))
self.send_request.assert_called_once('HEAD', 'url', 'foo', bar='baz')
self.send_request.assert_called_once_with(
'HEAD', 'url', 'foo', bar='baz')
def test_get(self):
self.assertEqual(self.checked_response, self.net.get(

13
acme/acme/interfaces.py Normal file
View file

@ -0,0 +1,13 @@
"""ACME interfaces."""
from acme import jose
class ClientRequestableResource(jose.JSONDeSerializable):
"""Resource that can be requested by client.
:ivar str resource_type: ACME resource identifier used in client
HTTPS requests in order to protect against MITM.
"""
# pylint: disable=abstract-method
resource_type = NotImplemented

View file

@ -1,23 +1,18 @@
"""Tests for acme.jose.json_util."""
import itertools
import os
import pkg_resources
import unittest
import mock
import OpenSSL
from acme import test_util
from acme.jose import errors
from acme.jose import interfaces
from acme.jose import util
CERT = util.ComparableX509(OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'cert.pem'))))
CSR = util.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'csr.pem'))))
CERT = test_util.load_cert('cert.pem')
CSR = test_util.load_csr('csr.pem')
class FieldTest(unittest.TestCase):

View file

@ -1,19 +1,14 @@
"""Tests for acme.jose.jwa."""
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from acme import test_util
from acme.jose import errors
from acme.jose import jwk_test
RSA1024_KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa1024_key.pem')),
password=None, backend=default_backend())
RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem')
RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem')
RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem')
class JWASignatureTest(unittest.TestCase):
@ -76,13 +71,13 @@ class JWARSTest(unittest.TestCase):
def test_sign_no_private_part(self):
from acme.jose.jwa import RS256
self.assertRaises(
errors.Error, RS256.sign, jwk_test.RSA512_KEY.public_key(), 'foo')
errors.Error, RS256.sign, RSA512_KEY.public_key(), 'foo')
def test_sign_key_too_small(self):
from acme.jose.jwa import RS256
from acme.jose.jwa import PS256
self.assertRaises(errors.Error, RS256.sign, jwk_test.RSA256_KEY, 'foo')
self.assertRaises(errors.Error, PS256.sign, jwk_test.RSA256_KEY, 'foo')
self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, 'foo')
self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, 'foo')
def test_rs(self):
from acme.jose.jwa import RS256
@ -92,11 +87,10 @@ class JWARSTest(unittest.TestCase):
'\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99'
'\xd2\xb9.>}\xfd'
)
self.assertEqual(RS256.sign(jwk_test.RSA512_KEY, 'foo'), sig)
self.assertTrue(RS256.verify(
jwk_test.RSA512_KEY.public_key(), 'foo', sig))
self.assertEqual(RS256.sign(RSA512_KEY, 'foo'), sig)
self.assertTrue(RS256.verify(RSA512_KEY.public_key(), 'foo', sig))
self.assertFalse(RS256.verify(
jwk_test.RSA512_KEY.public_key(), 'foo', sig + '!'))
RSA512_KEY.public_key(), 'foo', sig + '!'))
def test_ps(self):
from acme.jose.jwa import PS256

View file

@ -1,25 +1,15 @@
"""Tests for acme.jose.jwk."""
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from acme import test_util
from acme.jose import errors
from acme.jose import util
DSA_PEM = pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'dsa512_key.pem'))
RSA256_KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa256_key.pem')),
password=None, backend=default_backend())
RSA512_KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
DSA_PEM = test_util.load_vector('dsa512_key.pem')
RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem')
RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class JWKTest(unittest.TestCase):
@ -73,8 +63,8 @@ class JWKRSATest(unittest.TestCase):
'e': 'AQAB',
'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk',
}
self.jwk256_comparable = JWKRSA(key=util.ComparableRSAKey(
RSA256_KEY.public_key()))
# pylint: disable=protected-access
self.jwk256_not_comparable = JWKRSA(key=RSA256_KEY.public_key()._wrapped)
self.jwk512 = JWKRSA(key=RSA512_KEY.public_key())
self.jwk512json = {
'kty': 'RSA',
@ -96,9 +86,10 @@ class JWKRSATest(unittest.TestCase):
'qi': 'oi45cEkbVoJjAbnQpFY87Q',
})
def test_init_comparable(self):
self.assertTrue(isinstance(self.jwk256.key, util.ComparableRSAKey))
self.assertEqual(self.jwk256, self.jwk256_comparable)
def test_init_auto_comparable(self):
self.assertTrue(isinstance(
self.jwk256_not_comparable.key, util.ComparableRSAKey))
self.assertEqual(self.jwk256, self.jwk256_not_comparable)
def test_equals(self):
self.assertEqual(self.jwk256, self.jwk256)
@ -110,9 +101,8 @@ class JWKRSATest(unittest.TestCase):
def test_load(self):
from acme.jose.jwk import JWKRSA
self.assertEqual(
self.private, JWKRSA.load(pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa256_key.pem'))))
self.assertEqual(self.private, JWKRSA.load(
test_util.load_vector('rsa256_key.pem')))
def test_public_key(self):
self.assertEqual(self.jwk256, self.private.public_key())

View file

@ -1,28 +1,20 @@
"""Tests for acme.jose.jws."""
import base64
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import mock
import OpenSSL
from acme import test_util
from acme.jose import b64
from acme.jose import errors
from acme.jose import jwa
from acme.jose import jwk
from acme.jose import util
CERT = util.ComparableX509(OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
'letsencrypt.tests', 'testdata/cert.pem')))
RSA512_KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
CERT = test_util.load_cert('cert.pem')
KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
class MediaTypeTest(unittest.TestCase):
@ -112,7 +104,7 @@ class JWSTest(unittest.TestCase):
"""Tests for acme.jose.jws.JWS."""
def setUp(self):
self.privkey = jwk.JWKRSA(key=RSA512_KEY)
self.privkey = KEY
self.pubkey = self.privkey.public_key()
from acme.jose.jws import JWS
@ -209,8 +201,7 @@ class JWSTest(unittest.TestCase):
class CLITest(unittest.TestCase):
def setUp(self):
self.key_path = pkg_resources.resource_filename(
__name__, os.path.join('testdata', 'rsa512_key.pem'))
self.key_path = test_util.vector_path('rsa512_key.pem')
def test_unverified(self):
from acme.jose.jws import CLI

View file

@ -1,31 +1,22 @@
"""Tests for acme.jose.util."""
import functools
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import OpenSSL
from acme import test_util
class ComparableX509Test(unittest.TestCase):
"""Tests for acme.jose.util.ComparableX509."""
def setUp(self):
from acme.jose.util import ComparableX509
def _load(method, filename): # pylint: disable=missing-docstring
return ComparableX509(method(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', filename))))
# test_util.load_{csr,cert} return ComparableX509
self.req1 = test_util.load_csr('csr.pem')
self.req2 = test_util.load_csr('csr.pem')
self.req_other = test_util.load_csr('csr-san.pem')
self.req1 = _load(OpenSSL.crypto.load_certificate_request, 'csr.pem')
self.req2 = _load(OpenSSL.crypto.load_certificate_request, 'csr.pem')
self.req_other = _load(OpenSSL.crypto.load_certificate_request, 'csr-san.pem')
self.cert1 = _load(OpenSSL.crypto.load_certificate, 'cert.pem')
self.cert2 = _load(OpenSSL.crypto.load_certificate, 'cert.pem')
self.cert_other = _load(OpenSSL.crypto.load_certificate, 'cert-san.pem')
self.cert1 = test_util.load_cert('cert.pem')
self.cert2 = test_util.load_cert('cert.pem')
self.cert_other = test_util.load_cert('cert-san.pem')
def test_eq(self):
self.assertEqual(self.req1, self.req2)
@ -56,19 +47,10 @@ class ComparableRSAKeyTest(unittest.TestCase):
"""Tests for acme.jose.util.ComparableRSAKey."""
def setUp(self):
from acme.jose.util import ComparableRSAKey
backend = default_backend()
def load_key(): # pylint: disable=missing-docstring
return ComparableRSAKey(serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa256_key.pem')),
password=None, backend=backend))
self.key = load_key()
self.key_same = load_key()
self.key2 = ComparableRSAKey(serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=backend))
# test_utl.load_rsa_private_key return ComparableRSAKey
self.key = test_util.load_rsa_private_key('rsa256_key.pem')
self.key_same = test_util.load_rsa_private_key('rsa256_key.pem')
self.key2 = test_util.load_rsa_private_key('rsa512_key.pem')
def test_getattr_proxy(self):
self.assertEqual(256, self.key.key_size)

View file

@ -1,19 +1,12 @@
"""Tests for acme.jws."""
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from acme import errors
from acme import jose
from acme import test_util
RSA512_KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
class HeaderTest(unittest.TestCase):
@ -46,7 +39,7 @@ class JWSTest(unittest.TestCase):
"""Tests for acme.jws.JWS."""
def setUp(self):
self.privkey = jose.JWKRSA(key=RSA512_KEY)
self.privkey = KEY
self.pubkey = self.privkey.public_key()
self.nonce = jose.b64encode('Nonce')

View file

@ -3,6 +3,7 @@ import urlparse
from acme import challenges
from acme import fields
from acme import interfaces
from acme import jose
@ -117,7 +118,6 @@ class Identifier(jose.JSONObjectWithFields):
class Resource(jose.JSONObjectWithFields):
"""ACME Resource.
:ivar str uri: Location of the resource.
:ivar acme.messages.ResourceBody body: Resource body.
"""
@ -137,13 +137,15 @@ class ResourceBody(jose.JSONObjectWithFields):
"""ACME Resource Body."""
class Registration(ResourceBody):
class Registration(interfaces.ClientRequestableResource, ResourceBody):
"""Registration Resource Body.
:ivar acme.jose.jwk.JWK key: Public key.
:ivar tuple contact: Contact information following ACME spec
"""
resource_type = 'new-reg'
# 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)
@ -180,20 +182,9 @@ class Registration(ResourceBody):
"""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):
class RegistrationResource(interfaces.ClientRequestableResource,
ResourceWithURI):
"""Registration Resource.
:ivar acme.messages.Registration body:
@ -201,6 +192,7 @@ class RegistrationResource(ResourceWithURI):
:ivar str terms_of_service: URL for the CA TOS.
"""
resource_type = 'reg'
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)
@ -262,7 +254,7 @@ class ChallengeResource(Resource):
return self.body.uri # pylint: disable=no-member
class Authorization(ResourceBody):
class Authorization(interfaces.ClientRequestableResource, ResourceBody):
"""Authorization Resource Body.
:ivar acme.messages.Identifier identifier:
@ -275,6 +267,7 @@ class Authorization(ResourceBody):
:ivar datetime.datetime expires:
"""
resource_type = 'new-authz'
identifier = jose.Field('identifier', decoder=Identifier.from_json)
challenges = jose.Field('challenges', omitempty=True)
combinations = jose.Field('combinations', omitempty=True)
@ -308,7 +301,8 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri = jose.Field('new_cert_uri')
class CertificateRequest(jose.JSONObjectWithFields):
class CertificateRequest(interfaces.ClientRequestableResource,
jose.JSONObjectWithFields):
"""ACME new-cert request.
:ivar acme.jose.util.ComparableX509 csr:
@ -316,11 +310,13 @@ class CertificateRequest(jose.JSONObjectWithFields):
:ivar tuple authorizations: `tuple` of URIs (`str`)
"""
resource_type = 'new-cert'
csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr)
authorizations = jose.Field('authorizations', decoder=tuple)
class CertificateResource(ResourceWithURI):
class CertificateResource(interfaces.ClientRequestableResource,
ResourceWithURI):
"""Certificate Resource.
:ivar acme.jose.util.ComparableX509 body:
@ -329,17 +325,20 @@ class CertificateResource(ResourceWithURI):
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
"""
resource_type = 'cert'
cert_chain_uri = jose.Field('cert_chain_uri')
authzrs = jose.Field('authzrs')
class Revocation(jose.JSONObjectWithFields):
class Revocation(interfaces.ClientRequestableResource,
jose.JSONObjectWithFields):
"""Revocation message.
:ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in
`.ComparableX509`
"""
resource_type = 'revoke-cert'
certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)

View file

@ -1,30 +1,16 @@
"""Tests for acme.messages."""
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import mock
import OpenSSL
from acme import challenges
from acme import jose
from acme import test_util
CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'cert.der'))))
CSR = jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'csr.der'))))
KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
CERT = jose.ComparableX509(OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'cert.der'))))
CERT = test_util.load_cert('cert.der')
CSR = test_util.load_csr('csr.der')
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class ErrorTest(unittest.TestCase):
@ -122,6 +108,7 @@ class RegistrationTest(unittest.TestCase):
self.reg = Registration(
key=key, contact=contact, recovery_token=recovery_token,
agreement=agreement)
self.reg_none = Registration()
self.jobj_to = {
'contact': contact,
@ -146,12 +133,6 @@ class RegistrationTest(unittest.TestCase):
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())

View file

@ -1,18 +1,11 @@
"""Tests for acme.sig."""
import os
import pkg_resources
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from acme import jose
from acme import test_util
KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class SignatureTest(unittest.TestCase):

57
acme/acme/test_util.py Normal file
View file

@ -0,0 +1,57 @@
# Symlinked in letsencrypt/tests/test_util.py, casues duplicate-code
# warning that cannot be disabled locally.
"""Test utilities.
.. warning:: This module is not part of the public API.
"""
import os
import pkg_resources
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import OpenSSL
from acme import jose
def vector_path(*names):
"""Path to a test vector."""
return pkg_resources.resource_filename(
__name__, os.path.join('testdata', *names))
def load_vector(*names):
"""Load contents of a test vector."""
# luckily, resource_string opens file in binary mode
return pkg_resources.resource_string(
__name__, os.path.join('testdata', *names))
def _guess_loader(filename, loader_pem, loader_der):
_, ext = os.path.splitext(filename)
if ext.lower() == '.pem':
return loader_pem
elif ext.lower() == '.der':
return loader_der
else: # pragma: no cover
raise ValueError("Loader could not be recognized based on extension")
def load_cert(*names):
"""Load certificate."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return jose.ComparableX509(OpenSSL.crypto.load_certificate(
loader, load_vector(*names)))
def load_csr(*names):
"""Load certificate request."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
loader, load_vector(*names)))
def load_rsa_private_key(*names):
"""Load RSA private key."""
loader = _guess_loader(names[-1], serialization.load_pem_private_key,
serialization.load_der_private_key)
return jose.ComparableRSAKey(loader(
load_vector(*names), password=None, backend=default_backend()))

View file

@ -1,3 +1,7 @@
In order for acme.test_util._guess_loader to work properly, make sure
to use appropriate extension for vector filenames: .pem for PEM and
.der for DER.
The following command has been used to generate test keys:
for x in 256 512 1024; do openssl genrsa -out rsa${k}_key.pem $k; done

14
acme/acme/testdata/cert-san.pem vendored Normal file
View file

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx
ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM
IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4
YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG
A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix
KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS
BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR
7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c
+pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt
cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF
nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7
RDjyGMKy5ZgM2w==
-----END CERTIFICATE-----

13
acme/acme/testdata/cert.pem vendored Normal file
View file

@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx
ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM
IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4
YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG
A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix
KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS
BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR
7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c
+pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll
vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn
B/o=
-----END CERTIFICATE-----

10
acme/acme/testdata/csr-san.pem vendored Normal file
View file

@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN
AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t
MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy
tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A==
-----END CERTIFICATE REQUEST-----

10
acme/acme/testdata/csr.pem vendored Normal file
View file

@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw
EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy
c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG
9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f
p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN
AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB
AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G
n9XBE1N9W6HCIEut2d8wACg=
-----END CERTIFICATE REQUEST-----

14
acme/acme/testdata/dsa512_key.pem vendored Normal file
View file

@ -0,0 +1,14 @@
-----BEGIN DSA PARAMETERS-----
MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC
OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA
qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl
41pgNJpgu99YOYqPpS0g7A==
-----END DSA PARAMETERS-----
-----BEGIN DSA PRIVATE KEY-----
MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf
n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP
AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm
rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth
zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE
mNv063So6E+eYaIN
-----END DSA PRIVATE KEY-----

29
acme/setup.py Normal file
View file

@ -0,0 +1,29 @@
from setuptools import setup
from setuptools import find_packages
install_requires = [
'argparse',
# load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8)
'cryptography>=0.8',
'mock<1.1.0', # py26
'pyrfc3339',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
'PyOpenSSL',
'pytz',
'requests',
'werkzeug',
]
setup(
name='acme',
packages=find_packages(),
install_requires=install_requires,
entry_points={
'console_scripts': [
'jws = acme.jose.jws:CLI.run',
],
},
)

View file

@ -1,5 +0,0 @@
:mod:`letsencrypt.network`
--------------------------
.. automodule:: letsencrypt.network
:members:

View file

@ -36,6 +36,8 @@ with codecs.open(init_fn, encoding='utf8') as fd:
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath(os.path.join(here, '..')))
for pkg in 'acme', 'letsencrypt_apache', 'letsencrypt_nginx':
sys.path.insert(0, os.path.abspath(os.path.join(here, '..', pkg)))
# -- General configuration ------------------------------------------------

View file

@ -26,7 +26,7 @@ Install the development packages:
.. code-block:: shell
pip install -r requirements.txt -e .[dev,docs,testing]
pip install -r requirements.txt -e acme -e .[dev,docs,testing] -e letsencrypt_apache -e letsencrypt_nginx
.. note:: `-e` (short for `--editable`) turns on *editable mode* in
which any source code changes in the current working

View file

@ -108,7 +108,7 @@ 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 acme . letsencrypt_apache letsencrypt_nginx
.. warning:: Please do **not** use ``python setup.py install``. Please
do **not** attempt the installation commands as

View file

@ -26,7 +26,7 @@ key = jose.JWKRSA(key=rsa.generate_private_key(
backend=default_backend()))
acme = client.Client(NEW_REG_URL, key)
regr = acme.register(contact=())
regr = acme.register()
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
acme.update_registration(regr.update(
body=regr.body.update(agreement=regr.terms_of_service)))

View file

@ -1,233 +1,203 @@
"""Creates ACME accounts for server."""
import datetime
import hashlib
import logging
import os
import re
import socket
import configobj
from cryptography.hazmat.primitives import serialization
import pyrfc3339
import pytz
import zope.component
from acme import fields as acme_fields
from acme import jose
from acme import messages
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt.display import util as display_util
logger = logging.getLogger(__name__)
class Account(object):
class Account(object): # pylint: disable=too-few-public-methods
"""ACME protocol registration.
:ivar config: Client configuration object
:type config: :class:`~letsencrypt.interfaces.IConfig`
:ivar key: Account/Authorized Key
:type key: :class:`~letsencrypt.le_util.Key`
:ivar str email: Client's email address
:ivar str phone: Client's phone number
:ivar regr: Registration Resource
:type regr: :class:`~acme.messages.RegistrationResource`
:ivar .RegistrationResource regr: Registration Resource
:ivar .JWK key: Authorized Account Key
:ivar .Meta: Account metadata
:ivar str id: Globally unique account identifier.
"""
# Just make sure we don't get pwned
# Make sure that it also doesn't start with a period or have two consecutive
# periods <- this needs to be done in addition to the regex
EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$")
class Meta(jose.JSONObjectWithFields):
"""Account metadata
def __init__(self, config, key, email=None, phone=None, regr=None):
le_util.make_or_verify_dir(
config.accounts_dir, 0o700, os.geteuid())
self.key = key
self.config = config
if email is not None and self.safe_email(email):
self.email = email
else:
self.email = None
self.phone = phone
:ivar datetime.datetime creation_dt: Creation date and time (UTC).
:ivar str creation_host: FQDN of host, where account has been created.
self.regr = regr
@property
def uri(self):
"""URI link for new registrations."""
if self.regr is not None:
return self.regr.uri
else:
return None
@property
def new_authzr_uri(self): # pylint: disable=missing-docstring
if self.regr is not None:
return self.regr.new_authzr_uri
else:
return None
@property
def terms_of_service(self): # pylint: disable=missing-docstring
if self.regr is not None:
return self.regr.terms_of_service
else:
return None
@property
def recovery_token(self): # pylint: disable=missing-docstring
if self.regr is not None and self.regr.body is not None:
return self.regr.body.recovery_token
else:
return None
def save(self):
"""Save account to disk."""
le_util.make_or_verify_dir(
self.config.accounts_dir, 0o700, os.geteuid())
acc_config = configobj.ConfigObj()
acc_config.filename = os.path.join(
self.config.accounts_dir, self._get_config_filename(self.email))
acc_config.initial_comment = [
"DO NOT EDIT THIS FILE",
"Account information for %s under %s" % (
self._get_config_filename(self.email), self.config.server),
]
acc_config["key"] = self.key.file
acc_config["phone"] = self.phone
if self.regr is not None:
acc_config["RegistrationResource"] = {}
acc_config["RegistrationResource"]["uri"] = self.uri
acc_config["RegistrationResource"]["new_authzr_uri"] = (
self.new_authzr_uri)
acc_config["RegistrationResource"]["terms_of_service"] = (
self.terms_of_service)
regr_dict = self.regr.body.to_json()
acc_config["RegistrationResource"]["body"] = regr_dict
acc_config.write()
@classmethod
def _get_config_filename(cls, email):
return email if email is not None and email else "default"
@classmethod
def from_existing_account(cls, config, email=None):
"""Populate an account from an existing email."""
config_fp = os.path.join(
config.accounts_dir, cls._get_config_filename(email))
return cls._from_config_fp(config, config_fp)
@classmethod
def _from_config_fp(cls, config, config_fp):
try:
acc_config = configobj.ConfigObj(
infile=config_fp, file_error=True, create_empty=False)
except IOError:
raise errors.Error(
"Account for %s does not exist" % os.path.basename(config_fp))
if os.path.basename(config_fp) != "default":
email = os.path.basename(config_fp)
else:
email = None
phone = acc_config["phone"] if acc_config["phone"] != "None" else None
with open(acc_config["key"]) as key_file:
key = le_util.Key(acc_config["key"], key_file.read())
if "RegistrationResource" in acc_config:
acc_config_rr = acc_config["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=messages.Registration.from_json(acc_config_rr["body"]))
else:
regr = None
return cls(config, key, email, phone, regr)
@classmethod
def get_accounts(cls, config):
"""Return all current accounts.
:param config: Configuration
:type config: :class:`letsencrypt.interfaces.IConfig`
.. note:: ``creation_dt`` and ``creation_host`` are useful in
cross-machine migration scenarios.
"""
creation_dt = acme_fields.RFC3339Field("creation_dt")
creation_host = jose.Field("creation_host")
def __init__(self, regr, key, meta=None):
self.key = key
self.regr = regr
self.meta = self.Meta(
# pyrfc3339 drops microseconds, make sure __eq__ is sane
creation_dt=datetime.datetime.now(
tz=pytz.UTC).replace(microsecond=0),
creation_host=socket.getfqdn()) if meta is None else meta
self.id = hashlib.md5( # pylint: disable=invalid-name
self.key.key.public_key().public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo)
).hexdigest()
# Implementation note: Email? Multiple accounts can have the
# same email address. Registration URI? Assigned by the
# server, not guaranteed to be stable over time, nor
# cannonical URI can be generated. ACME protocol doesn't allow
# account key (and thus its fingerprint) to be updated...
@property
def slug(self):
"""Short account identification string, useful for UI."""
return "{1}@{0} ({2})".format(pyrfc3339.generate(
self.meta.creation_dt), self.meta.creation_host, self.id[:4])
def __repr__(self):
return "<{0}({1})>".format(self.__class__.__name__, self.id)
def __eq__(self, other):
return (isinstance(other, self.__class__) and
self.key == other.key and self.regr == other.regr and
self.meta == other.meta)
def report_new_account(acc, config):
"""Informs the user about their new Let's Encrypt account."""
reporter = zope.component.queryUtility(interfaces.IReporter)
if reporter is None:
return
reporter.add_message(
"Your account credentials have been saved in your Let's Encrypt "
"configuration directory at {0}. You should make a secure backup "
"of this folder now. This configuration directory will also "
"contain certificates and private keys obtained by Let's Encrypt "
"so making regular backups of this folder is ideal.".format(
config.config_dir),
reporter.MEDIUM_PRIORITY, True)
assert acc.regr.body.recovery_token is not None
recovery_msg = ("If you lose your account credentials, you can recover "
"them using the token \"{0}\". You must write that down "
"and put it in a safe place.".format(
acc.regr.body.recovery_token))
if acc.regr.body.emails:
recovery_msg += (" Another recovery method will be e-mails sent to "
"{0}.".format(", ".join(acc.regr.body.emails)))
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
class AccountMemoryStorage(interfaces.AccountStorage):
"""In-memory account strage."""
def __init__(self, initial_accounts=None):
self.accounts = initial_accounts if initial_accounts is not None else {}
def find_all(self):
return self.accounts.values()
def save(self, account):
if account.id in self.accounts:
logger.debug("Overwriting account: %s", account.id)
self.accounts[account.id] = account
def load(self, account_id):
try:
filenames = os.listdir(config.accounts_dir)
return self.accounts[account_id]
except KeyError:
raise errors.AccountNotFound(account_id)
class AccountFileStorage(interfaces.AccountStorage):
"""Accounts file storage.
:ivar .IConfig config: Client configuration
"""
def __init__(self, config):
le_util.make_or_verify_dir(config.accounts_dir, 0o700, os.geteuid())
self.config = config
def _account_dir_path(self, account_id):
return os.path.join(self.config.accounts_dir, account_id)
@classmethod
def _regr_path(cls, account_dir_path):
return os.path.join(account_dir_path, "regr.json")
@classmethod
def _key_path(cls, account_dir_path):
return os.path.join(account_dir_path, "private_key.json")
@classmethod
def _metadata_path(cls, account_dir_path):
return os.path.join(account_dir_path, "meta.json")
def find_all(self):
try:
candidates = os.listdir(self.config.accounts_dir)
except OSError:
return []
accounts = []
for name in filenames:
# Not some directory ie. keys
config_fp = os.path.join(config.accounts_dir, name)
if os.path.isfile(config_fp):
accounts.append(cls._from_config_fp(config, config_fp))
for account_id in candidates:
try:
accounts.append(self.load(account_id))
except errors.AccountStorageError:
logger.debug("Account loading problem", exc_info=True)
return accounts
@classmethod
def from_prompts(cls, config):
"""Generate an account from prompted user input.
def load(self, account_id):
account_dir_path = self._account_dir_path(account_id)
if not os.path.isdir(account_dir_path):
raise errors.AccountNotFound(
"Account at %s does not exist" % account_dir_path)
:param config: Configuration
:type config: :class:`letsencrypt.interfaces.IConfig`
try:
with open(self._regr_path(account_dir_path)) as regr_file:
regr = messages.RegistrationResource.json_loads(regr_file.read())
with open(self._key_path(account_dir_path)) as key_file:
key = jose.JWK.json_loads(key_file.read())
with open(self._metadata_path(account_dir_path)) as metadata_file:
meta = Account.Meta.json_loads(metadata_file.read())
except IOError as error:
raise errors.AccountStorageError(error)
:returns: Account or None
:rtype: :class:`letsencrypt.account.Account`
acc = Account(regr, key, meta)
if acc.id != account_id:
raise errors.AccountStorageError(
"Account ids mismatch (expected: {0}, found: {1}".format(
account_id, acc.id))
return acc
"""
while True:
code, email = zope.component.getUtility(interfaces.IDisplay).input(
"Enter email address")
if code == display_util.OK:
try:
return cls.from_email(config, email)
except errors.Error:
continue
else:
return None
@classmethod
def from_email(cls, config, email):
"""Generate a new account from an email address.
:param config: Configuration
:type config: :class:`letsencrypt.interfaces.IConfig`
:param str email: Email address
:raises .errors.Error: If invalid email address is given.
"""
if not email or cls.safe_email(email):
email = email if email else None
le_util.make_or_verify_dir(
config.account_keys_dir, 0o700, os.geteuid())
key = crypto_util.init_save_key(
config.rsa_key_size, config.account_keys_dir,
cls._get_config_filename(email))
return cls(config, key, email)
raise errors.Error("Invalid email address.")
@classmethod
def safe_email(cls, email):
"""Scrub email address before using it."""
if cls.EMAIL_REGEX.match(email):
return not email.startswith(".") and ".." not in email
else:
logger.warn("Invalid email address: %s.", email)
return False
def save(self, account):
account_dir_path = self._account_dir_path(account.id)
le_util.make_or_verify_dir(account_dir_path, 0o700, os.geteuid())
try:
with open(self._regr_path(account_dir_path), "w") as regr_file:
regr_file.write(account.regr.json_dumps())
with le_util.safe_open(self._key_path(account_dir_path),
"w", chmod=0o400) as key_file:
key_file.write(account.key.json_dumps())
with open(self._metadata_path(account_dir_path), "w") as metadata_file:
metadata_file.write(account.meta.json_dumps())
except IOError as error:
raise errors.AccountStorageError(error)

View file

@ -57,7 +57,7 @@ class DVSNI(AnnotatedChallenge):
"""
response = challenges.DVSNIResponse(s=s)
cert_pem = crypto_util.make_ss_cert(self.key.pem, [
cert_pem = crypto_util.make_ss_cert(self.key, [
self.domain, self.nonce_domain, response.z_domain(self.challb)])
return cert_pem, response

View file

@ -28,9 +28,7 @@ class AuthHandler(object):
:class:`~acme.challenges.ContinuityChallenge` types
:type cont_auth: :class:`letsencrypt.interfaces.IAuthenticator`
:ivar network: Network object for sending and receiving authorization
messages
:type network: :class:`letsencrypt.network.Network`
:ivar acme.client.Client acme: ACME client API.
:ivar account: Client's Account
:type account: :class:`letsencrypt.account.Account`
@ -43,10 +41,10 @@ class AuthHandler(object):
form of :class:`letsencrypt.achallenges.AnnotatedChallenge`
"""
def __init__(self, dv_auth, cont_auth, network, account):
def __init__(self, dv_auth, cont_auth, acme, account):
self.dv_auth = dv_auth
self.cont_auth = cont_auth
self.network = network
self.acme = acme
self.account = account
self.authzr = dict()
@ -71,8 +69,8 @@ class AuthHandler(object):
"""
for domain in domains:
self.authzr[domain] = self.network.request_domain_challenges(
domain, self.account.new_authzr_uri)
self.authzr[domain] = self.acme.request_domain_challenges(
domain, self.account.regr.new_authzr_uri)
self._choose_challenges(domains)
@ -157,7 +155,7 @@ class AuthHandler(object):
for achall, resp in itertools.izip(achalls, resps):
# Don't send challenges for None and False authenticator responses
if resp:
self.network.answer_challenge(achall.challb, resp)
self.acme.answer_challenge(achall.challb, resp)
# TODO: answer_challenge returns challr, with URI,
# that can be used in _find_updated_challr
# comparisons...
@ -211,7 +209,7 @@ class AuthHandler(object):
completed = []
failed = []
self.authzr[domain], _ = self.network.poll(self.authzr[domain])
self.authzr[domain], _ = self.acme.poll(self.authzr[domain])
if self.authzr[domain].body.status == messages.STATUS_VALID:
return achalls, []

View file

@ -6,6 +6,7 @@ import functools
import logging
import logging.handlers
import os
import pkg_resources
import sys
import time
import traceback
@ -76,23 +77,6 @@ More detailed help:
"""
def _account_init(args, config):
# Prepare for init of Client
if args.email is None:
return client.determine_account(config)
else:
try:
# The way to get the default would be args.email = ""
# First try existing account
return account.Account.from_existing_account(config, args.email)
except errors.Error:
try:
# Try to make an account based on the email address
return account.Account.from_email(config, args.email)
except errors.Error:
return None
def _find_domains(args, installer):
if args.domains is None:
domains = display_ops.choose_names(installer)
@ -106,30 +90,77 @@ def _find_domains(args, installer):
return domains
def _init_acme(config, acc, authenticator, installer):
acme = client.Client(config, acc, authenticator, installer)
def _determine_account(args, config):
"""Determine which account to use.
# Validate the key and csr
client.validate_key_csr(acc.key)
In order to make the renewer (configuration de/serialization) happy,
if ``args.account`` is ``None``, it will be updated based on the
user input. Same for ``args.email``.
:param argparse.Namespace args: CLI arguments
:param letsencrypt.interface.IConfig config: Configuration object
:param .AccountStorage account_storage: Account storage.
:returns: Account and optionally ACME client API (biproduct of new
registration).
:rtype: `tuple` of `letsencrypt.account.Account` and
`acme.client.Client`
"""
account_storage = account.AccountFileStorage(config)
acme = None
if args.account is not None:
acc = account_storage.load(args.account)
else:
accounts = account_storage.find_all()
if len(accounts) > 1:
acc = display_ops.choose_account(accounts)
elif len(accounts) == 1:
acc = accounts[0]
else: # no account registered yet
if args.email is None:
args.email = display_ops.get_email()
if not args.email: # get_email might return ""
args.email = None
def _tos_cb(regr):
if args.tos:
return True
msg = ("Please read the Terms of Service at {0}. You "
"must agree in order to register with the ACME "
"server at {1}".format(
regr.terms_of_service, config.server))
return zope.component.getUtility(interfaces.IDisplay).yesno(
msg, "Agree", "Cancel")
if authenticator is not None:
if acc.regr is None:
try:
acme.register()
acc, acme = client.register(
config, account_storage, tos_cb=_tos_cb)
except errors.Error as error:
logger.debug(error)
raise errors.Error("Unable to register an account with ACME "
"server")
logger.debug(error, exc_info=True)
raise errors.Error(
"Unable to register an account with ACME server")
return acme
args.account = acc.id
return acc, acme
def _init_le_client(args, config, authenticator, installer):
if authenticator is not None:
# if authenticator was given, then we will need account...
acc, acme = _determine_account(args, config)
logger.debug("Picked account: %r", acc)
# XXX
#crypto_util.validate_key_csr(acc.key)
else:
acc, acme = None, None
return client.Client(config, acc, authenticator, installer, acme=acme)
def run(args, config, plugins):
"""Obtain a certificate and install."""
acc = _account_init(args, config)
if acc is None:
return None
if args.configurator is not None and (args.installer is not None or
args.authenticator is not None):
return ("Either --configurator or --authenticator/--installer"
@ -150,14 +181,15 @@ def run(args, config, plugins):
return "Configurator could not be determined"
domains = _find_domains(args, installer)
# TODO: Handle errors from _init_acme?
acme = _init_acme(config, acc, authenticator, installer)
lineage = acme.obtain_and_enroll_certificate(
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(args, config, authenticator, installer)
lineage = le_client.obtain_and_enroll_certificate(
domains, authenticator, installer, plugins)
if not lineage:
return "Certificate could not be obtained"
acme.deploy_certificate(domains, lineage.privkey, lineage.cert, lineage.chain)
acme.enhance_config(domains, args.redirect)
le_client.deploy_certificate(
domains, lineage.privkey, lineage.cert, lineage.chain)
le_client.enhance_config(domains, args.redirect)
def auth(args, config, plugins):
@ -169,10 +201,6 @@ def auth(args, config, plugins):
# supplied, check if CSR matches given domains?
return "--domains and --csr are mutually exclusive"
acc = _account_init(args, config)
if acc is None:
return None
authenticator = display_ops.pick_authenticator(
config, args.authenticator, plugins)
if authenticator is None:
@ -183,16 +211,17 @@ def auth(args, config, plugins):
else:
installer = None
# TODO: Handle errors from _init_acme?
acme = _init_acme(config, acc, authenticator, installer)
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(args, config, authenticator, installer)
if args.csr is not None:
certr, chain = acme.obtain_certificate_from_csr(le_util.CSR(
certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR(
file=args.csr[0], data=args.csr[1], form="der"))
acme.save_certificate(certr, chain, args.cert_path, args.chain_path)
le_client.save_certificate(
certr, chain, args.cert_path, args.chain_path)
else:
domains = _find_domains(args, installer)
if not acme.obtain_and_enroll_certificate(
if not le_client.obtain_and_enroll_certificate(
domains, authenticator, installer, plugins):
return "Certificate could not be obtained"
@ -200,18 +229,16 @@ def auth(args, config, plugins):
def install(args, config, plugins):
"""Install a previously obtained cert in a server."""
# XXX: Update for renewer/RenewableCert
acc = _account_init(args, config)
if acc is None:
return None
installer = display_ops.pick_installer(config, args.installer, plugins)
if installer is None:
return "Installer could not be determined"
domains = _find_domains(args, installer)
acme = _init_acme(config, acc, authenticator=None, installer=installer)
le_client = _init_le_client(
args, config, authenticator=None, installer=installer)
assert args.cert_path is not None # required=True in the subparser
acme.deploy_certificate(domains, args.key_path, args.cert_path, args.chain_path)
acme.enhance_config(domains, args.redirect)
le_client.deploy_certificate(
domains, args.key_path, args.cert_path, args.chain_path)
le_client.enhance_config(domains, args.redirect)
def revoke(args, unused_config, unused_plugins):
@ -468,8 +495,14 @@ def create_parser(plugins, args):
"automation", "--no-confirm", dest="no_confirm", action="store_true",
help="Turn off confirmation screens, currently used for --revoke")
helpful.add(
"automation", "--agree-eula", "-e", dest="tos", action="store_true",
"automation", "--agree-eula", dest="eula", action="store_true",
help="Agree to the Let's Encrypt Developer Preview EULA")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
help="Agree to the Let's Encrypt Subscriber Agreement")
helpful.add(
"automation", "--account", metavar="ACCOUNT_ID",
help="Account ID to use")
helpful.add_group(
"testing", description="The following flags are meant for "
@ -724,6 +757,13 @@ def main(cli_args=sys.argv[1:]):
zope.component.provideUtility(report)
atexit.register(report.atexit_print_messages)
# TODO: remove developer EULA prompt for the launch
if not config.eula:
eula = pkg_resources.resource_string("letsencrypt", "EULA")
if not zope.component.getUtility(interfaces.IDisplay).yesno(
eula, "Agree", "Cancel"):
raise errors.Error("Must agree to TOS")
if not os.geteuid() == 0:
logger.warning(
"Root (sudo) is required to run most of letsencrypt functionality.")

View file

@ -1,13 +1,15 @@
"""ACME protocol client class and helper functions."""
"""Let's Encrypt client API."""
import logging
import os
import pkg_resources
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import rsa
import OpenSSL
import zope.component
from acme import client as acme_client
from acme import jose
from acme.jose import jwk
from acme import messages
from letsencrypt import account
from letsencrypt import auth_handler
@ -18,7 +20,6 @@ from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt import network
from letsencrypt import reverter
from letsencrypt import revoker
from letsencrypt import storage
@ -30,48 +31,107 @@ from letsencrypt.display import enhancements
logger = logging.getLogger(__name__)
def _acme_from_config_key(config, key):
# TODO: Allow for other alg types besides RS256
return acme_client.Client(new_reg_uri=config.server, key=key,
verify_ssl=(not config.no_verify_ssl))
def register(config, account_storage, tos_cb=None):
"""Register new account with an ACME CA.
This function takes care of generating fresh private key,
registering the account, optionally accepting CA Terms of Service
and finally saving the account. It should be called prior to
initialization of `Client`, unless account has already been created.
:param .IConfig config: Client configuration.
:param .AccountStorage account_storage: Account storage where newly
registered account will be saved to. Save happens only after TOS
acceptance step, so any account private keys or
`.RegistrationResource` will not be persisted if `tos_cb`
returns ``False``.
:param tos_cb: If ACME CA requires the user to accept a Terms of
Service before registering account, client action is
necessary. For example, a CLI tool would prompt the user
acceptance. `tos_cb` must be a callable that should accept
`.RegistrationResource` and return a `bool`: ``True`` iff the
Terms of Service present in the contained
`.Registration.terms_of_service` is accepted by the client, and
``False`` otherwise. ``tos_cb`` will be called only if the
client acction is necessary, i.e. when ``terms_of_service is not
None``. This argument is optional, if not supplied it will
default to automatic acceptance!
:raises letsencrypt.errors.Error: In case of any client problems, in
particular registration failure, or unaccepted Terms of Service.
:raises acme.errors.Error: In case of any protocol problems.
:returns: Newly registered and saved account, as well as protocol
API handle (should be used in `Client` initialization).
:rtype: `tuple` of `.Account` and `acme.client.Client`
"""
# Log non-standard actions, potentially wrong API calls
if account_storage.find_all():
logger.info("There are already existing accounts for %s", config.server)
if config.email is None:
logger.warn("Registering without email!")
# Each new registration shall use a fresh new key
key = jose.JWKRSA(key=jose.ComparableRSAKey(
rsa.generate_private_key(
public_exponent=65537,
key_size=config.rsa_key_size,
backend=default_backend())))
acme = _acme_from_config_key(config, key)
# TODO: add phone?
regr = acme.register(messages.Registration.from_data(email=config.email))
if regr.terms_of_service is not None:
if tos_cb is not None and not tos_cb(regr):
raise errors.Error(
"Registration cannot proceed without accepting "
"Terms of Service.")
regr = acme.agree_to_tos(regr)
acc = account.Account(regr, key)
account.report_new_account(acc, config)
account_storage.save(acc)
return acc, acme
class Client(object):
"""ACME protocol client.
:ivar network: Network object for sending and receiving messages
:type network: :class:`letsencrypt.network.Network`
:ivar account: Account object used for registration
:type account: :class:`letsencrypt.account.Account`
:ivar auth_handler: Object that supports the IAuthenticator interface.
auth_handler contains both a dv_authenticator and a
continuity_authenticator
:type auth_handler: :class:`letsencrypt.auth_handler.AuthHandler`
:ivar installer: Object supporting the IInstaller interface.
:type installer: :class:`letsencrypt.interfaces.IInstaller`
:ivar config: Configuration.
:type config: :class:`~letsencrypt.interfaces.IConfig`
:ivar .IConfig config: Client configuration.
:ivar .Account account: Account registered with `register`.
:ivar .AuthHandler auth_handler: Authorizations handler that will
dispatch DV and Continuity challenges to appropriate
authenticators (providing `.IAuthenticator` interface).
:ivar .IInstaller installer: Installer.
:ivar acme.client.Client acme: Optional ACME client API handle.
You might already have one from `register`.
"""
def __init__(self, config, account_, dv_auth, installer):
def __init__(self, config, account_, dv_auth, installer, acme=None):
"""Initialize a client.
:param dv_auth: IAuthenticator that can solve the
:const:`letsencrypt.constants.DV_CHALLENGES`.
The :meth:`~letsencrypt.interfaces.IAuthenticator.prepare`
must have already been run.
:type dv_auth: :class:`letsencrypt.interfaces.IAuthenticator`
:param .IAuthenticator dv_auth: Prepared (`.IAuthenticator.prepare`)
authenticator that can solve the `.constants.DV_CHALLENGES`.
"""
self.config = config
self.account = account_
self.installer = installer
# TODO: Allow for other alg types besides RS256
self.network = network.Network(
config.server, jwk.JWKRSA.load(self.account.key.pem),
verify_ssl=(not config.no_verify_ssl))
self.config = config
# Initialize ACME if account is provided
if acme is None and self.account is not None:
acme = _acme_from_config_key(config, self.account.key)
self.acme = acme
# TODO: Check if self.config.enroll_autorenew is None. If
# so, set it based to the default: figure out if dv_auth is
@ -81,53 +141,10 @@ class Client(object):
cont_auth = continuity_auth.ContinuityAuthenticator(config,
installer)
self.auth_handler = auth_handler.AuthHandler(
dv_auth, cont_auth, self.network, self.account)
dv_auth, cont_auth, self.acme, self.account)
else:
self.auth_handler = None
def register(self):
"""New Registration with the ACME server."""
self.account = self.network.register_from_account(self.account)
if self.account.terms_of_service is not None:
if not self.config.tos:
# TODO: Replace with self.account.terms_of_service
eula = pkg_resources.resource_string("letsencrypt", "EULA")
agree = zope.component.getUtility(interfaces.IDisplay).yesno(
eula, "Agree", "Cancel")
else:
agree = True
if agree:
self.account.regr = self.network.agree_to_tos(self.account.regr)
else:
# What is the proper response here...
raise errors.Error("Must agree to TOS")
self.account.save()
self._report_new_account()
def _report_new_account(self):
"""Informs the user about their new Let's Encrypt account."""
reporter = zope.component.getUtility(interfaces.IReporter)
reporter.add_message(
"Your account credentials have been saved in your Let's Encrypt "
"configuration directory at {0}. You should make a secure backup "
"of this folder now. This configuration directory will also "
"contain certificates and private keys obtained by Let's Encrypt "
"so making regular backups of this folder is ideal.".format(
self.config.config_dir),
reporter.MEDIUM_PRIORITY, True)
assert self.account.recovery_token is not None
recovery_msg = ("If you lose your account credentials, you can recover "
"them using the token \"{0}\". You must write that down "
"and put it in a safe place.".format(
self.account.recovery_token))
if self.account.email is not None:
recovery_msg += (" Another recovery method will be e-mails sent to "
"{0}.".format(self.account.email))
reporter.add_message(recovery_msg, reporter.HIGH_PRIORITY, True)
def _obtain_certificate(self, domains, csr):
"""Obtain certificate.
@ -155,11 +172,11 @@ class Client(object):
logger.debug("CSR: %s, domains: %s", csr, domains)
authzr = self.auth_handler.get_authorizations(domains)
certr = self.network.request_issuance(
certr = self.acme.request_issuance(
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, csr.data)),
authzr)
return certr, self.network.fetch_chain(certr)
return certr, self.acme.fetch_chain(certr)
def obtain_certificate_from_csr(self, csr):
"""Obtain certficiate from CSR.
@ -451,28 +468,6 @@ def validate_key_csr(privkey, csr=None):
raise errors.Error("The key and CSR do not match")
def determine_account(config):
"""Determine which account to use.
Will create an account if necessary.
:param config: Configuration object
:type config: :class:`letsencrypt.interfaces.IConfig`
:returns: Account
:rtype: :class:`letsencrypt.account.Account`
"""
accounts = account.Account.get_accounts(config)
if len(accounts) == 1:
return accounts[0]
elif len(accounts) > 1:
return display_ops.choose_account(accounts)
return account.Account.from_prompts(config)
def rollback(default_installer, checkpoints, config, plugins):
"""Revert configuration the specified number of checkpoints.

View file

@ -18,7 +18,6 @@ class NamespaceConfig(object):
paths defined in :py:mod:`letsencrypt.constants`:
- `accounts_dir`
- `account_keys_dir`
- `cert_dir`
- `cert_key_backup`
- `in_progress_dir`
@ -51,10 +50,6 @@ class NamespaceConfig(object):
return os.path.join(
self.namespace.config_dir, constants.ACCOUNTS_DIR, self.server_path)
@property
def account_keys_dir(self): #pylint: disable=missing-docstring
return os.path.join(self.accounts_dir, constants.ACCOUNT_KEYS_DIR)
@property
def backup_dir(self): # pylint: disable=missing-docstring
return os.path.join(self.namespace.work_dir, constants.BACKUP_DIR)

View file

@ -65,9 +65,6 @@ CONFIG_DIRS_MODE = 0o755
ACCOUNTS_DIR = "accounts"
"""Directory where all accounts are saved."""
ACCOUNT_KEYS_DIR = "keys"
"""Directory where account keys are saved. Relative to `ACCOUNTS_DIR`."""
BACKUP_DIR = "backups"
"""Directory (relative to `IConfig.work_dir`) where backups are kept."""

View file

@ -8,8 +8,11 @@ import datetime
import logging
import os
from cryptography.hazmat.primitives import serialization
import OpenSSL
from acme import jose
from letsencrypt import errors
from letsencrypt import le_util
@ -212,15 +215,21 @@ def pyopenssl_load_certificate(data):
return _pyopenssl_load(data, OpenSSL.crypto.load_certificate)
def make_ss_cert(key_str, domains, not_before=None,
def make_ss_cert(key, domains, not_before=None,
validity=(7 * 24 * 60 * 60)):
"""Returns new self-signed cert in PEM form.
Uses key_str and contains all domains.
Uses key and contains all domains.
"""
if isinstance(key, jose.JWK):
key = key.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption())
assert domains, "Must provide one or more hostnames for the cert."
pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key_str)
pkey = OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, key)
cert = OpenSSL.crypto.X509()
cert.set_serial_number(1337)
cert.set_version(2)

View file

@ -5,6 +5,7 @@ import os
import zope.component
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt.display import util as display_util
@ -112,6 +113,24 @@ def pick_configurator(
(interfaces.IAuthenticator, interfaces.IInstaller))
def get_email():
"""Prompt for valid email address.
:returns: Email or ``None`` if cancelled by user.
:rtype: str
"""
while True:
code, email = zope.component.getUtility(interfaces.IDisplay).input(
"Enter email address")
if code == display_util.OK:
if le_util.safe_email(email):
return email
else:
return None
def choose_account(accounts):
"""Choose an account.
@ -120,11 +139,7 @@ def choose_account(accounts):
"""
# Note this will get more complicated once we start recording authorizations
labels = [
"%s | %s" % (acc.email.ljust(display_util.WIDTH - 39),
acc.phone if acc.phone is not None else "")
for acc in accounts
]
labels = [acc.slug for acc in accounts]
code, index = util(interfaces.IDisplay).menu(
"Please choose an account", labels)

View file

@ -5,6 +5,14 @@ class Error(Exception):
"""Generic Let's Encrypt client error."""
class AccountStorageError(Error):
"""Generic `.AccountStorage` error."""
class AccountNotFound(AccountStorageError):
"""Account not found error."""
class ReverterError(Error):
"""Let's Encrypt Reverter error."""

View file

@ -1,10 +1,46 @@
"""Let's Encrypt client interfaces."""
import abc
import zope.interface
# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class
# pylint: disable=too-few-public-methods
class AccountStorage(object):
"""Accounts storage interface."""
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def find_all(self): # pragma: no cover
"""Find all accounts.
:returns: All found accounts.
:rtype: list
"""
raise NotImplementedError()
@abc.abstractmethod
def load(self, account_id): # pragma: no cover
"""Load an account by its id.
:raises .AccountNotFound: if account could not be found
:raises .AccountStorageError: if account could not be loaded
"""
raise NotImplementedError()
@abc.abstractmethod
def save(self, account): # pragma: no cover
"""Save account.
:raises .AccountStorageError: if account could not be saved
"""
raise NotImplementedError()
class IPluginFactory(zope.interface.Interface):
"""IPlugin factory.
@ -160,8 +196,6 @@ class IConfig(zope.interface.Interface):
accounts_dir = zope.interface.Attribute(
"Directory where all account information is stored.")
account_keys_dir = zope.interface.Attribute(
"Directory where all account keys are stored.")
backup_dir = zope.interface.Attribute("Configuration backups directory.")
cert_dir = zope.interface.Attribute(
"Directory where newly generated Certificate Signing Requests "

View file

@ -1,12 +1,17 @@
"""Utilities for all Let's Encrypt."""
import collections
import errno
import logging
import os
import re
import stat
from letsencrypt import errors
logger = logging.getLogger(__name__)
Key = collections.namedtuple("Key", "file pem")
# Note: form is the type of data, "pem" or "der"
CSR = collections.namedtuple("CSR", "file data form")
@ -53,16 +58,30 @@ def check_permissions(filepath, mode, uid=0):
return stat.S_IMODE(file_stat.st_mode) == mode and file_stat.st_uid == uid
def _safely_attempt_open(fname, mode):
file_d = os.open(fname, os.O_CREAT | os.O_EXCL | os.O_RDWR, mode)
return os.fdopen(file_d, "w"), fname
def safe_open(path, mode="w", chmod=None, buffering=None):
"""Safely open a file.
:param str path: Path to a file.
:param str mode: Same os `mode` for `open`.
:param int chmod: Same as `mode` for `os.open`, uses Python defaults
if ``None``.
:param int buffering: Same as `bufsize` for `os.fdopen`, uses Python
defaults if ``None``.
"""
# pylint: disable=star-args
open_args = () if chmod is None else (chmod,)
fdopen_args = () if buffering is None else (buffering,)
return os.fdopen(
os.open(path, os.O_CREAT | os.O_EXCL | os.O_RDWR, *open_args),
mode, *fdopen_args)
def _unique_file(path, filename_pat, count, mode):
while True:
current_path = os.path.join(path, filename_pat(count))
try:
return _safely_attempt_open(
os.path.join(path, filename_pat(count)), mode)
return safe_open(current_path, chmod=mode), current_path
except OSError as err:
# "File exists," is okay, try a different name.
if err.errno != errno.EEXIST:
@ -100,9 +119,9 @@ def unique_lineage_name(path, filename, mode=0o777):
specified location.
"""
preferred_path = os.path.join(path, "%s.conf" % (filename))
try:
return _safely_attempt_open(
os.path.join(path, "%s.conf" % (filename)), mode=mode)
return safe_open(preferred_path, chmod=mode), preferred_path
except OSError as err:
if err.errno != errno.EEXIST:
raise
@ -118,3 +137,16 @@ def safely_remove(path):
except OSError as err:
if err.errno != errno.ENOENT:
raise
# Just make sure we don't get pwned... Make sure that it also doesn't
# start with a period or have two consecutive periods <- this needs to
# be done in addition to the regex
EMAIL_REGEX = re.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+$")
def safe_email(email):
"""Scrub email address before using it."""
if EMAIL_REGEX.match(email) is not None:
return not email.startswith(".") and ".." not in email
else:
logger.warn("Invalid email address: %s.", email)
return False

View file

@ -1,26 +0,0 @@
"""Networking for ACME protocol."""
from acme import client
class Network(client.Client):
"""ACME networking."""
def register_from_account(self, account):
"""Register with server.
.. todo:: this should probably not be a part of network...
: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

View file

@ -4,12 +4,14 @@ import pkg_resources
import shutil
import tempfile
from cryptography.hazmat.primitives import serialization
import zope.interface
from acme.jose import util as jose_util
from letsencrypt import constants
from letsencrypt import interfaces
from letsencrypt import le_util
def option_namespace(name):
@ -144,7 +146,7 @@ class Dvsni(object):
if idx is not None:
self.indices.append(idx)
def get_cert_file(self, achall):
def get_cert_path(self, achall):
"""Returns standardized name for challenge certificate.
:param achall: Annotated DVSNI challenge.
@ -157,19 +159,34 @@ class Dvsni(object):
return os.path.join(
self.configurator.config.work_dir, achall.nonce_domain + ".crt")
def get_key_path(self, achall):
"""Get standardized path to challenge key."""
return os.path.join(
self.configurator.config.work_dir, achall.nonce_domain + '.pem')
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)
cert_path = self.get_cert_path(achall)
key_path = self.get_key_path(achall)
# Register the path before you write out the file
self.configurator.reverter.register_file_creation(True, key_path)
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:
with open(cert_path, "wb") as cert_chall_fd:
cert_chall_fd.write(cert_pem)
# Write out challenge key
key_pem = achall.key.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption())
with le_util.safe_open(key_path, 'wb', chmod=0o400) as key_file:
key_file.write(key_pem)
return response

View file

@ -1,4 +1,5 @@
"""Tests for letsencrypt.plugins.common."""
import os
import pkg_resources
import unittest
@ -111,9 +112,9 @@ class DvsniTest(unittest.TestCase):
"""Tests for letsencrypt.plugins.common.DvsniTest."""
rsa256_file = pkg_resources.resource_filename(
"acme.jose", "testdata/rsa256_key.pem")
"letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem"))
rsa256_pem = pkg_resources.resource_string(
"acme.jose", "testdata/rsa256_key.pem")
"letsencrypt.tests", os.path.join("testdata", "rsa256_key.pem"))
auth_key = le_util.Key(rsa256_file, rsa256_pem)
achalls = [
@ -150,22 +151,28 @@ class DvsniTest(unittest.TestCase):
# 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()
mock_open, mock_safe_open = mock.mock_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"))
with mock.patch("letsencrypt.plugins.common.open",
mock_open, create=True):
with mock.patch("letsencrypt.plugins.common.le_util.safe_open",
mock_safe_open):
# 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")
# pylint: disable=no-member
mock_open.assert_called_once_with(self.sni.get_cert_path(achall), "wb")
mock_open.return_value.write.assert_called_once_with("pem")
mock_safe_open.assert_called_once_with(
self.sni.get_key_path(achall), "wb", chmod=0o400)
mock_safe_open.return_value.write.assert_called_once_with(
achall.key.key.private_bytes())
if __name__ == "__main__":

View file

@ -16,7 +16,11 @@ logger = logging.getLogger(__name__)
class PluginEntryPoint(object):
"""Plugin entry point."""
PREFIX_FREE_DISTRIBUTIONS = ["letsencrypt"]
PREFIX_FREE_DISTRIBUTIONS = [
"letsencrypt",
"letsencrypt-apache",
"letsencrypt-nginx",
]
"""Distributions for which prefix will be omitted."""
# this object is mutable, don't allow it to be hashed!

View file

@ -41,7 +41,7 @@ class ManualAuthenticatorTest(unittest.TestCase):
resp = challenges.SimpleHTTPResponse(tls=False, path='Zm9v')
self.assertEqual([resp], self.auth.perform(self.achalls))
mock_raw_input.assert_called_once()
self.assertEqual(1, mock_raw_input.call_count)
mock_verify.assert_called_with(self.achalls[0].challb, "foo.com", 4430)
message = mock_stdout.write.mock_calls[0][1][0]

View file

@ -6,6 +6,7 @@ import socket
import sys
import time
from cryptography.hazmat.primitives import serialization
import OpenSSL
import zope.component
import zope.interface
@ -214,7 +215,10 @@ class StandaloneAuthenticator(common.Plugin):
# Signal that we've successfully bound TCP port
os.kill(self.parent_pid, signal.SIGIO)
self.private_key = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, key.pem)
OpenSSL.crypto.FILETYPE_PEM, key.key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption()))
while True:
self.connection, _ = self.sock.accept()

View file

@ -6,21 +6,27 @@ import signal
import socket
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import mock
import OpenSSL
from acme import challenges
from acme import jose
from letsencrypt import achallenges
from letsencrypt import le_util
from letsencrypt.tests import acme_util
KEY = le_util.Key("foo", pkg_resources.resource_string(
"acme.jose", os.path.join("testdata", "rsa512_key.pem")))
KEY_PATH = pkg_resources.resource_filename(
"letsencrypt.tests", os.path.join("testdata", "rsa512_key.pem"))
KEY_DATA = pkg_resources.resource_string(
"letsencrypt.tests", os.path.join("testdata", "rsa512_key.pem"))
KEY = jose.JWKRSA(key=jose.ComparableRSAKey(serialization.load_pem_private_key(
KEY_DATA, password=None, backend=default_backend())))
PRIVATE_KEY = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, KEY.pem)
OpenSSL.crypto.FILETYPE_PEM, KEY_DATA)
CONFIG = mock.Mock(dvsni_port=5001)

View file

@ -15,6 +15,7 @@ import configobj
import OpenSSL
import zope.component
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import cli
from letsencrypt import client
@ -76,15 +77,13 @@ def renew(cert, old_version):
authenticator = authenticator.init(config)
authenticator.prepare()
account = client.determine_account(config)
# TODO: are there other ways to get the right account object, e.g.
# based on the email parameter that might be present in
# renewalparams?
acc = account.AccountFileStorage(config).load(
account_id=renewalparams["account"])
our_client = client.Client(config, account, authenticator, None)
le_client = client.Client(config, acc, authenticator, None)
with open(cert.version("cert", old_version)) as f:
sans = crypto_util.get_sans_from_cert(f.read())
new_certr, new_chain, new_key, _ = our_client.obtain_certificate(sans)
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(sans)
if new_chain is not None:
# XXX: Assumes that there was no key change. We need logic
# for figuring out whether there was or not. Probably

View file

@ -15,12 +15,12 @@ import tempfile
import OpenSSL
from acme import client as acme_client
from acme.jose import util as jose_util
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt import network
from letsencrypt.display import util as display_util
from letsencrypt.display import revocation
@ -34,8 +34,7 @@ class Revoker(object):
.. todo:: Add a method to specify your own certificate for revocation - CLI
:ivar network: Network object
:type network: :class:`letsencrypt.network`
:ivar .acme.client.Client acme: ACME client
:ivar installer: Installer object
:type installer: :class:`~letsencrypt.interfaces.IInstaller`
@ -48,7 +47,7 @@ class Revoker(object):
"""
def __init__(self, installer, config, no_confirm=False):
# XXX
self.network = network.Network(new_reg_uri=None, key=None, alg=None)
self.acme = acme_client.Client(new_reg_uri=None, key=None, alg=None)
self.installer = installer
self.config = config
@ -263,7 +262,7 @@ class Revoker(object):
raise errors.RevokerError(
"Corrupted backup key file: %s" % cert.backup_key_path)
return self.network.revoke(cert=None) # XXX
return self.acme.revoke(cert=None) # XXX
def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use
"""Remove certificate and key.

View file

@ -1,208 +1,186 @@
"""Tests for letsencrypt.account."""
import logging
import mock
import datetime
import os
import pkg_resources
import shutil
import stat
import tempfile
import unittest
import mock
import pytz
from acme import jose
from acme import messages
from letsencrypt import configuration
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.display import util as display_util
from letsencrypt.tests import test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key_2.pem"))
class AccountTest(unittest.TestCase):
"""Tests letsencrypt.account.Account."""
"""Tests for letsencrypt.account.Account."""
def setUp(self):
from letsencrypt.account import Account
self.regr = mock.MagicMock()
self.meta = Account.Meta(
creation_host="test.letsencrypt.org",
creation_dt=datetime.datetime(
2015, 7, 4, 14, 4, 10, tzinfo=pytz.UTC))
self.acc = Account(self.regr, KEY, self.meta)
logging.disable(logging.CRITICAL)
with mock.patch("letsencrypt.account.socket") as mock_socket:
mock_socket.getfqdn.return_value = "test.letsencrypt.org"
with mock.patch("letsencrypt.account.datetime") as mock_dt:
mock_dt.datetime.now.return_value = self.meta.creation_dt
self.acc_no_meta = Account(self.regr, KEY)
self.accounts_dir = tempfile.mkdtemp("accounts")
self.account_keys_dir = os.path.join(self.accounts_dir, "keys")
os.makedirs(self.account_keys_dir, 0o700)
def test_init(self):
self.assertEqual(self.regr, self.acc.regr)
self.assertEqual(KEY, self.acc.key)
self.assertEqual(self.meta, self.acc_no_meta.meta)
def test_id(self):
self.assertEqual(
self.acc.id, "2ba35a3bdf380ed76a5ac9e740568395")
def test_slug(self):
self.assertEqual(
self.acc.slug, "test.letsencrypt.org@2015-07-04T14:04:10Z (2ba3)")
def test_repr(self):
self.assertEqual(
repr(self.acc),
"<Account(2ba35a3bdf380ed76a5ac9e740568395)>")
class ReportNewAccountTest(unittest.TestCase):
"""Tests for letsencrypt.account.report_new_account."""
def setUp(self):
self.config = mock.MagicMock(config_dir="/etc/letsencrypt")
reg = messages.Registration.from_data(email="rhino@jungle.io")
reg = reg.update(recovery_token="ECCENTRIC INVISIBILITY RHINOCEROS")
self.acc = mock.MagicMock(regr=messages.RegistrationResource(
uri=None, new_authzr_uri=None, body=reg))
def _call(self):
from letsencrypt.account import report_new_account
report_new_account(self.acc, self.config)
@mock.patch("letsencrypt.client.zope.component.queryUtility")
def test_no_reporter(self, mock_zope):
mock_zope.return_value = None
self._call()
@mock.patch("letsencrypt.client.zope.component.queryUtility")
def test_it(self, mock_zope):
self._call()
call_list = mock_zope().add_message.call_args_list
self.assertTrue(self.config.config_dir in call_list[0][0][0])
self.assertTrue(self.acc.regr.body.recovery_token in call_list[1][0][0])
self.assertTrue(
", ".join(self.acc.regr.body.emails) in call_list[1][0][0])
class AccountMemoryStorageTest(unittest.TestCase):
"""Tests for letsencrypt.account.AccountMemoryStorage."""
def setUp(self):
from letsencrypt.account import AccountMemoryStorage
self.storage = AccountMemoryStorage()
def test_it(self):
account = mock.Mock(id="x")
self.assertEqual([], self.storage.find_all())
self.assertRaises(errors.AccountNotFound, self.storage.load, "x")
self.storage.save(account)
self.assertEqual([account], self.storage.find_all())
self.assertEqual(account, self.storage.load("x"))
self.storage.save(account)
self.assertEqual([account], self.storage.find_all())
class AccountFileStorageTest(unittest.TestCase):
"""Tests for letsencrypt.account.AccountFileStorage."""
def setUp(self):
self.tmp = tempfile.mkdtemp()
self.config = mock.MagicMock(
spec=configuration.NamespaceConfig, accounts_dir=self.accounts_dir,
account_keys_dir=self.account_keys_dir, rsa_key_size=2048,
server="letsencrypt-demo.org")
accounts_dir=os.path.join(self.tmp, "accounts"))
from letsencrypt.account import AccountFileStorage
self.storage = AccountFileStorage(self.config)
key_file = pkg_resources.resource_filename(
"acme.jose", os.path.join("testdata", "rsa512_key.pem"))
key_pem = pkg_resources.resource_string(
"acme.jose", os.path.join("testdata", "rsa512_key.pem"))
self.key = le_util.Key(key_file, key_pem)
self.email = "client@letsencrypt.org"
self.regr = messages.RegistrationResource(
uri="uri",
new_authzr_uri="new_authzr_uri",
terms_of_service="terms_of_service",
body=messages.Registration(
recovery_token="recovery_token", agreement="agreement")
)
self.test_account = Account(
self.config, self.key, self.email, None, self.regr)
from letsencrypt.account import Account
self.acc = Account(
regr=messages.RegistrationResource(
uri=None, new_authzr_uri=None, body=messages.Registration()),
key=KEY)
def tearDown(self):
shutil.rmtree(self.accounts_dir)
logging.disable(logging.NOTSET)
shutil.rmtree(self.tmp)
@mock.patch("letsencrypt.account.zope.component.getUtility")
@mock.patch("letsencrypt.account.crypto_util.init_save_key")
def test_prompts(self, mock_key, mock_util):
from letsencrypt.account import Account
def test_init_creates_dir(self):
self.assertTrue(os.path.isdir(self.config.accounts_dir))
mock_util().input.return_value = (display_util.OK, self.email)
mock_key.return_value = self.key
def test_save_and_restore(self):
self.storage.save(self.acc)
account_path = os.path.join(self.config.accounts_dir, self.acc.id)
self.assertTrue(os.path.exists(account_path))
for file_name in "regr.json", "meta.json", "private_key.json":
self.assertTrue(os.path.exists(
os.path.join(account_path, file_name)))
self.assertEqual("0400", oct(os.stat(os.path.join(
account_path, "private_key.json"))[stat.ST_MODE] & 0o777))
acc = Account.from_prompts(self.config)
self.assertEqual(acc.email, self.email)
self.assertEqual(acc.key, self.key)
self.assertEqual(acc.config, self.config)
# restore
self.assertEqual(self.acc, self.storage.load(self.acc.id))
@mock.patch("letsencrypt.account.zope.component.getUtility")
@mock.patch("letsencrypt.account.Account.from_email")
def test_prompts_bad_email(self, mock_from_email, mock_util):
from letsencrypt.account import Account
def test_find_all(self):
self.storage.save(self.acc)
self.assertEqual([self.acc], self.storage.find_all())
mock_from_email.side_effect = (errors.Error, "acc")
mock_util().input.return_value = (display_util.OK, self.email)
def test_find_all_none_empty_list(self):
self.assertEqual([], self.storage.find_all())
self.assertEqual(Account.from_prompts(self.config), "acc")
def test_find_all_accounts_dir_absent(self):
os.rmdir(self.config.accounts_dir)
self.assertEqual([], self.storage.find_all())
def test_find_all_load_skips(self):
self.storage.load = mock.MagicMock(
side_effect=["x", errors.AccountStorageError, "z"])
with mock.patch("letsencrypt.account.os.listdir") as mock_listdir:
mock_listdir.return_value = ["x", "y", "z"]
self.assertEqual(["x", "z"], self.storage.find_all())
@mock.patch("letsencrypt.account.zope.component.getUtility")
@mock.patch("letsencrypt.account.crypto_util.init_save_key")
def test_prompts_empty_email(self, mock_key, mock_util):
from letsencrypt.account import Account
def test_load_non_existent_raises_error(self):
self.assertRaises(errors.AccountNotFound, self.storage.load, "missing")
mock_util().input.return_value = (display_util.OK, "")
acc = Account.from_prompts(self.config)
self.assertTrue(acc.email is None)
# _get_config_filename | pylint: disable=protected-access
mock_key.assert_called_once_with(
mock.ANY, mock.ANY, acc._get_config_filename(None))
def test_load_id_mismatch_raises_error(self):
self.storage.save(self.acc)
shutil.move(os.path.join(self.config.accounts_dir, self.acc.id),
os.path.join(self.config.accounts_dir, "x" + self.acc.id))
self.assertRaises(errors.AccountStorageError, self.storage.load,
"x" + self.acc.id)
@mock.patch("letsencrypt.account.zope.component.getUtility")
def test_prompts_cancel(self, mock_util):
from letsencrypt.account import Account
def test_load_ioerror(self):
self.storage.save(self.acc)
mock_open = mock.mock_open()
mock_open.side_effect = IOError
with mock.patch("__builtin__.open", mock_open):
self.assertRaises(
errors.AccountStorageError, self.storage.load, self.acc.id)
mock_util().input.return_value = (display_util.CANCEL, "")
self.assertTrue(Account.from_prompts(self.config) is None)
def test_from_email(self):
from letsencrypt.account import Account
self.assertRaises(
errors.Error, Account.from_email, self.config, "not_valid...email")
def test_save_from_existing_account(self):
from letsencrypt.account import Account
self.test_account.save()
acc = Account.from_existing_account(self.config, self.email)
self.assertEqual(acc.key, self.test_account.key)
self.assertEqual(acc.email, self.test_account.email)
self.assertEqual(acc.phone, self.test_account.phone)
self.assertEqual(acc.regr, self.test_account.regr)
def test_properties(self):
self.assertEqual(self.test_account.uri, "uri")
self.assertEqual(self.test_account.new_authzr_uri, "new_authzr_uri")
self.assertEqual(self.test_account.terms_of_service, "terms_of_service")
self.assertEqual(self.test_account.recovery_token, "recovery_token")
def test_partial_properties(self):
from letsencrypt.account import Account
partial = Account(self.config, self.key)
self.assertTrue(partial.uri is None)
self.assertTrue(partial.new_authzr_uri is None)
self.assertTrue(partial.terms_of_service is None)
self.assertTrue(partial.recovery_token is None)
def test_partial_account_default(self):
from letsencrypt.account import Account
partial = Account(self.config, self.key)
partial.save()
acc = Account.from_existing_account(self.config)
self.assertEqual(partial.key, acc.key)
self.assertEqual(partial.email, acc.email)
self.assertEqual(partial.phone, acc.phone)
self.assertEqual(partial.regr, acc.regr)
def test_get_accounts(self):
from letsencrypt.account import Account
accs = Account.get_accounts(self.config)
self.assertFalse(accs)
self.test_account.save()
accs = Account.get_accounts(self.config)
self.assertEqual(len(accs), 1)
self.assertEqual(accs[0].email, self.test_account.email)
acc2 = Account(self.config, self.key, "testing_email@gmail.com")
acc2.save()
accs = Account.get_accounts(self.config)
self.assertEqual(len(accs), 2)
def test_get_accounts_no_accounts(self):
from letsencrypt.account import Account
self.assertEqual(Account.get_accounts(
mock.Mock(accounts_dir="non-existant")), [])
def test_failed_existing_account(self):
from letsencrypt.account import Account
self.assertRaises(errors.Error, Account.from_existing_account,
self.config, "non-existant@email.org")
class SafeEmailTest(unittest.TestCase):
"""Test safe_email."""
def setUp(self):
logging.disable(logging.CRITICAL)
def tearDown(self):
logging.disable(logging.NOTSET)
@classmethod
def _call(cls, addr):
from letsencrypt.account import Account
return Account.safe_email(addr)
def test_valid_emails(self):
addrs = [
"letsencrypt@letsencrypt.org",
"tbd.ade@gmail.com",
"abc_def.jdk@hotmail.museum",
]
for addr in addrs:
self.assertTrue(self._call(addr), "%s failed." % addr)
def test_invalid_emails(self):
addrs = [
"letsencrypt@letsencrypt..org",
".tbd.ade@gmail.com",
"~/abc_def.jdk@hotmail.museum",
]
for addr in addrs:
self.assertFalse(self._call(addr), "%s failed." % addr)
def test_save_ioerrors(self):
mock_open = mock.mock_open()
mock_open.side_effect = IOError # TODO: [None, None, IOError]
with mock.patch("__builtin__.open", mock_open):
self.assertRaises(
errors.AccountStorageError, self.storage.save, self.acc)
if __name__ == "__main__":

View file

@ -1,15 +1,15 @@
"""Tests for letsencrypt.achallenges."""
import os
import pkg_resources
import unittest
import OpenSSL
from acme import challenges
from acme import jose
from letsencrypt import crypto_util
from letsencrypt import le_util
from letsencrypt.tests import acme_util
from letsencrypt.tests import test_util
class DVSNITest(unittest.TestCase):
@ -19,8 +19,7 @@ class DVSNITest(unittest.TestCase):
self.chall = acme_util.chall_to_challb(
challenges.DVSNI(r="r_value", nonce="12345ABCDE"), "pending")
self.response = challenges.DVSNIResponse()
key = le_util.Key("path", pkg_resources.resource_string(
"acme.jose", os.path.join("testdata", "rsa512_key.pem")))
key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
from letsencrypt.achallenges import DVSNI
self.achall = DVSNI(challb=self.chall, domain="example.com", key=key)

View file

@ -1,21 +1,15 @@
"""ACME utilities for testing."""
import datetime
import itertools
import os
import pkg_resources
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
from acme import challenges
from acme import jose
from acme import messages
from letsencrypt.tests import test_util
KEY = serialization.load_pem_private_key(
pkg_resources.resource_string(
__name__, os.path.join('testdata', 'rsa512_key.pem')),
password=None, backend=default_backend())
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
# Challenges
SIMPLE_HTTP = challenges.SimpleHTTP(

View file

@ -6,11 +6,11 @@ import unittest
import mock
from acme import challenges
from acme import client as acme_client
from acme import messages
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt import network
from letsencrypt.tests import acme_util
@ -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=network.Network)
self.mock_net = mock.MagicMock(spec=acme_client.Client)
self.handler = AuthHandler(
self.mock_dv_auth, self.mock_cont_auth,

View file

@ -8,6 +8,8 @@ import unittest
import mock
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import errors
@ -26,7 +28,8 @@ class CLITest(unittest.TestCase):
def _call(self, args):
from letsencrypt import cli
args = ['--text', '--config-dir', self.config_dir,
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir] + args
'--work-dir', self.work_dir, '--logs-dir', self.logs_dir,
'--agree-eula'] + args
with mock.patch('letsencrypt.cli.sys.stdout') as stdout:
with mock.patch('letsencrypt.cli.sys.stderr') as stderr:
with mock.patch('letsencrypt.cli.client') as client:
@ -42,7 +45,7 @@ class CLITest(unittest.TestCase):
def test_rollback(self):
_, _, _, client = self._call(['rollback'])
client.rollback.assert_called_once()
self.assertEqual(1, client.rollback.call_count)
_, _, _, client = self._call(['rollback', '--checkpoints', '123'])
client.rollback.assert_called_once_with(
@ -50,7 +53,7 @@ class CLITest(unittest.TestCase):
def test_config_changes(self):
_, _, _, client = self._call(['config_changes'])
client.view_config_changes.assert_called_once()
self.assertEqual(1, client.view_config_changes.call_count)
def test_plugins(self):
flags = ['--init', '--prepare', '--authenticators', '--installers']
@ -96,5 +99,68 @@ class CLITest(unittest.TestCase):
traceback.format_exception_only(KeyboardInterrupt, interrupt)))
class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.cli._determine_account."""
def setUp(self):
self.args = mock.MagicMock(account=None, email=None)
self.config = configuration.NamespaceConfig(self.args)
self.accs = [mock.MagicMock(id="x"), mock.MagicMock(id="y")]
self.account_storage = account.AccountMemoryStorage()
def _call(self):
# pylint: disable=protected-access
from letsencrypt.cli import _determine_account
with mock.patch("letsencrypt.cli.account.AccountFileStorage") as mock_storage:
mock_storage.return_value = self.account_storage
return _determine_account(self.args, self.config)
def test_args_account_set(self):
self.account_storage.save(self.accs[1])
self.args.account = self.accs[1].id
self.assertEqual((self.accs[1], None), self._call())
self.assertEqual(self.accs[1].id, self.args.account)
self.assertTrue(self.args.email is None)
def test_single_account(self):
self.account_storage.save(self.accs[0])
self.assertEqual((self.accs[0], None), self._call())
self.assertEqual(self.accs[0].id, self.args.account)
self.assertTrue(self.args.email is None)
@mock.patch("letsencrypt.client.display_ops.choose_account")
def test_multiple_accounts(self, mock_choose_accounts):
for acc in self.accs:
self.account_storage.save(acc)
mock_choose_accounts.return_value = self.accs[1]
self.assertEqual((self.accs[1], None), self._call())
self.assertEqual(
set(mock_choose_accounts.call_args[0][0]), set(self.accs))
self.assertEqual(self.accs[1].id, self.args.account)
self.assertTrue(self.args.email is None)
@mock.patch("letsencrypt.client.display_ops.get_email")
def test_no_accounts_no_email(self, mock_get_email):
mock_get_email.return_value = "foo@bar.baz"
with mock.patch("letsencrypt.cli.client") as client:
client.register.return_value = (
self.accs[0], mock.sentinel.acme)
self.assertEqual((self.accs[0], mock.sentinel.acme), self._call())
client.register.assert_called_once_with(
self.config, self.account_storage, tos_cb=mock.ANY)
self.assertEqual(self.accs[0].id, self.args.account)
self.assertEqual("foo@bar.baz", self.args.email)
def test_no_accounts_email(self):
self.args.email = "other email"
with mock.patch("letsencrypt.cli.client") as client:
client.register.return_value = (self.accs[1], mock.sentinel.acme)
self._call()
self.assertEqual(self.accs[1].id, self.args.account)
self.assertEqual("other email", self.args.email)
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -1,9 +1,5 @@
"""Tests for letsencrypt.client."""
import os
import unittest
import pkg_resources
import shutil
import tempfile
import configobj
import OpenSSL
@ -13,13 +9,46 @@ from acme import jose
from letsencrypt import account
from letsencrypt import configuration
from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.tests import test_util
KEY = pkg_resources.resource_string(
__name__, os.path.join("testdata", "rsa512_key.pem"))
CSR_SAN = pkg_resources.resource_string(
__name__, os.path.join("testdata", "csr-san.der"))
KEY = test_util.load_vector("rsa512_key.pem")
CSR_SAN = test_util.load_vector("csr-san.der")
class RegisterTest(unittest.TestCase):
"""Tests for letsencrypt.client.register."""
def setUp(self):
self.config = mock.MagicMock(rsa_key_size=1024)
self.account_storage = account.AccountMemoryStorage()
self.tos_cb = mock.MagicMock()
def _call(self):
from letsencrypt.client import register
return register(self.config, self.account_storage, self.tos_cb)
def test_no_tos(self):
with mock.patch("letsencrypt.client.acme_client.Client") as mock_client:
mock_client.register().terms_of_service = "http://tos"
with mock.patch("letsencrypt.account.report_new_account"):
self.tos_cb.return_value = False
self.assertRaises(errors.Error, self._call)
self.tos_cb.return_value = True
self._call()
self.tos_cb = None
self._call()
def test_it(self):
with mock.patch("letsencrypt.client.acme_client.Client"):
with mock.patch("letsencrypt.account."
"report_new_account"):
self._call()
class ClientTest(unittest.TestCase):
@ -32,29 +61,30 @@ class ClientTest(unittest.TestCase):
self.account = mock.MagicMock(**{"key.pem": KEY})
from letsencrypt.client import Client
with mock.patch("letsencrypt.client.network.Network") as network:
with mock.patch("letsencrypt.client.acme_client.Client") as acme:
self.acme_client = acme
self.acme = acme.return_value = mock.MagicMock()
self.client = Client(
config=self.config, account_=self.account,
dv_auth=None, installer=None)
self.network = network
def test_init_network_verify_ssl(self):
self.network.assert_called_once_with(
mock.ANY, mock.ANY, verify_ssl=True)
def test_init_acme_verify_ssl(self):
self.acme_client.assert_called_once_with(
new_reg_uri=mock.ANY, key=mock.ANY, verify_ssl=True)
def _mock_obtain_certificate(self):
self.client.auth_handler = mock.MagicMock()
self.network().request_issuance.return_value = mock.sentinel.certr
self.network().fetch_chain.return_value = mock.sentinel.chain
self.acme.request_issuance.return_value = mock.sentinel.certr
self.acme.fetch_chain.return_value = mock.sentinel.chain
def _check_obtain_certificate(self):
self.client.auth_handler.get_authorizations.assert_called_once_with(
["example.com", "www.example.com"])
self.network.request_issuance.assert_callend_once_with(
self.acme.request_issuance.assert_called_once_with(
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)),
self.client.auth_handler.get_authorizations())
self.network().fetch_chain.assert_called_once_with(mock.sentinel.certr)
self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr)
def test_obtain_certificate_from_csr(self):
self._mock_obtain_certificate()
@ -83,18 +113,6 @@ class ClientTest(unittest.TestCase):
mock.sentinel.key, domains, self.config.cert_dir)
self._check_obtain_certificate()
@mock.patch("letsencrypt.client.zope.component.getUtility")
def test_report_new_account(self, mock_zope):
# pylint: disable=protected-access
self.account.recovery_token = "ECCENTRIC INVISIBILITY RHINOCEROS"
self.account.email = "rhino@jungle.io"
self.client._report_new_account()
call_list = mock_zope().add_message.call_args_list
self.assertTrue(self.config.config_dir in call_list[0][0][0])
self.assertTrue(self.account.recovery_token in call_list[1][0][0])
self.assertTrue(self.account.email in call_list[1][0][0])
@mock.patch("letsencrypt.client.zope.component.getUtility")
def test_report_renewal_status(self, mock_zope):
# pylint: disable=protected-access
@ -128,50 +146,6 @@ class ClientTest(unittest.TestCase):
self.assertTrue(cert.cli_config.renewal_configs_dir in msg)
class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.client.determine_authenticator."""
def setUp(self):
self.accounts_dir = tempfile.mkdtemp("accounts")
account_keys_dir = os.path.join(self.accounts_dir, "keys")
os.makedirs(account_keys_dir, 0o700)
self.config = mock.MagicMock(
spec=configuration.NamespaceConfig, accounts_dir=self.accounts_dir,
account_keys_dir=account_keys_dir, rsa_key_size=2048,
server="letsencrypt-demo.org")
def tearDown(self):
shutil.rmtree(self.accounts_dir)
@mock.patch("letsencrypt.account.Account.from_prompts")
@mock.patch("letsencrypt.client.display_ops.choose_account")
def test_determine_account(self, mock_op, mock_prompt):
"""Test determine account"""
from letsencrypt import client
key = le_util.Key(tempfile.mkstemp()[1], "pem")
test_acc = account.Account(self.config, key, "email1@gmail.com")
mock_op.return_value = test_acc
# Test 0
mock_prompt.return_value = None
self.assertTrue(client.determine_account(self.config) is None)
# Test 1
test_acc.save()
acc = client.determine_account(self.config)
self.assertEqual(acc.email, test_acc.email)
# Test multiple
self.assertFalse(mock_op.called)
acc2 = account.Account(self.config, key)
acc2.save()
chosen_acc = client.determine_account(self.config)
self.assertTrue(mock_op.called)
self.assertTrue(chosen_acc.email, test_acc.email)
class RollbackTest(unittest.TestCase):
"""Tests for letsencrypt.client.rollback."""

View file

@ -31,7 +31,6 @@ class NamespaceConfigTest(unittest.TestCase):
@mock.patch('letsencrypt.configuration.constants')
def test_dynamic_dirs(self, constants):
constants.ACCOUNTS_DIR = 'acc'
constants.ACCOUNT_KEYS_DIR = 'keys'
constants.BACKUP_DIR = 'backups'
constants.CERT_KEY_BACKUP_DIR = 'c/'
constants.CERT_DIR = 'certs'
@ -42,9 +41,6 @@ class NamespaceConfigTest(unittest.TestCase):
self.assertEqual(
self.config.accounts_dir, '/tmp/config/acc/acme-server.org:443/new')
self.assertEqual(
self.config.account_keys_dir,
'/tmp/config/acc/acme-server.org:443/new/keys')
self.assertEqual(self.config.backup_dir, '/tmp/foo/backups')
self.assertEqual(self.config.cert_dir, '/tmp/config/certs')
self.assertEqual(

View file

@ -1,7 +1,5 @@
"""Tests for letsencrypt.crypto_util."""
import logging
import os
import pkg_resources
import shutil
import tempfile
import unittest
@ -9,15 +7,13 @@ import unittest
import OpenSSL
import mock
from letsencrypt.tests import test_util
RSA256_KEY = pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa256_key.pem'))
RSA512_KEY = pkg_resources.resource_string(
'acme.jose', os.path.join('testdata', 'rsa512_key.pem'))
CERT = pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'cert.pem'))
SAN_CERT = pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'cert-san.pem'))
RSA256_KEY = test_util.load_vector('rsa256_key.pem')
RSA512_KEY = test_util.load_vector('rsa512_key.pem')
CERT = test_util.load_vector('cert.pem')
SAN_CERT = test_util.load_vector('cert-san.pem')
class InitSaveKeyTest(unittest.TestCase):
@ -100,21 +96,17 @@ class ValidCSRTest(unittest.TestCase):
from letsencrypt.crypto_util import valid_csr
return valid_csr(csr)
def _call_testdata(self, name):
return self._call(pkg_resources.resource_string(
__name__, os.path.join('testdata', name)))
def test_valid_pem_true(self):
self.assertTrue(self._call_testdata('csr.pem'))
self.assertTrue(self._call(test_util.load_vector('csr.pem')))
def test_valid_pem_san_true(self):
self.assertTrue(self._call_testdata('csr-san.pem'))
self.assertTrue(self._call(test_util.load_vector('csr-san.pem')))
def test_valid_der_false(self):
self.assertFalse(self._call_testdata('csr.der'))
self.assertFalse(self._call(test_util.load_vector('csr.der')))
def test_valid_der_san_false(self):
self.assertFalse(self._call_testdata('csr-san.der'))
self.assertFalse(self._call(test_util.load_vector('csr-san.der')))
def test_empty_false(self):
self.assertFalse(self._call(''))
@ -127,16 +119,17 @@ class CSRMatchesPubkeyTest(unittest.TestCase):
"""Tests for letsencrypt.crypto_util.csr_matches_pubkey."""
@classmethod
def _call_testdata(cls, name, privkey):
def _call(cls, *args, **kwargs):
from letsencrypt.crypto_util import csr_matches_pubkey
return csr_matches_pubkey(pkg_resources.resource_string(
__name__, os.path.join('testdata', name)), privkey)
return csr_matches_pubkey(*args, **kwargs)
def test_valid_true(self):
self.assertTrue(self._call_testdata('csr.pem', RSA512_KEY))
self.assertTrue(self._call(
test_util.load_vector('csr.pem'), RSA512_KEY))
def test_invalid_false(self):
self.assertFalse(self._call_testdata('csr.pem', RSA256_KEY))
self.assertFalse(self._call(
test_util.load_vector('csr.pem'), RSA256_KEY))
class MakeKeyTest(unittest.TestCase): # pylint: disable=too-few-public-methods
@ -185,50 +178,42 @@ class GetSANsFromCertTest(unittest.TestCase):
return get_sans_from_cert(*args, **kwargs)
def test_single(self):
self.assertEqual([], self._call(pkg_resources.resource_string(
__name__, os.path.join('testdata', 'cert.pem'))))
self.assertEqual([], self._call(test_util.load_vector('cert.pem')))
def test_san(self):
self.assertEqual(
['example.com', 'www.example.com'],
self._call(pkg_resources.resource_string(
__name__, os.path.join('testdata', 'cert-san.pem'))))
self._call(test_util.load_vector('cert-san.pem')))
class GetSANsFromCSRTest(unittest.TestCase):
"""Tests for letsencrypt.crypto_util.get_sans_from_csr."""
def test_extract_one_san(self):
@classmethod
def _call(cls, *args, **kwargs):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr.pem'))
self.assertEqual(get_sans_from_csr(csr), ['example.com'])
return get_sans_from_csr(*args, **kwargs)
def test_extract_one_san(self):
self.assertEqual(['example.com'], self._call(
test_util.load_vector('csr.pem')))
def test_extract_two_sans(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr-san.pem'))
self.assertEqual(get_sans_from_csr(csr), ['example.com',
'www.example.com'])
self.assertEqual(['example.com', 'www.example.com'], self._call(
test_util.load_vector('csr-san.pem')))
def test_extract_six_sans(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr-6sans.pem'))
self.assertEqual(get_sans_from_csr(csr),
self.assertEqual(self._call(test_util.load_vector('csr-6sans.pem')),
["example.com", "example.org", "example.net",
"example.info", "subdomain.example.com",
"other.subdomain.example.com"])
def test_parse_non_csr(self):
from letsencrypt.crypto_util import get_sans_from_csr
self.assertRaises(OpenSSL.crypto.Error, get_sans_from_csr,
"hello there")
self.assertRaises(OpenSSL.crypto.Error, self._call, "hello there")
def test_parse_no_sans(self):
from letsencrypt.crypto_util import get_sans_from_csr
csr = pkg_resources.resource_string(
__name__, os.path.join('testdata', 'csr-nosans.pem'))
self.assertEqual([], get_sans_from_csr(csr))
self.assertEqual(
[], self._call(test_util.load_vector('csr-nosans.pem')))
if __name__ == '__main__':

View file

@ -7,12 +7,19 @@ import unittest
import mock
import zope.component
from acme import jose
from acme import messages
from letsencrypt import account
from letsencrypt import interfaces
from letsencrypt import le_util
from letsencrypt.display import util as display_util
from letsencrypt.tests import test_util
KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
class ChoosePluginTest(unittest.TestCase):
"""Tests for letsencrypt.display.ops.choose_plugin."""
@ -73,11 +80,11 @@ class PickPluginTest(unittest.TestCase):
def test_default_provided(self):
self.default = "foo"
self._call()
self.reg.filter.assert_called_once()
self.assertEqual(1, self.reg.filter.call_count)
def test_no_default(self):
self._call()
self.reg.filter.assert_called_once()
self.assertEqual(1, self.reg.ifaces.call_count)
def test_no_candidate(self):
self.assertTrue(self._call() is None)
@ -140,8 +147,40 @@ class ConveniencePickPluginTest(unittest.TestCase):
interfaces.IAuthenticator, interfaces.IInstaller))
class GetEmailTest(unittest.TestCase):
"""Tests for letsencrypt.display.ops.get_email."""
def setUp(self):
mock_display = mock.MagicMock()
self.input = mock_display.input
zope.component.provideUtility(mock_display, interfaces.IDisplay)
@classmethod
def _call(cls):
from letsencrypt.display.ops import get_email
return get_email()
def test_cancel_none(self):
self.input.return_value = (display_util.CANCEL, "foo@bar.baz")
self.assertTrue(self._call() is None)
def test_ok_safe(self):
self.input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("letsencrypt.display.ops.le_util"
".safe_email") as mock_safe_email:
mock_safe_email.return_value = True
self.assertTrue(self._call() is "foo@bar.baz")
def test_ok_not_safe(self):
self.input.return_value = (display_util.OK, "foo@bar.baz")
with mock.patch("letsencrypt.display.ops.le_util"
".safe_email") as mock_safe_email:
mock_safe_email.side_effect = [False, True]
self.assertTrue(self._call() is "foo@bar.baz")
class ChooseAccountTest(unittest.TestCase):
"""Test choose_account."""
"""Tests for letsencrypt.display.ops.choose_account."""
def setUp(self):
zope.component.provideUtility(display_util.FileDisplay(sys.stdout))
@ -153,13 +192,14 @@ class ChooseAccountTest(unittest.TestCase):
accounts_dir=self.accounts_dir,
account_keys_dir=self.account_keys_dir,
server="letsencrypt-demo.org")
self.key = le_util.Key("keypath", "pem")
self.key = KEY
self.acc1 = account.Account(self.config, self.key, "email1@g.com")
self.acc2 = account.Account(
self.config, self.key, "email2@g.com", "phone")
self.acc1.save()
self.acc2.save()
self.acc1 = account.Account(messages.RegistrationResource(
uri=None, new_authzr_uri=None, body=messages.Registration.from_data(
email="email1@g.com")), self.key)
self.acc2 = account.Account(messages.RegistrationResource(
uri=None, new_authzr_uri=None, body=messages.Registration.from_data(
email="email2@g.com", phone="phone")), self.key)
@classmethod
def _call(cls, accounts):

View file

@ -1,6 +1,4 @@
"""Test :mod:`letsencrypt.display.revocation`."""
import os
import pkg_resources
import sys
import unittest
@ -9,15 +7,14 @@ import zope.component
from letsencrypt.display import util as display_util
from letsencrypt.tests import test_util
class DisplayCertsTest(unittest.TestCase):
def setUp(self):
from letsencrypt.revoker import Cert
base_package = "letsencrypt.tests"
self.cert0 = Cert(pkg_resources.resource_filename(
base_package, os.path.join("testdata", "cert.pem")))
self.cert1 = Cert(pkg_resources.resource_filename(
base_package, os.path.join("testdata", "cert-san.pem")))
self.cert0 = Cert(test_util.vector_path("cert.pem"))
self.cert1 = Cert(test_util.vector_path("cert-san.pem"))
self.certs = [self.cert0, self.cert1]
@ -62,9 +59,7 @@ class MoreInfoCertTest(unittest.TestCase):
class SuccessRevocationTest(unittest.TestCase):
def setUp(self):
from letsencrypt.revoker import Cert
base_package = "letsencrypt.tests"
self.cert = Cert(pkg_resources.resource_filename(
base_package, os.path.join("testdata", "cert.pem")))
self.cert = Cert(test_util.vector_path("cert.pem"))
@classmethod
def _call(cls, cert):
@ -82,8 +77,7 @@ class SuccessRevocationTest(unittest.TestCase):
class ConfirmRevocationTest(unittest.TestCase):
def setUp(self):
from letsencrypt.revoker import Cert
self.cert = Cert(pkg_resources.resource_filename(
"letsencrypt.tests", os.path.join("testdata", "cert.pem")))
self.cert = Cert(test_util.vector_path("cert.pem"))
@classmethod
def _call(cls, cert):

View file

@ -21,7 +21,7 @@ class MakeOrVerifyDirTest(unittest.TestCase):
def setUp(self):
self.root_path = tempfile.mkdtemp()
self.path = os.path.join(self.root_path, 'foo')
self.path = os.path.join(self.root_path, "foo")
os.mkdir(self.path, 0o400)
self.uid = os.getuid()
@ -34,7 +34,7 @@ class MakeOrVerifyDirTest(unittest.TestCase):
return make_or_verify_dir(directory, mode, self.uid)
def test_creates_dir_when_missing(self):
path = os.path.join(self.root_path, 'bar')
path = os.path.join(self.root_path, "bar")
self._call(path, 0o650)
self.assertTrue(os.path.isdir(path))
self.assertEqual(stat.S_IMODE(os.stat(path).st_mode), 0o650)
@ -47,9 +47,9 @@ class MakeOrVerifyDirTest(unittest.TestCase):
self.assertRaises(errors.Error, self._call, self.path, 0o600)
def test_reraises_os_error(self):
with mock.patch.object(os, 'makedirs') as makedirs:
with mock.patch.object(os, "makedirs") as makedirs:
makedirs.side_effect = OSError()
self.assertRaises(OSError, self._call, 'bar', 12312312)
self.assertRaises(OSError, self._call, "bar", 12312312)
class CheckPermissionsTest(unittest.TestCase):
@ -85,7 +85,7 @@ class UniqueFileTest(unittest.TestCase):
def setUp(self):
self.root_path = tempfile.mkdtemp()
self.default_name = os.path.join(self.root_path, 'foo.txt')
self.default_name = os.path.join(self.root_path, "foo.txt")
def tearDown(self):
shutil.rmtree(self.root_path, ignore_errors=True)
@ -96,9 +96,9 @@ class UniqueFileTest(unittest.TestCase):
def test_returns_fd_for_writing(self):
fd, name = self._call()
fd.write('bar')
fd.write("bar")
fd.close()
self.assertEqual(open(name).read(), 'bar')
self.assertEqual(open(name).read(), "bar")
def test_right_mode(self):
self.assertEqual(0o700, os.stat(self._call(0o700)[1]).st_mode & 0o777)
@ -118,11 +118,11 @@ class UniqueFileTest(unittest.TestCase):
self.assertEqual(os.path.dirname(name3), self.root_path)
basename1 = os.path.basename(name2)
self.assertTrue(basename1.endswith('foo.txt'))
self.assertTrue(basename1.endswith("foo.txt"))
basename2 = os.path.basename(name2)
self.assertTrue(basename2.endswith('foo.txt'))
self.assertTrue(basename2.endswith("foo.txt"))
basename3 = os.path.basename(name3)
self.assertTrue(basename3.endswith('foo.txt'))
self.assertTrue(basename3.endswith("foo.txt"))
class UniqueLineageNameTest(unittest.TestCase):
@ -139,9 +139,9 @@ class UniqueLineageNameTest(unittest.TestCase):
return unique_lineage_name(self.root_path, filename, mode)
def test_basic(self):
f, name = self._call("wow")
f, path = self._call("wow")
self.assertTrue(isinstance(f, file))
self.assertTrue(isinstance(name, str))
self.assertEqual(os.path.join(self.root_path, "wow.conf"), path)
def test_multiple(self):
for _ in xrange(10):
@ -165,5 +165,32 @@ class UniqueLineageNameTest(unittest.TestCase):
mock_fdopen.side_effect = err
self.assertRaises(OSError, self._call, "wow")
if __name__ == '__main__':
class SafeEmailTest(unittest.TestCase):
"""Test safe_email."""
@classmethod
def _call(cls, addr):
from letsencrypt.le_util import safe_email
return safe_email(addr)
def test_valid_emails(self):
addrs = [
"letsencrypt@letsencrypt.org",
"tbd.ade@gmail.com",
"abc_def.jdk@hotmail.museum",
]
for addr in addrs:
self.assertTrue(self._call(addr), "%s failed." % addr)
def test_invalid_emails(self):
addrs = [
"letsencrypt@letsencrypt..org",
".tbd.ade@gmail.com",
"~/abc_def.jdk@hotmail.museum",
]
for addr in addrs:
self.assertFalse(self._call(addr), "%s failed." % addr)
if __name__ == "__main__":
unittest.main() # pragma: no cover

View file

@ -1,50 +0,0 @@
"""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

View file

@ -1,11 +1,8 @@
"""Tests for letsencrypt.proof_of_possession."""
import os
import pkg_resources
import tempfile
import unittest
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import mock
from acme import challenges
@ -16,22 +13,15 @@ from letsencrypt import achallenges
from letsencrypt import proof_of_possession
from letsencrypt.display import util as display_util
from letsencrypt.tests import test_util
BASE_PACKAGE = "letsencrypt.tests"
CERT0_PATH = pkg_resources.resource_filename(
"acme.jose", os.path.join("testdata", "cert.der"))
CERT2_PATH = pkg_resources.resource_filename(
BASE_PACKAGE, os.path.join("testdata", "dsa_cert.pem"))
CERT2_KEY_PATH = pkg_resources.resource_filename(
BASE_PACKAGE, os.path.join("testdata", "dsa512_key.pem"))
CERT3_PATH = pkg_resources.resource_filename(
BASE_PACKAGE, os.path.join("testdata", "matching_cert.pem"))
CERT3_KEY_PATH = pkg_resources.resource_filename(
BASE_PACKAGE, os.path.join("testdata", "rsa512_key.pem"))
with open(CERT3_KEY_PATH) as cert3_file:
CERT3_KEY = serialization.load_pem_private_key(
cert3_file.read(), password=None,
backend=default_backend()).public_key()
CERT0_PATH = test_util.vector_path("cert.der")
CERT2_PATH = test_util.vector_path("dsa_cert.pem")
CERT2_KEY_PATH = test_util.vector_path("dsa512_key.pem")
CERT3_PATH = test_util.vector_path("matching_cert.pem")
CERT3_KEY_PATH = test_util.vector_path("rsa512_key_2.pem")
CERT3_KEY = test_util.load_rsa_private_key("rsa512_key_2.pem").public_key()
class ProofOfPossessionTest(unittest.TestCase):

View file

@ -2,22 +2,20 @@
import datetime
import os
import tempfile
import pkg_resources
import shutil
import unittest
import configobj
import mock
import OpenSSL
import pytz
from letsencrypt import configuration
from letsencrypt.storage import ALL_FOUR
from letsencrypt.tests import test_util
CERT = OpenSSL.crypto.load_certificate(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
'letsencrypt.tests', os.path.join('testdata', 'cert.pem')))
CERT = test_util.load_cert('cert.pem')
def unlink_all(rc_object):
@ -295,8 +293,7 @@ class RenewableCertTests(unittest.TestCase):
self.assertFalse(self.test_rc.has_pending_deployment())
def _test_notafterbefore(self, function, timestamp):
test_cert = pkg_resources.resource_string(
"letsencrypt.tests", "testdata/cert.pem")
test_cert = test_util.load_vector("cert.pem")
os.symlink(os.path.join("..", "..", "archive", "example.org",
"cert12.pem"), self.test_rc.cert)
with open(self.test_rc.cert, "w") as f:
@ -319,8 +316,7 @@ class RenewableCertTests(unittest.TestCase):
def test_time_interval_judgments(self, mock_datetime):
"""Test should_autodeploy() and should_autorenew() on the basis
of expiry time windows."""
test_cert = pkg_resources.resource_string(
"letsencrypt.tests", "testdata/cert.pem")
test_cert = test_util.load_vector("cert.pem")
for kind in ALL_FOUR:
where = getattr(self.test_rc, kind)
os.symlink(os.path.join("..", "..", "archive", "example.org",
@ -556,13 +552,12 @@ class RenewableCertTests(unittest.TestCase):
datetime.timedelta(intended[time]))
@mock.patch("letsencrypt.renewer.plugins_disco")
@mock.patch("letsencrypt.client.determine_account")
@mock.patch("letsencrypt.account.AccountFileStorage")
@mock.patch("letsencrypt.client.Client")
def test_renew(self, mock_c, mock_da, mock_pd):
def test_renew(self, mock_c, mock_acc_storage, mock_pd):
from letsencrypt import renewer
test_cert = pkg_resources.resource_string(
"letsencrypt.tests", "testdata/cert-san.pem")
test_cert = test_util.load_vector("cert-san.pem")
for kind in ALL_FOUR:
os.symlink(os.path.join("..", "..", "archive", "example.org",
kind + "1.pem"),
@ -580,6 +575,7 @@ class RenewableCertTests(unittest.TestCase):
self.test_rc.configfile["renewalparams"]["server"] = "acme.example.com"
self.test_rc.configfile["renewalparams"]["authenticator"] = "fake"
self.test_rc.configfile["renewalparams"]["dvsni_port"] = "4430"
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
mock_auth = mock.MagicMock()
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
# Fails because "fake" != "apache"
@ -594,7 +590,7 @@ class RenewableCertTests(unittest.TestCase):
self.assertEqual(2, renewer.renew(self.test_rc, 1))
# TODO: We could also make several assertions about calls that should
# have been made to the mock functions here.
self.assertEqual(mock_da.call_count, 1)
mock_acc_storage().load.assert_called_once_with(account_id="abcde")
mock_client.obtain_certificate.return_value = (
mock.sentinel.certr, None, mock.sentinel.key, mock.sentinel.csr)
# This should fail because the renewal itself appears to fail

View file

@ -1,7 +1,6 @@
"""Test letsencrypt.revoker."""
import csv
import os
import pkg_resources
import shutil
import tempfile
import unittest
@ -13,10 +12,11 @@ from letsencrypt import errors
from letsencrypt import le_util
from letsencrypt.display import util as display_util
from letsencrypt.tests import test_util
KEY = OpenSSL.crypto.load_privatekey(
OpenSSL.crypto.FILETYPE_PEM, pkg_resources.resource_string(
__name__, os.path.join("testdata", "rsa512_key.pem")))
OpenSSL.crypto.FILETYPE_PEM, test_util.load_vector("rsa512_key.pem"))
class RevokerBase(unittest.TestCase): # pylint: disable=too-few-public-methods
@ -69,9 +69,9 @@ class RevokerTest(RevokerBase):
def tearDown(self):
shutil.rmtree(self.backup_dir)
@mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("acme.client.Client.revoke")
@mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_key_all(self, mock_display, mock_net):
def test_revoke_by_key_all(self, mock_display, mock_acme):
mock_display().confirm_revocation.return_value = True
self.revoker.revoke_from_key(self.key)
@ -81,7 +81,7 @@ class RevokerTest(RevokerBase):
for i in xrange(2):
self.assertFalse(self._backups_exist(self.certs[i].get_row()))
self.assertEqual(mock_net.call_count, 2)
self.assertEqual(mock_acme.call_count, 2)
@mock.patch("letsencrypt.revoker.OpenSSL.crypto.load_privatekey")
def test_revoke_by_invalid_keys(self, mock_load_privatekey):
@ -93,13 +93,12 @@ class RevokerTest(RevokerBase):
self.assertRaises(
errors.RevokerError, self.revoker.revoke_from_key, self.key)
@mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("acme.client.Client.revoke")
@mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_wrong_key(self, mock_display, mock_net):
def test_revoke_by_wrong_key(self, mock_display, mock_acme):
mock_display().confirm_revocation.return_value = True
key_path = pkg_resources.resource_filename(
"acme.jose", os.path.join("testdata", "rsa256_key.pem"))
key_path = test_util.vector_path("rsa256_key.pem")
wrong_key = le_util.Key(key_path, open(key_path).read())
self.revoker.revoke_from_key(wrong_key)
@ -107,11 +106,11 @@ class RevokerTest(RevokerBase):
# Nothing was removed
self.assertEqual(len(self._get_rows()), 2)
# No revocation went through
self.assertEqual(mock_net.call_count, 0)
self.assertEqual(mock_acme.call_count, 0)
@mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("acme.client.Client.revoke")
@mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_cert(self, mock_display, mock_net):
def test_revoke_by_cert(self, mock_display, mock_acme):
mock_display().confirm_revocation.return_value = True
self.revoker.revoke_from_cert(self.paths[1])
@ -124,11 +123,11 @@ class RevokerTest(RevokerBase):
self.assertTrue(self._backups_exist(row0))
self.assertFalse(self._backups_exist(row1))
self.assertEqual(mock_net.call_count, 1)
self.assertEqual(mock_acme.call_count, 1)
@mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("acme.client.Client.revoke")
@mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_cert_not_found(self, mock_display, mock_net):
def test_revoke_by_cert_not_found(self, mock_display, mock_acme):
mock_display().confirm_revocation.return_value = True
self.revoker.revoke_from_cert(self.paths[0])
@ -143,11 +142,11 @@ class RevokerTest(RevokerBase):
self.assertTrue(self._backups_exist(row1))
self.assertFalse(self._backups_exist(row0))
self.assertEqual(mock_net.call_count, 1)
self.assertEqual(mock_acme.call_count, 1)
@mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("acme.client.Client.revoke")
@mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_menu(self, mock_display, mock_net):
def test_revoke_by_menu(self, mock_display, mock_acme):
mock_display().confirm_revocation.return_value = True
mock_display.display_certs.side_effect = [
(display_util.HELP, 0),
@ -165,13 +164,13 @@ class RevokerTest(RevokerBase):
self.assertFalse(self._backups_exist(row0))
self.assertTrue(self._backups_exist(row1))
self.assertEqual(mock_net.call_count, 1)
self.assertEqual(mock_acme.call_count, 1)
self.assertEqual(mock_display.more_info_cert.call_count, 1)
@mock.patch("letsencrypt.revoker.logger")
@mock.patch("letsencrypt.network.Network.revoke")
@mock.patch("acme.client.Client.revoke")
@mock.patch("letsencrypt.revoker.revocation")
def test_revoke_by_menu_delete_all(self, mock_display, mock_net, mock_log):
def test_revoke_by_menu_delete_all(self, mock_display, mock_acme, mock_log):
mock_display().confirm_revocation.return_value = True
mock_display.display_certs.return_value = (display_util.OK, 0)
@ -183,7 +182,7 @@ class RevokerTest(RevokerBase):
for i in xrange(2):
self.assertFalse(self._backups_exist(self.certs[i].get_row()))
self.assertEqual(mock_net.call_count, 2)
self.assertEqual(mock_acme.call_count, 2)
# Info is called when there aren't any certs left...
self.assertTrue(mock_log.info.called)
@ -395,22 +394,14 @@ class CertTest(unittest.TestCase):
def create_revoker_certs():
"""Create a few revoker.Cert objects."""
cert0_path = test_util.vector_path("cert.pem")
cert1_path = test_util.vector_path("cert-san.pem")
key_path = test_util.vector_path("rsa512_key.pem")
from letsencrypt.revoker import Cert
base_package = "letsencrypt.tests"
cert0_path = pkg_resources.resource_filename(
base_package, os.path.join("testdata", "cert.pem"))
cert1_path = pkg_resources.resource_filename(
base_package, os.path.join("testdata", "cert-san.pem"))
cert0 = Cert(cert0_path)
cert1 = Cert(cert1_path)
key_path = pkg_resources.resource_filename(
base_package, os.path.join("testdata", "rsa512_key.pem"))
return [cert0_path, cert1_path], [cert0, cert1], key_path

View file

@ -0,0 +1 @@
../../acme/acme/test_util.py

BIN
letsencrypt/tests/testdata/cert.der vendored Normal file

Binary file not shown.

View file

@ -0,0 +1,6 @@
-----BEGIN RSA PRIVATE KEY-----
MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh
AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N
E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3
rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt
-----END RSA PRIVATE KEY-----

View file

@ -1,9 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM
8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/
1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz
bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp
J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG
SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd
pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA==
MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79
vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn
elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc
mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp
Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj
8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq
6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,9 @@
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAPS2EXFRNza/qpXnnBHF/CcFQ543htV+7nLAmrLrmTNHtPXJmLlM
8SJDIzv/ceAFXL110VzxFfi81lpH5E5c0TMCAwEAAQJBALmppYQ/JVARjWBcsEm/
1/bXBJ127YLv4gQIY5baL4r6IdEE33OXMTTmD9wf+ajuq1eaH0htHkwhOvREu0sz
bskCIQD/Cg+xhEVLcwK3pFp3afPIhj1IPFiL3Uy/nqyMZ6O/RQIhAPWiDBofp7Cp
J4dGZs+hkRySq/IOeeRJlNK1Pq64nToXAiBZ7+te1100YSd5KT051SRB94zO13EG
SZESFduVW8rz3QIgK+tLiqg6TYYRQUi/PUTAM4GuKNuZw828RGiPyqHLywUCIQCd
pkZrNphL/y0D7HSbPIfZzD90M2V8tUjlK0BTqk1bHA==
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,2 @@
recursive-include letsencrypt_apache/tests/testdata *
include letsencrypt_apache/options-ssl-apache.conf

Some files were not shown because too many files have changed in this diff Show more