Skip to content

Commit

Permalink
Merge remote-tracking branch 'github/letsencrypt/master' into lint
Browse files Browse the repository at this point in the history
  • Loading branch information
kuba committed Sep 11, 2015
2 parents 0ebef62 + e3b4805 commit 33c2aed
Show file tree
Hide file tree
Showing 39 changed files with 420 additions and 113 deletions.
27 changes: 21 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
language: python

go:
- 1.5

services:
- rabbitmq
- mysql

# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
before_install:
- travis_retry sudo ./bootstrap/ubuntu.sh
- travis_retry sudo apt-get install --no-install-recommends nginx-light openssl
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5)"'

# using separate envs with different TOXENVs creates 4x1 Travis build
Expand All @@ -24,14 +19,34 @@ env:
matrix:
- TOXENV=py26 BOULDER_INTEGRATION=1
- TOXENV=py27 BOULDER_INTEGRATION=1
- TOXENV=py33
- TOXENV=py34
- TOXENV=lint
- TOXENV=cover

# make sure simplehttp simple verification works (custom /etc/hosts)
sudo: false # containers
addons:
# make sure simplehttp simple verification works (custom /etc/hosts)
hosts:
- le.wtf
mariadb: "10.0"
apt:
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
- lsb-release
- python
- python-dev
- python-virtualenv
- gcc
- dialog
- libaugeas0
- libssl-dev
- libffi-dev
- ca-certificates
# For letsencrypt-nginx integration testing
- nginx-light
- openssl
# For Boulder integration testing
- rsyslog

install: "travis_retry pip install tox coveralls"
before_script: '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/boulder-start.sh'
Expand Down
6 changes: 3 additions & 3 deletions acme/acme/challenges_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ def test_check_validation_wrong_fields(self):
account_public_key=account_key.public_key()))

@mock.patch("acme.challenges.requests.get")
def test_simple_verify_good_token(self, mock_get):
def test_simple_verify_good_validation(self, mock_get):
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
for resp in self.resp_http, self.resp_https:
mock_get.reset_mock()
Expand All @@ -156,9 +156,9 @@ def test_simple_verify_good_token(self, mock_get):
"local", self.chall), verify=False)

@mock.patch("acme.challenges.requests.get")
def test_simple_verify_bad_token(self, mock_get):
def test_simple_verify_bad_validation(self, mock_get):
mock_get.return_value = mock.MagicMock(
text=self.chall.token + "!", headers=self.good_headers)
text="!", headers=self.good_headers)
self.assertFalse(self.resp_http.simple_verify(
self.chall, "local", None))

Expand Down
34 changes: 24 additions & 10 deletions acme/acme/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
import logging
import time

import six
from six.moves import http_client # pylint: disable=import-error

import OpenSSL
import requests
import six
import sys
import werkzeug

from acme import errors
Expand All @@ -19,8 +20,9 @@

logger = logging.getLogger(__name__)

# Python does not validate certificates by default before version 2.7.9
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if six.PY2:
if sys.version_info < (2, 7, 9): # pragma: no cover
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()


Expand All @@ -31,7 +33,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
Clean up raised error types hierarchy, document, and handle (wrap)
instances of `.DeserializationError` raised in `from_json()`.
:ivar str new_reg_uri: Location of new-reg
:ivar messages.Directory directory:
:ivar key: `.JWK` (private)
:ivar alg: `.JWASignature`
:ivar bool verify_ssl: Verify SSL certificates?
Expand All @@ -42,12 +44,23 @@ class Client(object): # pylint: disable=too-many-instance-attributes
"""
DER_CONTENT_TYPE = 'application/pkix-cert'

def __init__(self, new_reg_uri, key, alg=jose.RS256,
verify_ssl=True, net=None):
self.new_reg_uri = new_reg_uri
def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True,
net=None):
"""Initialize.
:param directory: Directory Resource (`.messages.Directory`) or
URI from which the resource will be downloaded.
"""
self.key = key
self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net

if isinstance(directory, six.string_types):
self.directory = messages.Directory.from_json(
self.net.get(directory).json())
else:
self.directory = directory

@classmethod
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
terms_of_service=None):
Expand Down Expand Up @@ -81,7 +94,7 @@ def register(self, new_reg=None):
new_reg = messages.NewRegistration() if new_reg is None else new_reg
assert isinstance(new_reg, messages.NewRegistration)

response = self.net.post(self.new_reg_uri, new_reg)
response = self.net.post(self.directory[new_reg], new_reg)
# TODO: handle errors
assert response.status_code == http_client.CREATED

Expand Down Expand Up @@ -440,8 +453,9 @@ def revoke(self, cert):
:raises .ClientError: If revocation is unsuccessful.
"""
response = self.net.post(messages.Revocation.url(self.new_reg_uri),
messages.Revocation(certificate=cert))
response = self.net.post(self.directory[messages.Revocation],
messages.Revocation(certificate=cert),
content_type=None)
if response.status_code != http_client.OK:
raise errors.ClientError(
'Successful revocation must return HTTP OK status')
Expand Down Expand Up @@ -559,7 +573,7 @@ def head(self, *args, **kwargs):
"""Send HEAD request without checking the response.
Note, that `_check_response` is not called, as it is expected
that status code other than successfuly 2xx will be returned, or
that status code other than successfully 2xx will be returned, or
messages2.Error will be raised by the server.
"""
Expand Down
19 changes: 15 additions & 4 deletions acme/acme/client_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ def setUp(self):
self.net.post.return_value = self.response
self.net.get.return_value = self.response

self.directory = messages.Directory({
messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg',
messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert',
})

from acme.client import Client
self.client = Client(
new_reg_uri='https://www.letsencrypt-demo.org/acme/new-reg',
key=KEY, alg=jose.RS256, net=self.net)
directory=self.directory, key=KEY, alg=jose.RS256, net=self.net)

self.identifier = messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value='example.com')
Expand Down Expand Up @@ -72,6 +76,13 @@ def setUp(self):
uri='https://www.letsencrypt-demo.org/acme/cert/1',
cert_chain_uri='https://www.letsencrypt-demo.org/ca')

def test_init_downloads_directory(self):
uri = 'http://www.letsencrypt-demo.org/directory'
from acme.client import Client
self.client = Client(
directory=uri, key=KEY, alg=jose.RS256, net=self.net)
self.net.get.assert_called_once_with(uri)

def test_register(self):
# "Instance of 'Field' has no to_json/update member" bug:
# pylint: disable=no-member
Expand Down Expand Up @@ -348,8 +359,8 @@ def test_fetch_chain_no_up_link(self):

def test_revoke(self):
self.client.revoke(self.certr.body)
self.net.post.assert_called_once_with(messages.Revocation.url(
self.client.new_reg_uri), mock.ANY)
self.net.post.assert_called_once_with(
self.directory[messages.Revocation], mock.ANY, content_type=None)

def test_revoke_bad_status_raises_error(self):
self.response.status_code = http_client.METHOD_NOT_ALLOWED
Expand Down
9 changes: 5 additions & 4 deletions acme/acme/crypto_util_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ def test_probe_ok(self):
def test_probe_not_recognized_name(self):
self.assertRaises(errors.Error, self._probe, b'bar')

def test_probe_connection_error(self):
self._probe(b'foo')
time.sleep(1) # TODO: avoid race conditions in other way
self.assertRaises(errors.Error, self._probe, b'bar')
# TODO: py33/py34 tox hangs forever on do_hendshake in second probe
#def probe_connection_error(self):
# self._probe(b'foo')
# #time.sleep(1) # TODO: avoid race conditions in other way
# self.assertRaises(errors.Error, self._probe, b'bar')


class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
Expand Down
2 changes: 1 addition & 1 deletion acme/acme/jose/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class JSONDeSerializable(object):
be encoded into a JSON document. **Full serialization** produces
a Python object composed of only basic types as required by the
:ref:`conversion table <conversion-table>`. **Partial
serialization** (acomplished by :meth:`to_partial_json`)
serialization** (accomplished by :meth:`to_partial_json`)
produces a Python object that might also be built from other
:class:`JSONDeSerializable` objects.
Expand Down
2 changes: 1 addition & 1 deletion acme/acme/jose/jws.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class Header(json_util.JSONObjectWithFields):
.. warning:: This class does not support any extensions through
the "crit" (Critical) Header Parameter (4.1.11) and as a
conforming implementation, :meth:`from_json` treats its
occurence as an error. Please subclass if you seek for
occurrence as an error. Please subclass if you seek for
a different behaviour.
:ivar x5tS256: "x5t#S256"
Expand Down
4 changes: 2 additions & 2 deletions acme/acme/jose/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods
"""Wrapper for `cryptography` RSA keys.
Wraps around:
- `cryptography.hazmat.primitives.assymetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.assymetric.RSAPublicKey`
- `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey`
- `cryptography.hazmat.primitives.asymmetric.RSAPublicKey`
"""

Expand Down
70 changes: 55 additions & 15 deletions acme/acme/messages.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
"""ACME protocol messages."""
import collections

from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error

from acme import challenges
from acme import fields
from acme import jose
from acme import util


class Error(jose.JSONObjectWithFields, Exception):
Expand Down Expand Up @@ -128,6 +127,56 @@ class Identifier(jose.JSONObjectWithFields):
value = jose.Field('value')


class Directory(jose.JSONDeSerializable):
"""Directory."""

_REGISTERED_TYPES = {}

@classmethod
def _canon_key(cls, key):
return getattr(key, 'resource_type', key)

@classmethod
def register(cls, resource_body_cls):
"""Register resource."""
assert resource_body_cls.resource_type not in cls._REGISTERED_TYPES
cls._REGISTERED_TYPES[resource_body_cls.resource_type] = resource_body_cls
return resource_body_cls

def __init__(self, jobj):
canon_jobj = util.map_keys(jobj, self._canon_key)
if not set(canon_jobj).issubset(self._REGISTERED_TYPES):
# TODO: acme-spec is not clear about this: 'It is a JSON
# dictionary, whose keys are the "resource" values listed
# in {{https-requests}}'z
raise ValueError('Wrong directory fields')
# TODO: check that everything is an absolute URL; acme-spec is
# not clear on that
self._jobj = canon_jobj

def __getattr__(self, name):
try:
return self[name.replace('_', '-')]
except KeyError as error:
raise AttributeError(str(error))

def __getitem__(self, name):
try:
return self._jobj[self._canon_key(name)]
except KeyError:
raise KeyError('Directory field not found')

def to_partial_json(self):
return self._jobj

@classmethod
def from_json(cls, jobj):
try:
return cls(jobj)
except ValueError as error:
raise jose.DeserializationError(str(error))


class Resource(jose.JSONObjectWithFields):
"""ACME Resource.
Expand Down Expand Up @@ -217,6 +266,7 @@ def emails(self):
return self._filter_contact(self.email_prefix)


@Directory.register
class NewRegistration(Registration):
"""New registration."""
resource_type = 'new-reg'
Expand Down Expand Up @@ -332,6 +382,7 @@ def resolved_combinations(self):
for combo in self.combinations)


@Directory.register
class NewAuthorization(Authorization):
"""New authorization."""
resource_type = 'new-authz'
Expand All @@ -349,6 +400,7 @@ class AuthorizationResource(ResourceWithURI):
new_cert_uri = jose.Field('new_cert_uri')


@Directory.register
class CertificateRequest(jose.JSONObjectWithFields):
"""ACME new-cert request.
Expand All @@ -374,6 +426,7 @@ class CertificateResource(ResourceWithURI):
authzrs = jose.Field('authzrs')


@Directory.register
class Revocation(jose.JSONObjectWithFields):
"""Revocation message.
Expand All @@ -385,16 +438,3 @@ class Revocation(jose.JSONObjectWithFields):
resource = fields.Resource(resource_type)
certificate = jose.Field(
'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert)

# TODO: acme-spec#138, this allows only one ACME server instance per domain
PATH = '/acme/revoke-cert'
"""Path to revocation URL, see `url`"""

@classmethod
def url(cls, base):
"""Get revocation URL.
:param str base: New Registration Resource or server (root) URL.
"""
return urllib_parse.urljoin(base, cls.PATH)
Loading

0 comments on commit 33c2aed

Please sign in to comment.