Skip to content

Commit

Permalink
Merge remote-tracking branch 'github/letsencrypt/master' into pkgs_se…
Browse files Browse the repository at this point in the history
…p_prep

Conflicts:
	letsencrypt/continuity_auth.py
	letsencrypt_nginx/configurator.py
	letsencrypt_nginx/dvsni.py
	letsencrypt_nginx/tests/configurator_test.py
	letsencrypt_nginx/tests/dvsni_test.py
  • Loading branch information
kuba committed May 12, 2015
2 parents c7aff67 + eb2f019 commit 55b6198
Show file tree
Hide file tree
Showing 19 changed files with 589 additions and 112 deletions.
7 changes: 4 additions & 3 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ All you need to do is::

user@www:~$ sudo letsencrypt -d www.example.org auth

and if you have a compatbile web server (Apache), Let's Encrypt can
and if you have a compatbile web server (Apache or Nginx), Let's Encrypt can
not only get a new certificate, but also deploy it and configure your
server automatically!::

Expand Down Expand Up @@ -60,7 +60,8 @@ Current Features

* web servers supported:

- apache2.x (tested and working on Ubuntu Linux)
- apache/2.x (tested and working on Ubuntu Linux)
- nginx/0.8.48+ (tested and mostly working on Ubuntu Linux)
- standalone (runs its own webserver to prove you control the domain)

* the private key is generated locally on your system
Expand All @@ -70,7 +71,7 @@ Current Features
* can revoke certificates
* adjustable RSA key bitlength (2048 (default), 4096, ...)
* optionally can install a http->https redirect, so your site effectively
runs https only
runs https only (Apache only)
* fully automated
* configuration changes are logged and can be reverted using the CLI
* text and ncurses UI
Expand Down
5 changes: 5 additions & 0 deletions docs/api/client/proof_of_possession.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
:mod:`letsencrypt.client.proof_of_possession`
--------------------------------------------------

.. automodule:: letsencrypt.client.proof_of_possession
:members:
3 changes: 2 additions & 1 deletion letsencrypt/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ def __init__(self, config, account_, dv_auth, installer):
self.config = config

if dv_auth is not None:
cont_auth = continuity_auth.ContinuityAuthenticator(config)
cont_auth = continuity_auth.ContinuityAuthenticator(config,
installer)
self.auth_handler = auth_handler.AuthHandler(
dv_auth, cont_auth, self.network, self.account)
else:
Expand Down
21 changes: 16 additions & 5 deletions letsencrypt/continuity_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,50 @@
from letsencrypt import achallenges
from letsencrypt import errors
from letsencrypt import interfaces
from letsencrypt import proof_of_possession
from letsencrypt import recovery_token


class ContinuityAuthenticator(object):
"""IAuthenticator for
:const:`~acme.challenges.ContinuityChallenge` class challenges.
:ivar rec_token: Performs "recoveryToken" challenges
:ivar rec_token: Performs "recoveryToken" challenges.
:type rec_token: :class:`letsencrypt.recovery_token.RecoveryToken`
:ivar proof_of_pos: Performs "proofOfPossession" challenges.
:type proof_of_pos:
:class:`letsencrypt.client.proof_of_possession.Proof_of_Possession`
"""
zope.interface.implements(interfaces.IAuthenticator)

# This will have an installer soon for get_key/cert purposes
def __init__(self, config):
def __init__(self, config, installer):
"""Initialize Client Authenticator.
:param config: Configuration.
:type config: :class:`letsencrypt.interfaces.IConfig`
:param installer: Let's Encrypt Installer.
:type installer: :class:`letsencrypt.client.interfaces.IInstaller`
"""
self.rec_token = recovery_token.RecoveryToken(
config.server, config.rec_token_dir)
self.proof_of_pos = proof_of_possession.ProofOfPossession(installer)

def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
"""Return list of challenge preferences."""
return [challenges.RecoveryToken]
return [challenges.ProofOfPossession, challenges.RecoveryToken]

def perform(self, achalls):
"""Perform client specific challenges for IAuthenticator"""
responses = []
for achall in achalls:
if isinstance(achall, achallenges.RecoveryToken):
if isinstance(achall, achallenges.ProofOfPossession):
responses.append(self.proof_of_pos.perform(achall))
elif isinstance(achall, achallenges.RecoveryToken):
responses.append(self.rec_token.perform(achall))
else:
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
Expand All @@ -49,5 +60,5 @@ def cleanup(self, achalls):
for achall in achalls:
if isinstance(achall, achallenges.RecoveryToken):
self.rec_token.cleanup(achall)
else:
elif not isinstance(achall, achallenges.ProofOfPossession):
raise errors.LetsEncryptContAuthError("Unexpected Challenge")
86 changes: 86 additions & 0 deletions letsencrypt/proof_of_possession.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Proof of Possession Identifier Validation Challenge."""
import M2Crypto
import os
import zope.component

from acme import challenges
from acme import jose
from acme import other

from letsencrypt import interfaces
from letsencrypt.display import util as display_util


class ProofOfPossession(object): # pylint: disable=too-few-public-methods
"""Proof of Possession Identifier Validation Challenge.
Based on draft-barnes-acme, section 6.5.
:ivar installer: Installer object
:type installer: :class:`~letsencrypt.interfaces.IInstaller`
"""
def __init__(self, installer):
self.installer = installer

def perform(self, achall):
"""Perform the Proof of Possession Challenge.
:param achall: Proof of Possession Challenge
:type achall: :class:`letsencrypt.achallenges.ProofOfPossession`
:returns: Response or None/False if the challenge cannot be completed
:rtype: :class:`acme.challenges.ProofOfPossessionResponse`
or False
"""
if (achall.alg in [jose.HS256, jose.HS384, jose.HS512] or
not isinstance(achall.hints.jwk, achall.alg.kty)):
return None

for cert, key, _ in self.installer.get_all_certs_keys():
der_cert_key = M2Crypto.X509.load_cert(cert).get_pubkey().as_der()
try:
cert_key = achall.alg.kty.load(der_cert_key)
# If JWKES.load raises other exceptions, they should be caught here
except (IndexError, ValueError, TypeError):
continue
if cert_key == achall.hints.jwk:
return self._gen_response(achall, key)

# Is there are different prompt we should give the user?
code, key = zope.component.getUtility(
interfaces.IDisplay).input(
"Path to private key for identifier: %s " % achall.domain)
if code != display_util.CANCEL:
return self._gen_response(achall, key)

# If we get here, the key wasn't found
return False

def _gen_response(self, achall, key_path): # pylint: disable=no-self-use
"""Create the response to the Proof of Possession Challenge.
:param achall: Proof of Possession Challenge
:type achall: :class:`letsencrypt.achallenges.ProofOfPossession`
:param str key_path: Path to the key corresponding to the hinted to
public key.
:returns: Response or False if the challenge cannot be completed
:rtype: :class:`acme.challenges.ProofOfPossessionResponse`
or False
"""
if os.path.isfile(key_path):
with open(key_path, 'rb') as key:
try:
# Needs to be changed if JWKES doesn't have a key attribute
jwk = achall.alg.kty.load(key.read())
sig = other.Signature.from_msg(achall.nonce, jwk.key,
alg=achall.alg)
except (IndexError, ValueError, TypeError, jose.errors.Error):
return False
return challenges.ProofOfPossessionResponse(nonce=achall.nonce,
signature=sig)
return False
27 changes: 24 additions & 3 deletions letsencrypt/tests/continuity_auth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ def setUp(self):
from letsencrypt.continuity_auth import ContinuityAuthenticator

self.auth = ContinuityAuthenticator(
mock.MagicMock(server="demo_server.org"))
mock.MagicMock(server="demo_server.org"), None)
self.auth.rec_token.perform = mock.MagicMock(
name="rec_token_perform", side_effect=gen_client_resp)
self.auth.proof_of_pos.perform = mock.MagicMock(
name="proof_of_pos_perform", side_effect=gen_client_resp)

def test_rec_token1(self):
token = achallenges.RecoveryToken(challb=None, domain="0")
Expand All @@ -36,14 +38,33 @@ def test_rec_token5(self):
for i in xrange(5):
self.assertEqual(responses[i], "RecoveryToken%d" % i)

def test_pop_and_rec_token(self):
achalls = []
for i in xrange(4):
if i % 2 == 0:
achalls.append(achallenges.RecoveryToken(challb=None,
domain=str(i)))
else:
achalls.append(achallenges.ProofOfPossession(challb=None,
domain=str(i)))
responses = self.auth.perform(achalls)

self.assertEqual(len(responses), 4)
for i in xrange(4):
if i % 2 == 0:
self.assertEqual(responses[i], "RecoveryToken%d" % i)
else:
self.assertEqual(responses[i], "ProofOfPossession%d" % i)

def test_unexpected(self):
self.assertRaises(
errors.LetsEncryptContAuthError, self.auth.perform, [
achallenges.DVSNI(challb=None, domain="0", key="invalid_key")])

def test_chall_pref(self):
self.assertEqual(
self.auth.get_chall_pref("example.com"), [challenges.RecoveryToken])
self.auth.get_chall_pref("example.com"),
[challenges.ProofOfPossession, challenges.RecoveryToken])


class CleanupTest(unittest.TestCase):
Expand All @@ -53,7 +74,7 @@ def setUp(self):
from letsencrypt.continuity_auth import ContinuityAuthenticator

self.auth = ContinuityAuthenticator(
mock.MagicMock(server="demo_server.org"))
mock.MagicMock(server="demo_server.org"), None)
self.mock_cleanup = mock.MagicMock(name="rec_token_cleanup")
self.auth.rec_token.cleanup = self.mock_cleanup

Expand Down
87 changes: 87 additions & 0 deletions letsencrypt/tests/proof_of_possession_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""Tests for proof_of_possession.py"""
import Crypto.PublicKey.RSA
import os
import pkg_resources
import unittest

import mock

from acme import challenges
from acme import jose
from acme import messages2

from letsencrypt import achallenges
from letsencrypt import proof_of_possession
from letsencrypt.display import util as display_util


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"))
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"))
CERT3_KEY = Crypto.PublicKey.RSA.importKey(pkg_resources.resource_string(
BASE_PACKAGE, os.path.join('testdata', 'rsa512_key.pem'))).publickey()


class ProofOfPossessionTest(unittest.TestCase):
def setUp(self):
self.installer = mock.MagicMock()
certs = [CERT0_PATH, CERT1_PATH, CERT2_PATH, CERT3_PATH]
keys = [None, None, CERT2_KEY_PATH, CERT3_KEY_PATH]
self.installer.get_all_certs_keys.return_value = zip(
certs, keys, 4 * [None])
self.proof_of_pos = proof_of_possession.ProofOfPossession(
self.installer)

hints = challenges.ProofOfPossession.Hints(
jwk=jose.JWKRSA(key=CERT3_KEY), cert_fingerprints=(),
certs=(), serial_numbers=(), subject_key_identifiers=(),
issuers=(), authorized_for=())
chall = challenges.ProofOfPossession(
alg=jose.RS256, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints)
challb = messages2.ChallengeBody(
chall=chall, uri="http://example", status=messages2.STATUS_PENDING)
self.achall = achallenges.ProofOfPossession(
challb=challb, domain="example.com")

def test_perform_bad_challenge(self):
hints = challenges.ProofOfPossession.Hints(
jwk=jose.jwk.JWKOct(key=CERT3_KEY), cert_fingerprints=(),
certs=(), serial_numbers=(), subject_key_identifiers=(),
issuers=(), authorized_for=())
chall = challenges.ProofOfPossession(
alg=jose.HS512, nonce='zczv4HMLVe_0kimJ25Juig', hints=hints)
challb = messages2.ChallengeBody(
chall=chall, uri="http://example", status=messages2.STATUS_PENDING)
self.achall = achallenges.ProofOfPossession(
challb=challb, domain="example.com")
self.assertEqual(self.proof_of_pos.perform(self.achall), None)

def test_perform_no_input(self):
self.assertTrue(self.proof_of_pos.perform(self.achall).verify())

@mock.patch("letsencrypt.recovery_token.zope.component.getUtility")
def test_perform_with_input(self, mock_input):
# Remove the matching certificate
self.installer.get_all_certs_keys.return_value.pop()
mock_input().input.side_effect = [(display_util.CANCEL, ""),
(display_util.OK, CERT0_PATH),
(display_util.OK, "imaginary_file"),
(display_util.OK, CERT3_KEY_PATH)]
self.assertFalse(self.proof_of_pos.perform(self.achall))
self.assertFalse(self.proof_of_pos.perform(self.achall))
self.assertFalse(self.proof_of_pos.perform(self.achall))
self.assertTrue(self.proof_of_pos.perform(self.achall).verify())


if __name__ == "__main__":
unittest.main() # pragma: no cover
14 changes: 14 additions & 0 deletions letsencrypt/tests/testdata/dsa512_key.pem
Original file line number Diff line number Diff line change
@@ -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-----
17 changes: 17 additions & 0 deletions letsencrypt/tests/testdata/dsa_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICuDCCAnWgAwIBAgIJAPjmErVMzwVLMAsGCWCGSAFlAwQDAjB3MQswCQYDVQQG
EwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBBcmJvcjErMCkG
A1UECgwiVW5pdmVyc2l0eSBvZiBNaWNoaWdhbiBhbmQgdGhlIEVGRjEUMBIGA1UE
AwwLZXhhbXBsZS5jb20wHhcNMTUwNTEyMTUzOTQzWhcNMTUwNjExMTUzOTQzWjB3
MQswCQYDVQQGEwJVUzERMA8GA1UECAwITWljaGlnYW4xEjAQBgNVBAcMCUFubiBB
cmJvcjErMCkGA1UECgwiVW5pdmVyc2l0eSBvZiBNaWNoaWdhbiBhbmQgdGhlIEVG
RjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wgfEwgakGByqGSM44BAEwgZ0CQQDB5sSg
YF+iQpB4AscecBkxDBhTfkgsQF1XyhSbO/uqlJVSgeKHKp+foYI6LEApI/wQlhxO
KUio9sVt8XI4+VsvAhUA2gUcQOJCCScC8qsbvykfMAl1BI8CQQCp+RrkGeX4J4Qy
nNVkas5WpkT8sV1kr15Ppi1aPOq0iR/eHBdRXEmxOcEbjGat++XjWmA0mmC731g5
io+lLSDsA0MAAkBNDYtTOMZBIzpSWNw9jkjY4P1MeRRH2Qfa22HNl3vRSgj1u2tV
pOLOCphKG6iT3iCVJA0rQf3YmBSTexwk9oCQo1AwTjAdBgNVHQ4EFgQUZ2DlTDGU
PMwTUt0KztM6IyX61BcwHwYDVR0jBBgwFoAUZ2DlTDGUPMwTUt0KztM6IyX61Bcw
DAYDVR0TBAUwAwEB/zALBglghkgBZQMEAwIDMAAwLQIVAIbMgGx+KwBr4rgqZ2Lh
AAO8TegHAhQsuxpIIIphiReoWEtEJk4TqEIz/A==
-----END CERTIFICATE-----
14 changes: 14 additions & 0 deletions letsencrypt/tests/testdata/matching_cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICNzCCAeGgAwIBAgIJALizm9Y3q620MA0GCSqGSIb3DQEBCwUAMHcxCzAJBgNV
BAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAGA1UEBwwJQW5uIEFyYm9yMSsw
KQYDVQQKDCJVbml2ZXJzaXR5IG9mIE1pY2hpZ2FuIGFuZCB0aGUgRUZGMRQwEgYD
VQQDDAtleGFtcGxlLmNvbTAeFw0xNTA1MDkwMDI0NTJaFw0xNjA1MDgwMDI0NTJa
MHcxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhNaWNoaWdhbjESMBAGA1UEBwwJQW5u
IEFyYm9yMSswKQYDVQQKDCJVbml2ZXJzaXR5IG9mIE1pY2hpZ2FuIGFuZCB0aGUg
RUZGMRQwEgYDVQQDDAtleGFtcGxlLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgC
QQD0thFxUTc2v6qV55wRxfwnBUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3Hg
BVy9ddFc8RX4vNZaR+ROXNEzAgMBAAGjUDBOMB0GA1UdDgQWBBRJieHEVSHKmBk0
mTExx1erzlylCjAfBgNVHSMEGDAWgBRJieHEVSHKmBk0mTExx1erzlylCjAMBgNV
HRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA0EABT/nlpqOaanFSLZmWIrKv0zt63k4
bmWNMA8fYT45KYpLomsW8qXdpC82IlVKfNk7fW0UYT3HOeDSJRcycxNCTQ==
-----END CERTIFICATE-----
Loading

0 comments on commit 55b6198

Please sign in to comment.