mirror of
https://github.com/certbot/certbot.git
synced 2026-07-27 00:00:44 +02:00
Merge remote-tracking branch 'letsencrypt/master'
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
# special files
|
||||
*.bat text eol=crlf
|
||||
*.jpeg binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
@@ -38,8 +38,9 @@ load-plugins=linter_plugin
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use"--disable=all --enable=classes
|
||||
# --disable=W"
|
||||
disable=fixme,locally-disabled,abstract-class-not-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name
|
||||
# abstract-class-not-used cannot be disabled locally (at least in pylint 1.4.1)
|
||||
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes
|
||||
# abstract-class-not-used cannot be disabled locally (at least in
|
||||
# pylint 1.4.1), same for abstract-class-little-used
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
@@ -39,7 +39,6 @@ addons:
|
||||
mariadb: "10.0"
|
||||
apt:
|
||||
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
|
||||
- lsb-release
|
||||
- python
|
||||
- python-dev
|
||||
- python-virtualenv
|
||||
|
||||
Vendored
+3
-6
@@ -4,14 +4,11 @@
|
||||
# Vagrantfile API/syntax version. Don't touch unless you know what you're doing!
|
||||
VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
# Setup instructions from docs/using.rst
|
||||
# Setup instructions from docs/contributing.rst
|
||||
$ubuntu_setup_script = <<SETUP_SCRIPT
|
||||
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 acme -e .[dev,docs,testing] -e letsencrypt-apache -e letsencrypt-nginx
|
||||
fi
|
||||
./bootstrap/install-deps.sh
|
||||
./bootstrap/dev/venv.sh
|
||||
SETUP_SCRIPT
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
|
||||
+176
-168
@@ -1,9 +1,11 @@
|
||||
"""ACME Identifier Validation Challenges."""
|
||||
import abc
|
||||
import functools
|
||||
import hashlib
|
||||
import logging
|
||||
import socket
|
||||
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
import OpenSSL
|
||||
import requests
|
||||
|
||||
@@ -76,26 +78,21 @@ class UnrecognizedChallenge(Challenge):
|
||||
return cls(jobj)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class SimpleHTTP(DVChallenge):
|
||||
"""ACME "simpleHttp" challenge.
|
||||
class _TokenDVChallenge(DVChallenge):
|
||||
"""DV Challenge with token.
|
||||
|
||||
:ivar unicode token:
|
||||
:ivar bytes token:
|
||||
|
||||
"""
|
||||
typ = "simpleHttp"
|
||||
|
||||
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
|
||||
"""Minimum size of the :attr:`token` in bytes."""
|
||||
|
||||
URI_ROOT_PATH = ".well-known/acme-challenge"
|
||||
"""URI root path for the server provisioned resource."""
|
||||
|
||||
# TODO: acme-spec doesn't specify token as base64-encoded value
|
||||
token = jose.Field(
|
||||
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
|
||||
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
|
||||
|
||||
# XXX: rename to ~token_good_for_url
|
||||
@property
|
||||
def good_token(self): # XXX: @token.decoder
|
||||
"""Is `token` good?
|
||||
@@ -109,124 +106,130 @@ class SimpleHTTP(DVChallenge):
|
||||
# URI_ROOT_PATH!
|
||||
return b'..' not in self.token and b'/' not in self.token
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Path (starting with '/') for provisioned resource."""
|
||||
return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')
|
||||
|
||||
class KeyAuthorizationChallengeResponse(ChallengeResponse):
|
||||
"""Response to Challenges based on Key Authorization.
|
||||
|
||||
@ChallengeResponse.register
|
||||
class SimpleHTTPResponse(ChallengeResponse):
|
||||
"""ACME "simpleHttp" challenge response.
|
||||
|
||||
:ivar bool tls:
|
||||
:param unicode key_authorization:
|
||||
|
||||
"""
|
||||
typ = "simpleHttp"
|
||||
tls = jose.Field("tls", default=True, omitempty=True)
|
||||
key_authorization = jose.Field("keyAuthorization")
|
||||
thumbprint_hash_function = hashes.SHA256
|
||||
|
||||
URI_ROOT_PATH = SimpleHTTP.URI_ROOT_PATH
|
||||
_URI_TEMPLATE = "{scheme}://{domain}/" + URI_ROOT_PATH + "/{token}"
|
||||
def verify(self, chall, account_public_key):
|
||||
"""Verify the key authorization.
|
||||
|
||||
CONTENT_TYPE = "application/jose+json"
|
||||
PORT = 80
|
||||
TLS_PORT = 443
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
"""URL scheme for the provisioned resource."""
|
||||
return "https" if self.tls else "http"
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
"""Port that the ACME client should be listening for validation."""
|
||||
return self.TLS_PORT if self.tls else self.PORT
|
||||
|
||||
def uri(self, domain, chall):
|
||||
"""Create an URI to the provisioned resource.
|
||||
|
||||
Forms an URI to the HTTPS server provisioned resource
|
||||
(containing :attr:`~SimpleHTTP.token`).
|
||||
|
||||
:param unicode domain: Domain name being verified.
|
||||
:param challenges.SimpleHTTP chall:
|
||||
|
||||
"""
|
||||
return self._URI_TEMPLATE.format(
|
||||
scheme=self.scheme, domain=domain, token=chall.encode("token"))
|
||||
|
||||
def gen_resource(self, chall):
|
||||
"""Generate provisioned resource.
|
||||
|
||||
:param challenges.SimpleHTTP chall:
|
||||
:rtype: SimpleHTTPProvisionedResource
|
||||
|
||||
"""
|
||||
return SimpleHTTPProvisionedResource(token=chall.token, tls=self.tls)
|
||||
|
||||
def gen_validation(self, chall, account_key, alg=jose.RS256, **kwargs):
|
||||
"""Generate validation.
|
||||
|
||||
:param challenges.SimpleHTTP chall:
|
||||
:param .JWK account_key: Private account key.
|
||||
:param .JWA alg:
|
||||
|
||||
:returns: `.SimpleHTTPProvisionedResource` signed in `.JWS`
|
||||
:rtype: .JWS
|
||||
|
||||
"""
|
||||
return jose.JWS.sign(
|
||||
payload=self.gen_resource(chall).json_dumps(
|
||||
sort_keys=True).encode('utf-8'),
|
||||
key=account_key, alg=alg, **kwargs)
|
||||
|
||||
def check_validation(self, validation, chall, account_public_key):
|
||||
"""Check validation.
|
||||
|
||||
:param .JWS validation:
|
||||
:param challenges.SimpleHTTP chall:
|
||||
:type account_public_key:
|
||||
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
|
||||
wrapped in `.ComparableKey`
|
||||
:param KeyAuthorization chall: Challenge that corresponds to
|
||||
this response.
|
||||
:param JWK account_public_key:
|
||||
|
||||
:return: ``True`` iff verification of the key authorization was
|
||||
successful.
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not validation.verify(key=account_public_key):
|
||||
parts = self.key_authorization.split('.') # pylint: disable=no-member
|
||||
if len(parts) != 2:
|
||||
logger.debug("Key authorization (%r) is not well formed",
|
||||
self.key_authorization)
|
||||
return False
|
||||
|
||||
try:
|
||||
resource = SimpleHTTPProvisionedResource.json_loads(
|
||||
validation.payload.decode('utf-8'))
|
||||
except jose.DeserializationError as error:
|
||||
logger.debug(error)
|
||||
if parts[0] != chall.encode("token"):
|
||||
logger.debug("Mismatching token in key authorization: "
|
||||
"%r instead of %r", parts[0], chall.encode("token"))
|
||||
return False
|
||||
|
||||
return resource.token == chall.token and resource.tls == self.tls
|
||||
thumbprint = jose.b64encode(account_public_key.thumbprint(
|
||||
hash_function=self.thumbprint_hash_function)).decode()
|
||||
if parts[1] != thumbprint:
|
||||
logger.debug("Mismatching thumbprint in key authorization: "
|
||||
"%r instead of %r", parts[0], thumbprint)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class KeyAuthorizationChallenge(_TokenDVChallenge):
|
||||
# pylint: disable=abstract-class-little-used,too-many-ancestors
|
||||
"""Challenge based on Key Authorization.
|
||||
|
||||
:param response_cls: Subclass of `KeyAuthorizationChallengeResponse`
|
||||
that will be used to generate `response`.
|
||||
|
||||
"""
|
||||
__metaclass__ = abc.ABCMeta
|
||||
|
||||
response_cls = NotImplemented
|
||||
thumbprint_hash_function = (
|
||||
KeyAuthorizationChallengeResponse.thumbprint_hash_function)
|
||||
|
||||
def key_authorization(self, account_key):
|
||||
"""Generate Key Authorization.
|
||||
|
||||
:param JWK account_key:
|
||||
:rtype unicode:
|
||||
|
||||
"""
|
||||
return self.encode("token") + "." + jose.b64encode(
|
||||
account_key.thumbprint(
|
||||
hash_function=self.thumbprint_hash_function)).decode()
|
||||
|
||||
def response(self, account_key):
|
||||
"""Generate response to the challenge.
|
||||
|
||||
:param JWK account_key:
|
||||
|
||||
:returns: Response (initialized `response_cls`) to the challenge.
|
||||
:rtype: KeyAuthorizationChallengeResponse
|
||||
|
||||
"""
|
||||
return self.response_cls(
|
||||
key_authorization=self.key_authorization(account_key))
|
||||
|
||||
@abc.abstractmethod
|
||||
def validation(self, account_key):
|
||||
"""Generate validation for the challenge.
|
||||
|
||||
Subclasses must implement this method, but they are likely to
|
||||
return completely different data structures, depending on what's
|
||||
necessary to complete the challenge. Interepretation of that
|
||||
return value must be known to the caller.
|
||||
|
||||
:param JWK account_key:
|
||||
:returns: Challenge-specific validation.
|
||||
|
||||
"""
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
|
||||
def response_and_validation(self, account_key):
|
||||
"""Generate response and validation.
|
||||
|
||||
Convenience function that return results of `response` and
|
||||
`validation`.
|
||||
|
||||
:param JWK account_key:
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
return (self.response(account_key), self.validation(account_key))
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
class HTTP01Response(KeyAuthorizationChallengeResponse):
|
||||
"""ACME http-01 challenge response."""
|
||||
typ = "http-01"
|
||||
|
||||
PORT = 80
|
||||
|
||||
def simple_verify(self, chall, domain, account_public_key, port=None):
|
||||
"""Simple verify.
|
||||
|
||||
According to the ACME specification, "the ACME server MUST
|
||||
ignore the certificate provided by the HTTPS server", so
|
||||
``requests.get`` is called with ``verify=False``.
|
||||
|
||||
:param challenges.SimpleHTTP chall: Corresponding challenge.
|
||||
:param unicode domain: Domain name being verified.
|
||||
:param account_public_key: Public key for the key pair
|
||||
being authorized. If ``None`` key verification is not
|
||||
performed!
|
||||
:type account_public_key:
|
||||
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
|
||||
wrapped in `.ComparableKey`
|
||||
:param JWK account_public_key:
|
||||
:param int port: Port used in the validation.
|
||||
|
||||
:returns: ``True`` iff validation is successful, ``False``
|
||||
@@ -234,48 +237,89 @@ class SimpleHTTPResponse(ChallengeResponse):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if not self.verify(chall, account_public_key):
|
||||
logger.debug("Verification of key authorization in response failed")
|
||||
return False
|
||||
|
||||
# TODO: ACME specification defines URI template that doesn't
|
||||
# allow to use a custom port... Make sure port is not in the
|
||||
# request URI, if it's standard.
|
||||
if port is not None and port != self.port:
|
||||
logger.warn(
|
||||
if port is not None and port != self.PORT:
|
||||
logger.warning(
|
||||
"Using non-standard port for SimpleHTTP verification: %s", port)
|
||||
domain += ":{0}".format(port)
|
||||
|
||||
uri = self.uri(domain, chall)
|
||||
uri = chall.uri(domain)
|
||||
logger.debug("Verifying %s at %s...", chall.typ, uri)
|
||||
try:
|
||||
http_response = requests.get(uri, verify=False)
|
||||
http_response = requests.get(uri)
|
||||
except requests.exceptions.RequestException as error:
|
||||
logger.error("Unable to reach %s: %s", uri, error)
|
||||
return False
|
||||
logger.debug("Received %s: %s. Headers: %s", http_response,
|
||||
http_response.text, http_response.headers)
|
||||
|
||||
if self.CONTENT_TYPE != http_response.headers.get(
|
||||
"Content-Type", self.CONTENT_TYPE):
|
||||
found_ct = http_response.headers.get(
|
||||
"Content-Type", chall.CONTENT_TYPE)
|
||||
if found_ct != chall.CONTENT_TYPE:
|
||||
logger.debug("Wrong Content-Type: found %r, expected %r",
|
||||
found_ct, chall.CONTENT_TYPE)
|
||||
return False
|
||||
|
||||
try:
|
||||
validation = jose.JWS.json_loads(http_response.text)
|
||||
except jose.DeserializationError as error:
|
||||
logger.debug(error)
|
||||
if self.key_authorization != http_response.text:
|
||||
logger.debug("Key authorization from response (%r) doesn't match "
|
||||
"HTTP response (%r)", self.key_authorization,
|
||||
http_response.text)
|
||||
return False
|
||||
|
||||
return self.check_validation(validation, chall, account_public_key)
|
||||
return True
|
||||
|
||||
|
||||
class SimpleHTTPProvisionedResource(jose.JSONObjectWithFields):
|
||||
"""SimpleHTTP provisioned resource."""
|
||||
typ = fields.Fixed("type", SimpleHTTP.typ)
|
||||
token = SimpleHTTP._fields["token"]
|
||||
# If the "tls" field is not included in the response, then
|
||||
# validation object MUST have its "tls" field set to "true".
|
||||
tls = jose.Field("tls", omitempty=False)
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class HTTP01(KeyAuthorizationChallenge):
|
||||
"""ACME http-01 challenge."""
|
||||
response_cls = HTTP01Response
|
||||
typ = response_cls.typ
|
||||
|
||||
CONTENT_TYPE = "text/plain"
|
||||
"""Only valid value for Content-Type if the header is included."""
|
||||
|
||||
URI_ROOT_PATH = ".well-known/acme-challenge"
|
||||
"""URI root path for the server provisioned resource."""
|
||||
|
||||
@property
|
||||
def path(self):
|
||||
"""Path (starting with '/') for provisioned resource.
|
||||
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return '/' + self.URI_ROOT_PATH + '/' + self.encode('token')
|
||||
|
||||
def uri(self, domain):
|
||||
"""Create an URI to the provisioned resource.
|
||||
|
||||
Forms an URI to the HTTPS server provisioned resource
|
||||
(containing :attr:`~SimpleHTTP.token`).
|
||||
|
||||
:param unicode domain: Domain name being verified.
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
return "http://" + domain + self.path
|
||||
|
||||
def validation(self, account_key):
|
||||
"""Generate validation.
|
||||
|
||||
:param JWK account_key:
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
return self.key_authorization(account_key)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class DVSNI(DVChallenge):
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class DVSNI(_TokenDVChallenge):
|
||||
"""ACME "dvsni" challenge.
|
||||
|
||||
:ivar bytes token: Random data, **not** base64-encoded.
|
||||
@@ -286,13 +330,6 @@ class DVSNI(DVChallenge):
|
||||
PORT = 443
|
||||
"""Port to perform DVSNI challenge."""
|
||||
|
||||
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
|
||||
"""Minimum size of the :attr:`token` in bytes."""
|
||||
|
||||
token = jose.Field(
|
||||
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
|
||||
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
|
||||
|
||||
def gen_response(self, account_key, alg=jose.RS256, **kwargs):
|
||||
"""Generate response.
|
||||
|
||||
@@ -406,17 +443,12 @@ class DVSNIResponse(ChallengeResponse):
|
||||
|
||||
:param .challenges.DVSNI chall: Corresponding challenge.
|
||||
:param str domain: Domain name being validated.
|
||||
:type account_public_key:
|
||||
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
|
||||
wrapped in `.ComparableKey`
|
||||
:param JWK account_public_key:
|
||||
:param OpenSSL.crypto.X509 cert: Optional certificate. If not
|
||||
provided (``None``) certificate will be retrieved using
|
||||
`probe_cert`.
|
||||
|
||||
|
||||
:returns: ``True`` iff client's control of the domain has been
|
||||
verified, ``False`` otherwise.
|
||||
:rtype: bool
|
||||
@@ -491,7 +523,7 @@ class ProofOfPossession(ContinuityChallenge):
|
||||
class Hints(jose.JSONObjectWithFields):
|
||||
"""Hints for "proofOfPossession" challenge.
|
||||
|
||||
:ivar jwk: JSON Web Key (:class:`acme.jose.JWK`)
|
||||
:ivar JWK jwk: JSON Web Key
|
||||
:ivar tuple cert_fingerprints: `tuple` of `unicode`
|
||||
:ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509`
|
||||
certificates.
|
||||
@@ -548,25 +580,14 @@ class ProofOfPossessionResponse(ChallengeResponse):
|
||||
return self.signature.verify(self.nonce)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
class DNS(DVChallenge):
|
||||
"""ACME "dns" challenge.
|
||||
|
||||
:ivar unicode token:
|
||||
|
||||
"""
|
||||
@Challenge.register # pylint: disable=too-many-ancestors
|
||||
class DNS(_TokenDVChallenge):
|
||||
"""ACME "dns" challenge."""
|
||||
typ = "dns"
|
||||
|
||||
LABEL = "_acme-challenge"
|
||||
"""Label clients prepend to the domain name being validated."""
|
||||
|
||||
TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec
|
||||
"""Minimum size of the :attr:`token` in bytes."""
|
||||
|
||||
token = jose.Field(
|
||||
"token", encoder=jose.encode_b64jose, decoder=functools.partial(
|
||||
jose.decode_b64jose, size=TOKEN_SIZE, minimum=True))
|
||||
|
||||
def gen_validation(self, account_key, alg=jose.RS256, **kwargs):
|
||||
"""Generate validation.
|
||||
|
||||
@@ -585,14 +606,7 @@ class DNS(DVChallenge):
|
||||
"""Check validation.
|
||||
|
||||
:param JWS validation:
|
||||
:type account_public_key:
|
||||
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
|
||||
wrapped in `.ComparableKey`
|
||||
|
||||
:param JWK account_public_key:
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
@@ -641,13 +655,7 @@ class DNSResponse(ChallengeResponse):
|
||||
"""Check validation.
|
||||
|
||||
:param challenges.DNS chall:
|
||||
:type account_public_key:
|
||||
`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPublicKey`
|
||||
or
|
||||
`~cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey`
|
||||
wrapped in `.ComparableKey`
|
||||
:param JWK account_public_key:
|
||||
|
||||
:rtype: bool
|
||||
|
||||
|
||||
+126
-153
@@ -14,7 +14,7 @@ from acme import test_util
|
||||
|
||||
|
||||
CERT = test_util.load_cert('cert.pem')
|
||||
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
|
||||
|
||||
|
||||
class ChallengeTest(unittest.TestCase):
|
||||
@@ -43,171 +43,149 @@ class UnrecognizedChallengeTest(unittest.TestCase):
|
||||
self.chall, UnrecognizedChallenge.from_json(self.jobj))
|
||||
|
||||
|
||||
class SimpleHTTPTest(unittest.TestCase):
|
||||
class KeyAuthorizationChallengeResponseTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import SimpleHTTP
|
||||
self.msg = SimpleHTTP(
|
||||
token=jose.decode_b64jose(
|
||||
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
|
||||
def _encode(name):
|
||||
assert name == "token"
|
||||
return "foo"
|
||||
self.chall = mock.Mock()
|
||||
self.chall.encode.side_effect = _encode
|
||||
|
||||
def test_verify_ok(self):
|
||||
from acme.challenges import KeyAuthorizationChallengeResponse
|
||||
response = KeyAuthorizationChallengeResponse(
|
||||
key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
|
||||
self.assertTrue(response.verify(self.chall, KEY.public_key()))
|
||||
|
||||
def test_verify_wrong_token(self):
|
||||
from acme.challenges import KeyAuthorizationChallengeResponse
|
||||
response = KeyAuthorizationChallengeResponse(
|
||||
key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
|
||||
self.assertFalse(response.verify(self.chall, KEY.public_key()))
|
||||
|
||||
def test_verify_wrong_thumbprint(self):
|
||||
from acme.challenges import KeyAuthorizationChallengeResponse
|
||||
response = KeyAuthorizationChallengeResponse(
|
||||
key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv')
|
||||
self.assertFalse(response.verify(self.chall, KEY.public_key()))
|
||||
|
||||
def test_verify_wrong_form(self):
|
||||
from acme.challenges import KeyAuthorizationChallengeResponse
|
||||
response = KeyAuthorizationChallengeResponse(
|
||||
key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
|
||||
self.assertFalse(response.verify(self.chall, KEY.public_key()))
|
||||
|
||||
|
||||
class HTTP01ResponseTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import HTTP01Response
|
||||
self.msg = HTTP01Response(key_authorization=u'foo')
|
||||
self.jmsg = {
|
||||
'type': 'simpleHttp',
|
||||
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
|
||||
'resource': 'challenge',
|
||||
'type': 'http-01',
|
||||
'keyAuthorization': u'foo',
|
||||
}
|
||||
|
||||
from acme.challenges import HTTP01
|
||||
self.chall = HTTP01(token=(b'x' * 16))
|
||||
self.response = self.chall.response(KEY)
|
||||
self.good_headers = {'Content-Type': HTTP01.CONTENT_TYPE}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import SimpleHTTP
|
||||
self.assertEqual(self.msg, SimpleHTTP.from_json(self.jmsg))
|
||||
from acme.challenges import HTTP01Response
|
||||
self.assertEqual(
|
||||
self.msg, HTTP01Response.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import SimpleHTTP
|
||||
hash(SimpleHTTP.from_json(self.jmsg))
|
||||
from acme.challenges import HTTP01Response
|
||||
hash(HTTP01Response.from_json(self.jmsg))
|
||||
|
||||
def test_good_token(self):
|
||||
self.assertTrue(self.msg.good_token)
|
||||
self.assertFalse(
|
||||
self.msg.update(token=b'..').good_token)
|
||||
|
||||
|
||||
class SimpleHTTPResponseTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
self.msg_http = SimpleHTTPResponse(tls=False)
|
||||
self.msg_https = SimpleHTTPResponse(tls=True)
|
||||
self.jmsg_http = {
|
||||
'resource': 'challenge',
|
||||
'type': 'simpleHttp',
|
||||
'tls': False,
|
||||
}
|
||||
self.jmsg_https = {
|
||||
'resource': 'challenge',
|
||||
'type': 'simpleHttp',
|
||||
'tls': True,
|
||||
}
|
||||
|
||||
from acme.challenges import SimpleHTTP
|
||||
self.chall = SimpleHTTP(token=(b"x" * 16))
|
||||
self.resp_http = SimpleHTTPResponse(tls=False)
|
||||
self.resp_https = SimpleHTTPResponse(tls=True)
|
||||
self.good_headers = {'Content-Type': SimpleHTTPResponse.CONTENT_TYPE}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg_http, self.msg_http.to_partial_json())
|
||||
self.assertEqual(self.jmsg_https, self.msg_https.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
self.assertEqual(
|
||||
self.msg_http, SimpleHTTPResponse.from_json(self.jmsg_http))
|
||||
self.assertEqual(
|
||||
self.msg_https, SimpleHTTPResponse.from_json(self.jmsg_https))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import SimpleHTTPResponse
|
||||
hash(SimpleHTTPResponse.from_json(self.jmsg_http))
|
||||
hash(SimpleHTTPResponse.from_json(self.jmsg_https))
|
||||
|
||||
def test_scheme(self):
|
||||
self.assertEqual('http', self.msg_http.scheme)
|
||||
self.assertEqual('https', self.msg_https.scheme)
|
||||
|
||||
def test_port(self):
|
||||
self.assertEqual(80, self.msg_http.port)
|
||||
self.assertEqual(443, self.msg_https.port)
|
||||
|
||||
def test_uri(self):
|
||||
self.assertEqual(
|
||||
'http://example.com/.well-known/acme-challenge/'
|
||||
'eHh4eHh4eHh4eHh4eHh4eA', self.msg_http.uri(
|
||||
'example.com', self.chall))
|
||||
self.assertEqual(
|
||||
'https://example.com/.well-known/acme-challenge/'
|
||||
'eHh4eHh4eHh4eHh4eHh4eA', self.msg_https.uri(
|
||||
'example.com', self.chall))
|
||||
|
||||
def test_gen_check_validation(self):
|
||||
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||
self.assertTrue(self.resp_http.check_validation(
|
||||
validation=self.resp_http.gen_validation(self.chall, account_key),
|
||||
chall=self.chall, account_public_key=account_key.public_key()))
|
||||
|
||||
def test_gen_check_validation_wrong_key(self):
|
||||
key1 = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||
key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem'))
|
||||
self.assertFalse(self.resp_http.check_validation(
|
||||
validation=self.resp_http.gen_validation(self.chall, key1),
|
||||
chall=self.chall, account_public_key=key2.public_key()))
|
||||
|
||||
def test_check_validation_wrong_payload(self):
|
||||
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||
validations = tuple(
|
||||
jose.JWS.sign(payload=payload, alg=jose.RS256, key=account_key)
|
||||
for payload in (b'', b'{}', self.chall.json_dumps().encode('utf-8'),
|
||||
self.resp_http.json_dumps().encode('utf-8'))
|
||||
)
|
||||
for validation in validations:
|
||||
self.assertFalse(self.resp_http.check_validation(
|
||||
validation=validation, chall=self.chall,
|
||||
account_public_key=account_key.public_key()))
|
||||
|
||||
def test_check_validation_wrong_fields(self):
|
||||
resource = self.resp_http.gen_resource(self.chall)
|
||||
account_key = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||
validations = tuple(
|
||||
jose.JWS.sign(payload=bad_resource.json_dumps().encode('utf-8'),
|
||||
alg=jose.RS256, key=account_key)
|
||||
for bad_resource in (resource.update(tls=True),
|
||||
resource.update(token=(b'x' * 20)))
|
||||
)
|
||||
for validation in validations:
|
||||
self.assertFalse(self.resp_http.check_validation(
|
||||
validation=validation, chall=self.chall,
|
||||
account_public_key=account_key.public_key()))
|
||||
def test_simple_verify_bad_key_authorization(self):
|
||||
key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem'))
|
||||
self.response.simple_verify(self.chall, "local", key2.public_key())
|
||||
|
||||
@mock.patch("acme.challenges.requests.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()
|
||||
validation = resp.gen_validation(self.chall, account_key)
|
||||
mock_get.return_value = mock.MagicMock(
|
||||
text=validation.json_dumps(), headers=self.good_headers)
|
||||
self.assertTrue(resp.simple_verify(self.chall, "local", None))
|
||||
mock_get.assert_called_once_with(resp.uri(
|
||||
"local", self.chall), verify=False)
|
||||
validation = self.chall.validation(KEY)
|
||||
mock_get.return_value = mock.MagicMock(
|
||||
text=validation, headers=self.good_headers)
|
||||
self.assertTrue(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
mock_get.assert_called_once_with(self.chall.uri("local"))
|
||||
|
||||
@mock.patch("acme.challenges.requests.get")
|
||||
def test_simple_verify_bad_validation(self, mock_get):
|
||||
mock_get.return_value = mock.MagicMock(
|
||||
text="!", headers=self.good_headers)
|
||||
self.assertFalse(self.resp_http.simple_verify(
|
||||
self.chall, "local", None))
|
||||
self.assertFalse(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
|
||||
@mock.patch("acme.challenges.requests.get")
|
||||
def test_simple_verify_bad_content_type(self, mock_get):
|
||||
mock_get().text = self.chall.token
|
||||
self.assertFalse(self.resp_http.simple_verify(
|
||||
self.chall, "local", None))
|
||||
self.assertFalse(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
|
||||
@mock.patch("acme.challenges.requests.get")
|
||||
def test_simple_verify_connection_error(self, mock_get):
|
||||
mock_get.side_effect = requests.exceptions.RequestException
|
||||
self.assertFalse(self.resp_http.simple_verify(
|
||||
self.chall, "local", None))
|
||||
self.assertFalse(self.response.simple_verify(
|
||||
self.chall, "local", KEY.public_key()))
|
||||
|
||||
@mock.patch("acme.challenges.requests.get")
|
||||
def test_simple_verify_port(self, mock_get):
|
||||
self.resp_http.simple_verify(
|
||||
self.chall, domain="local", account_public_key=None, port=4430)
|
||||
self.assertEqual("local:4430", urllib_parse.urlparse(
|
||||
self.response.simple_verify(
|
||||
self.chall, domain="local",
|
||||
account_public_key=KEY.public_key(), port=8080)
|
||||
self.assertEqual("local:8080", urllib_parse.urlparse(
|
||||
mock_get.mock_calls[0][1][0]).netloc)
|
||||
|
||||
|
||||
class HTTP01Test(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.challenges import HTTP01
|
||||
self.msg = HTTP01(
|
||||
token=jose.decode_b64jose(
|
||||
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA'))
|
||||
self.jmsg = {
|
||||
'type': 'http-01',
|
||||
'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
|
||||
}
|
||||
|
||||
def test_path(self):
|
||||
self.assertEqual(self.msg.path, '/.well-known/acme-challenge/'
|
||||
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')
|
||||
|
||||
def test_uri(self):
|
||||
self.assertEqual(
|
||||
'http://example.com/.well-known/acme-challenge/'
|
||||
'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA',
|
||||
self.msg.uri('example.com'))
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jmsg, self.msg.to_partial_json())
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.challenges import HTTP01
|
||||
self.assertEqual(self.msg, HTTP01.from_json(self.jmsg))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.challenges import HTTP01
|
||||
hash(HTTP01.from_json(self.jmsg))
|
||||
|
||||
def test_good_token(self):
|
||||
self.assertTrue(self.msg.good_token)
|
||||
self.assertFalse(
|
||||
self.msg.update(token=b'..').good_token)
|
||||
|
||||
|
||||
class DVSNITest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
@@ -237,18 +215,15 @@ class DVSNITest(unittest.TestCase):
|
||||
jose.DeserializationError, DVSNI.from_json, self.jmsg)
|
||||
|
||||
def test_gen_response(self):
|
||||
key = jose.JWKRSA(key=KEY)
|
||||
from acme.challenges import DVSNI
|
||||
self.assertEqual(self.msg, DVSNI.json_loads(
|
||||
self.msg.gen_response(key).validation.payload.decode()))
|
||||
self.msg.gen_response(KEY).validation.payload.decode()))
|
||||
|
||||
|
||||
class DVSNIResponseTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
self.key = jose.JWKRSA(key=KEY)
|
||||
|
||||
from acme.challenges import DVSNI
|
||||
self.chall = DVSNI(
|
||||
token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e'))
|
||||
@@ -256,7 +231,7 @@ class DVSNIResponseTest(unittest.TestCase):
|
||||
from acme.challenges import DVSNIResponse
|
||||
self.validation = jose.JWS.sign(
|
||||
payload=self.chall.json_dumps(sort_keys=True).encode(),
|
||||
key=self.key, alg=jose.RS256)
|
||||
key=KEY, alg=jose.RS256)
|
||||
self.msg = DVSNIResponse(validation=self.validation)
|
||||
self.jmsg_to = {
|
||||
'resource': 'challenge',
|
||||
@@ -340,30 +315,30 @@ class DVSNIResponseTest(unittest.TestCase):
|
||||
def test_simple_verify_wrong_payload(self):
|
||||
for payload in b'', b'{}':
|
||||
msg = self.msg.update(validation=jose.JWS.sign(
|
||||
payload=payload, key=self.key, alg=jose.RS256))
|
||||
payload=payload, key=KEY, alg=jose.RS256))
|
||||
self.assertFalse(msg.simple_verify(
|
||||
self.chall, self.domain, self.key.public_key()))
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
|
||||
def test_simple_verify_wrong_token(self):
|
||||
msg = self.msg.update(validation=jose.JWS.sign(
|
||||
payload=self.chall.update(token=(b'b' * 20)).json_dumps().encode(),
|
||||
key=self.key, alg=jose.RS256))
|
||||
key=KEY, alg=jose.RS256))
|
||||
self.assertFalse(msg.simple_verify(
|
||||
self.chall, self.domain, self.key.public_key()))
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
|
||||
@mock.patch('acme.challenges.DVSNIResponse.verify_cert', autospec=True)
|
||||
def test_simple_verify(self, mock_verify_cert):
|
||||
mock_verify_cert.return_value = mock.sentinel.verification
|
||||
self.assertEqual(mock.sentinel.verification, self.msg.simple_verify(
|
||||
self.chall, self.domain, self.key.public_key(),
|
||||
self.chall, self.domain, KEY.public_key(),
|
||||
cert=mock.sentinel.cert))
|
||||
mock_verify_cert.assert_called_once_with(self.msg, mock.sentinel.cert)
|
||||
|
||||
def test_simple_verify_false_on_probe_error(self):
|
||||
chall = mock.Mock()
|
||||
chall.probe_cert.side_effect = errors.Error
|
||||
@mock.patch('acme.challenges.DVSNIResponse.probe_cert')
|
||||
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
|
||||
mock_probe_cert.side_effect = errors.Error
|
||||
self.assertFalse(self.msg.simple_verify(
|
||||
self.chall, self.domain, self.key.public_key()))
|
||||
self.chall, self.domain, KEY.public_key()))
|
||||
|
||||
|
||||
class RecoveryContactTest(unittest.TestCase):
|
||||
@@ -442,7 +417,7 @@ class RecoveryContactResponseTest(unittest.TestCase):
|
||||
class ProofOfPossessionHintsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
jwk = jose.JWKRSA(key=KEY.public_key())
|
||||
jwk = KEY.public_key()
|
||||
issuers = (
|
||||
'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA',
|
||||
'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure',
|
||||
@@ -511,7 +486,7 @@ class ProofOfPossessionTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.challenges import ProofOfPossession
|
||||
hints = ProofOfPossession.Hints(
|
||||
jwk=jose.JWKRSA(key=KEY.public_key()), cert_fingerprints=(),
|
||||
jwk=KEY.public_key(), cert_fingerprints=(),
|
||||
certs=(), serial_numbers=(), subject_key_identifiers=(),
|
||||
issuers=(), authorized_for=())
|
||||
self.msg = ProofOfPossession(
|
||||
@@ -551,7 +526,7 @@ class ProofOfPossessionResponseTest(unittest.TestCase):
|
||||
# nonce and challenge nonce are the same, don't make the same
|
||||
# mistake here...
|
||||
signature = other.Signature(
|
||||
alg=jose.RS256, jwk=jose.JWKRSA(key=KEY.public_key()),
|
||||
alg=jose.RS256, jwk=KEY.public_key(),
|
||||
sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83'
|
||||
b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap'
|
||||
b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde'
|
||||
@@ -659,14 +634,12 @@ class DNSTest(unittest.TestCase):
|
||||
class DNSResponseTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.key = jose.JWKRSA(key=KEY)
|
||||
|
||||
from acme.challenges import DNS
|
||||
self.chall = DNS(token=jose.b64decode(
|
||||
b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA"))
|
||||
self.validation = jose.JWS.sign(
|
||||
payload=self.chall.json_dumps(sort_keys=True).encode(),
|
||||
key=self.key, alg=jose.RS256)
|
||||
key=KEY, alg=jose.RS256)
|
||||
|
||||
from acme.challenges import DNSResponse
|
||||
self.msg = DNSResponse(validation=self.validation)
|
||||
@@ -694,7 +667,7 @@ class DNSResponseTest(unittest.TestCase):
|
||||
|
||||
def test_check_validation(self):
|
||||
self.assertTrue(
|
||||
self.msg.check_validation(self.chall, self.key.public_key()))
|
||||
self.msg.check_validation(self.chall, KEY.public_key()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -47,6 +47,8 @@ class JWK(json_util.TypedJSONObjectWithFields):
|
||||
|
||||
https://tools.ietf.org/html/rfc7638
|
||||
|
||||
:returns bytes:
|
||||
|
||||
"""
|
||||
digest = hashes.Hash(hash_function(), backend=default_backend())
|
||||
digest.update(json.dumps(
|
||||
|
||||
@@ -104,7 +104,7 @@ class Header(json_util.JSONObjectWithFields):
|
||||
.. todo:: Supports only "jwk" header parameter lookup.
|
||||
|
||||
:returns: (Public) key found in the header.
|
||||
:rtype: :class:`acme.jose.jwk.JWK`
|
||||
:rtype: .JWK
|
||||
|
||||
:raises acme.jose.errors.Error: if key could not be found
|
||||
|
||||
@@ -194,8 +194,7 @@ class Signature(json_util.JSONObjectWithFields):
|
||||
def verify(self, payload, key=None):
|
||||
"""Verify.
|
||||
|
||||
:param key: Key used for verification.
|
||||
:type key: :class:`acme.jose.jwk.JWK`
|
||||
:param JWK key: Key used for verification.
|
||||
|
||||
"""
|
||||
key = self.combined.find_key() if key is None else key
|
||||
@@ -208,8 +207,7 @@ class Signature(json_util.JSONObjectWithFields):
|
||||
protect=frozenset(), **kwargs):
|
||||
"""Sign.
|
||||
|
||||
:param key: Key for signature.
|
||||
:type key: :class:`acme.jose.jwk.JWK`
|
||||
:param JWK key: Key for signature.
|
||||
|
||||
"""
|
||||
assert isinstance(key, alg.kty)
|
||||
|
||||
@@ -278,7 +278,7 @@ class AuthorizationTest(unittest.TestCase):
|
||||
self.challbs = (
|
||||
ChallengeBody(
|
||||
uri='http://challb1', status=STATUS_VALID,
|
||||
chall=challenges.SimpleHTTP(token=b'IlirfxKKXAsHtmzK29Pj8A')),
|
||||
chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')),
|
||||
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
|
||||
chall=challenges.DNS(
|
||||
token=b'DGyRejmCefe7v4NfDGDKfA')),
|
||||
|
||||
+13
-41
@@ -4,7 +4,6 @@ import collections
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import six
|
||||
@@ -50,62 +49,35 @@ class ACMEServerMixin: # pylint: disable=old-style-class
|
||||
server_version = "ACME client standalone challenge solver"
|
||||
allow_reuse_address = True
|
||||
|
||||
def __init__(self):
|
||||
self._stopped = False
|
||||
|
||||
def serve_forever2(self):
|
||||
"""Serve forever, until other thread calls `shutdown2`."""
|
||||
logger.debug("Starting server at %s:%d...",
|
||||
*self.socket.getsockname()[:2])
|
||||
while not self._stopped:
|
||||
self.handle_request()
|
||||
|
||||
def shutdown2(self):
|
||||
"""Shutdown server loop from `serve_forever2`."""
|
||||
self._stopped = True
|
||||
|
||||
# dummy request to terminate last server_forever2.handle_request()
|
||||
sock = socket.socket()
|
||||
try:
|
||||
sock.connect(self.socket.getsockname())
|
||||
except socket.error:
|
||||
pass # thread is probably already finished
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
self.server_close()
|
||||
|
||||
|
||||
class DVSNIServer(TLSServer, ACMEServerMixin):
|
||||
"""DVSNI Server."""
|
||||
|
||||
def __init__(self, server_address, certs):
|
||||
ACMEServerMixin.__init__(self)
|
||||
TLSServer.__init__(
|
||||
self, server_address, socketserver.BaseRequestHandler, certs=certs)
|
||||
|
||||
|
||||
class SimpleHTTPServer(BaseHTTPServer.HTTPServer, ACMEServerMixin):
|
||||
"""SimpleHTTP Server."""
|
||||
class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin):
|
||||
"""HTTP01 Server."""
|
||||
|
||||
def __init__(self, server_address, resources):
|
||||
ACMEServerMixin.__init__(self)
|
||||
BaseHTTPServer.HTTPServer.__init__(
|
||||
self, server_address, SimpleHTTPRequestHandler.partial_init(
|
||||
self, server_address, HTTP01RequestHandler.partial_init(
|
||||
simple_http_resources=resources))
|
||||
|
||||
|
||||
class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""SimpleHTTP challenge handler.
|
||||
class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
"""HTTP01 challenge handler.
|
||||
|
||||
Adheres to the stdlib's `socketserver.BaseRequestHandler` interface.
|
||||
|
||||
:ivar set simple_http_resources: A set of `SimpleHTTPResource`
|
||||
:ivar set simple_http_resources: A set of `HTTP01Resource`
|
||||
objects. TODO: better name?
|
||||
|
||||
"""
|
||||
SimpleHTTPResource = collections.namedtuple(
|
||||
"SimpleHTTPResource", "chall response validation")
|
||||
HTTP01Resource = collections.namedtuple(
|
||||
"HTTP01Resource", "chall response validation")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.simple_http_resources = kwargs.pop("simple_http_resources", set())
|
||||
@@ -114,7 +86,7 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
def do_GET(self): # pylint: disable=invalid-name,missing-docstring
|
||||
if self.path == "/":
|
||||
self.handle_index()
|
||||
elif self.path.startswith("/" + challenges.SimpleHTTP.URI_ROOT_PATH):
|
||||
elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH):
|
||||
self.handle_simple_http_resource()
|
||||
else:
|
||||
self.handle_404()
|
||||
@@ -134,15 +106,15 @@ class SimpleHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
self.wfile.write(b"404")
|
||||
|
||||
def handle_simple_http_resource(self):
|
||||
"""Handle SimpleHTTP provisioned resources."""
|
||||
"""Handle HTTP01 provisioned resources."""
|
||||
for resource in self.simple_http_resources:
|
||||
if resource.chall.path == self.path:
|
||||
logger.debug("Serving SimpleHTTP with token %r",
|
||||
logger.debug("Serving HTTP01 with token %r",
|
||||
resource.chall.encode("token"))
|
||||
self.send_response(http_client.OK)
|
||||
self.send_header("Content-type", resource.response.CONTENT_TYPE)
|
||||
self.send_header("Content-type", resource.chall.CONTENT_TYPE)
|
||||
self.end_headers()
|
||||
self.wfile.write(resource.validation.json_dumps().encode())
|
||||
self.wfile.write(resource.validation.encode())
|
||||
return
|
||||
else: # pylint: disable=useless-else-on-loop
|
||||
logger.debug("No resources to serve")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Tests for acme.standalone."""
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import threading
|
||||
import tempfile
|
||||
import time
|
||||
@@ -29,54 +28,6 @@ class TLSServerTest(unittest.TestCase):
|
||||
server.server_close() # pylint: disable=no-member
|
||||
|
||||
|
||||
class ACMEServerMixinTest(unittest.TestCase):
|
||||
"""Tests for acme.standalone.ACMEServerMixin."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.standalone import ACMEServerMixin
|
||||
|
||||
class _MockHandler(socketserver.BaseRequestHandler):
|
||||
# pylint: disable=missing-docstring,no-member,no-init
|
||||
|
||||
def handle(self):
|
||||
self.request.sendall(b"DONE")
|
||||
|
||||
class _MockServer(socketserver.TCPServer, ACMEServerMixin):
|
||||
def __init__(self, *args, **kwargs):
|
||||
socketserver.TCPServer.__init__(self, *args, **kwargs)
|
||||
ACMEServerMixin.__init__(self)
|
||||
|
||||
self.server = _MockServer(("", 0), _MockHandler)
|
||||
|
||||
def _busy_wait(self): # pragma: no cover
|
||||
# This function is used to avoid race conditions in tests, but
|
||||
# not all of the functionality is always used, hence "no
|
||||
# cover"
|
||||
while True:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
# pylint: disable=no-member
|
||||
sock.connect(self.server.socket.getsockname())
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
sock.recv(4) # wait until handle_request is actually called
|
||||
break
|
||||
finally:
|
||||
sock.close()
|
||||
time.sleep(1)
|
||||
|
||||
def test_serve_shutdown(self):
|
||||
thread = threading.Thread(target=self.server.serve_forever2)
|
||||
thread.start()
|
||||
self._busy_wait()
|
||||
self.server.shutdown2()
|
||||
|
||||
def test_shutdown2_not_running(self):
|
||||
self.server.shutdown2()
|
||||
self.server.shutdown2()
|
||||
|
||||
|
||||
class DVSNIServerTest(unittest.TestCase):
|
||||
"""Test for acme.standalone.DVSNIServer."""
|
||||
|
||||
@@ -89,42 +40,38 @@ class DVSNIServerTest(unittest.TestCase):
|
||||
from acme.standalone import DVSNIServer
|
||||
self.server = DVSNIServer(("", 0), certs=self.certs)
|
||||
# pylint: disable=no-member
|
||||
self.thread = threading.Thread(target=self.server.handle_request)
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown2()
|
||||
self.server.shutdown() # pylint: disable=no-member
|
||||
self.thread.join()
|
||||
|
||||
def test_init(self):
|
||||
# pylint: disable=protected-access
|
||||
self.assertFalse(self.server._stopped)
|
||||
|
||||
def test_dvsni(self):
|
||||
def test_it(self):
|
||||
host, port = self.server.socket.getsockname()[:2]
|
||||
cert = crypto_util.probe_sni(b'localhost', host=host, port=port)
|
||||
cert = crypto_util.probe_sni(b'localhost', host=host, port=port, timeout=1)
|
||||
self.assertEqual(jose.ComparableX509(cert),
|
||||
jose.ComparableX509(self.certs[b'localhost'][1]))
|
||||
|
||||
|
||||
class SimpleHTTPServerTest(unittest.TestCase):
|
||||
"""Tests for acme.standalone.SimpleHTTPServer."""
|
||||
class HTTP01ServerTest(unittest.TestCase):
|
||||
"""Tests for acme.standalone.HTTP01Server."""
|
||||
|
||||
def setUp(self):
|
||||
self.account_key = jose.JWK.load(
|
||||
test_util.load_vector('rsa1024_key.pem'))
|
||||
self.resources = set()
|
||||
|
||||
from acme.standalone import SimpleHTTPServer
|
||||
self.server = SimpleHTTPServer(('', 0), resources=self.resources)
|
||||
from acme.standalone import HTTP01Server
|
||||
self.server = HTTP01Server(('', 0), resources=self.resources)
|
||||
|
||||
# pylint: disable=no-member
|
||||
self.port = self.server.socket.getsockname()[1]
|
||||
self.thread = threading.Thread(target=self.server.handle_request)
|
||||
self.thread = threading.Thread(target=self.server.serve_forever)
|
||||
self.thread.start()
|
||||
|
||||
def tearDown(self):
|
||||
self.server.shutdown2()
|
||||
self.server.shutdown() # pylint: disable=no-member
|
||||
self.thread.join()
|
||||
|
||||
def test_index(self):
|
||||
@@ -139,25 +86,24 @@ class SimpleHTTPServerTest(unittest.TestCase):
|
||||
'http://localhost:{0}/foo'.format(self.port), verify=False)
|
||||
self.assertEqual(response.status_code, http_client.NOT_FOUND)
|
||||
|
||||
def _test_simple_http(self, add):
|
||||
chall = challenges.SimpleHTTP(token=(b'x' * 16))
|
||||
response = challenges.SimpleHTTPResponse(tls=False)
|
||||
def _test_http01(self, add):
|
||||
chall = challenges.HTTP01(token=(b'x' * 16))
|
||||
response, validation = chall.response_and_validation(self.account_key)
|
||||
|
||||
from acme.standalone import SimpleHTTPRequestHandler
|
||||
resource = SimpleHTTPRequestHandler.SimpleHTTPResource(
|
||||
chall=chall, response=response, validation=response.gen_validation(
|
||||
chall, self.account_key))
|
||||
from acme.standalone import HTTP01RequestHandler
|
||||
resource = HTTP01RequestHandler.HTTP01Resource(
|
||||
chall=chall, response=response, validation=validation)
|
||||
if add:
|
||||
self.resources.add(resource)
|
||||
return resource.response.simple_verify(
|
||||
resource.chall, 'localhost', self.account_key.public_key(),
|
||||
port=self.port)
|
||||
|
||||
def test_simple_http_found(self):
|
||||
self.assertTrue(self._test_simple_http(add=True))
|
||||
def test_http01_found(self):
|
||||
self.assertTrue(self._test_http01(add=True))
|
||||
|
||||
def test_simple_http_not_found(self):
|
||||
self.assertFalse(self._test_simple_http(add=False))
|
||||
def test_http01_not_found(self):
|
||||
self.assertFalse(self._test_http01(add=False))
|
||||
|
||||
|
||||
class TestSimpleDVSNIServer(unittest.TestCase):
|
||||
|
||||
Vendored
+3
-3
@@ -4,12 +4,12 @@ to use appropriate extension for vector filenames: .pem for PEM and
|
||||
|
||||
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
|
||||
for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done
|
||||
|
||||
and for the CSR:
|
||||
|
||||
openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
|
||||
openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der
|
||||
|
||||
and for the certificate:
|
||||
|
||||
openssl req -key rsa512_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
|
||||
openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der
|
||||
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+27
@@ -0,0 +1,27 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEA8HwZMHeImB/iM8/n8CTCR4KeYQB2gLGO3v8xLms+PWH3Zbxc
|
||||
dVtEn25Y34scIh+iOuEXBcSBalBddLHKBGVN3nCfmpupoLm52xgRG44q9OWODpg4
|
||||
FSi4afqVw2agMx0RHi0v3GVcdpqB83UW42kK1ESZHUuq7mxLg8u3IMYZFm6Amsf+
|
||||
YQjBbDNn8NczJOFhsExP2EdM5ykgM1Om8aqTqqPMgPub68/r4Sym+BjLnvRq5Qtz
|
||||
h/jCfOBIIpAwg3lj7l8OyE3kkD3ALtuiuminNUqLHEkUaLq/Xiv8V8mvnrhG7h3Q
|
||||
+L1Xc707P0dz5YM5XxTMhmUE1cae/lQ0KbNrpwIDAQABAoIBAAiDXCDrGlrIRimv
|
||||
YnaN1pLRfOnSKl/D6VrbjdIm2b0yip9/W4aMBJHgRiUjt4s9s3CCJ1585lftIGHR
|
||||
KWWecHM/aWb/u7GE4Z9v6qsfDUY+GhlKKjIVjvGxfTu9lk446TI4R0l2DR/luFP2
|
||||
ASlrvoZlJ0ZyN0rZapLv0zvFx32Tukd+3rcMmXfHl7aRGMZG1YTKNmBJ4d9iJ6cP
|
||||
HG3fgSzLQMPLNO/20MzbXdREG5FNQtwaMuFnIcVbtMCvc/71lQQEfANMLCUweEed
|
||||
YWGOjgDeh+731nJsopel+2TSTgnf5VhcFrgChZZdqeKvP+HbXjTE2VkWo7BrzoM7
|
||||
xICYBwECgYEA/ZF/JOjZfwIMUdikv8vldJzRMdFryl4NJWnh4NeThNOEpUcpxnyK
|
||||
wyMnnQaGJa51u9EEnzl0sZ2h2ODjD6KFpz6fkWaVRq5SWalVPAoKZGaoPZV3IUOI
|
||||
8Tm0xkXho+A/FUUEcxCLME+3V9EdPfHaVRJOrbfDyxvNhsj4w9F0aAkCgYEA8sp7
|
||||
XTrolOknJGv4Qt1w6gcm5+cMtLaRfi8ZHPHujl2x9eWE8/s2818az7jc0Xr/G4HQ
|
||||
NeU+3Es4BblEckSHmhUZhx26cZgkLSIIDofEtaEc6u8CyWfxsWvn3l4T3kMdeSLC
|
||||
9UoLk59AH2tkMIh8vzV8LSisLJa341lMdgryQi8CgYAlJKr7PSCe+i3Tz2hSsAts
|
||||
iYwbQBIKErzaPihYRzvUuSc1DreP26535y5mUg5UdrnISVXj/Qaa/fw3SLn6EFSD
|
||||
qyi0o9I6CE8H00YpBU+AZYk/fCV3Oe1VaJ6SbKog1zhmZTXBpSq+aO7ybi9aY5MX
|
||||
4xajW8fSeMAifk3yYTwsAQKBgErcEcOCOVpItU/uloKPYpRWFjHktK83p46fmP+q
|
||||
vOJak1d9KExOBfhuN4caucNBSE1D7l3fzE0CSEjDgg41gRYKMW/Ow8DopybfWlqY
|
||||
lBdokNEDVvmgug35dmnC2h9q1DiYdkJJTV57+Lp3U1H/k28lX59Q7h1lb1eDHic7
|
||||
YszzAoGBAOx05dhOiYbzAJSTQu3oBHFn4mTYIqCcDO6cQrEJwPKAq7mAhT0yOk9N
|
||||
CrqRV/1aes665829cyTwcAZl6nqbzHv5XjX5+g6vmooCb4oCkq49rumHjoQdrX8D
|
||||
RR5b+Spkc1jo4rctCcExzSkgo+K5N3oBVYznecje7O7Z0/qiJE/8
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -17,6 +17,12 @@ Contents:
|
||||
:members:
|
||||
|
||||
|
||||
Example client:
|
||||
|
||||
.. include:: ../examples/example_client.py
|
||||
:code: python
|
||||
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from acme import jose
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
NEW_REG_URL = 'https://www.letsencrypt-demo.org/acme/new-reg'
|
||||
DIRECTORY_URL = 'https://acme-staging.api.letsencrypt.org/directory'
|
||||
BITS = 2048 # minimum for Boulder
|
||||
DOMAIN = 'example1.com' # example.com is ignored by Boulder
|
||||
|
||||
@@ -24,7 +24,7 @@ key = jose.JWKRSA(key=rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=BITS,
|
||||
backend=default_backend()))
|
||||
acme = client.Client(NEW_REG_URL, key)
|
||||
acme = client.Client(DIRECTORY_URL, key)
|
||||
|
||||
regr = acme.register()
|
||||
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
|
||||
@@ -58,6 +58,7 @@ setup(
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
|
||||
+20
-22
@@ -1,29 +1,27 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Tested with:
|
||||
# - Manjaro 15.09 (x86_64)
|
||||
# - ArchLinux (x86_64)
|
||||
|
||||
# Both "gcc-multilib" and "gcc" packages provide gcc. If user already has
|
||||
# "gcc-multilib" installed, let's stick to their choice
|
||||
if pacman -Qc gcc-multilib &>/dev/null
|
||||
then
|
||||
GCC_PACKAGE="gcc-multilib";
|
||||
else
|
||||
GCC_PACKAGE="gcc";
|
||||
fi
|
||||
|
||||
#
|
||||
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
|
||||
# only "virtualenv2" binary, not "virtualenv" necessary in
|
||||
# ./bootstrap/dev/_common_venv.sh
|
||||
pacman -S --needed \
|
||||
git \
|
||||
python2 \
|
||||
python-virtualenv \
|
||||
"$GCC_PACKAGE" \
|
||||
dialog \
|
||||
augeas \
|
||||
openssl \
|
||||
libffi \
|
||||
ca-certificates \
|
||||
pkg-config \
|
||||
|
||||
deps="
|
||||
git
|
||||
python2
|
||||
python-virtualenv
|
||||
gcc
|
||||
dialog
|
||||
augeas
|
||||
openssl
|
||||
libffi
|
||||
ca-certificates
|
||||
pkg-config
|
||||
"
|
||||
|
||||
missing=$(pacman -T $deps)
|
||||
|
||||
if [ "$missing" ]; then
|
||||
pacman -S --needed $missing
|
||||
fi
|
||||
|
||||
@@ -33,7 +33,7 @@ if apt-cache show python-virtualenv > /dev/null ; then
|
||||
fi
|
||||
|
||||
apt-get install -y --no-install-recommends \
|
||||
git-core \
|
||||
git \
|
||||
python \
|
||||
python-dev \
|
||||
$virtualenv \
|
||||
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
PACKAGES="dev-vcs/git
|
||||
dev-lang/python:2.7
|
||||
dev-python/virtualenv
|
||||
dev-util/dialog
|
||||
app-admin/augeas
|
||||
dev-libs/openssl
|
||||
dev-libs/libffi
|
||||
app-misc/ca-certificates
|
||||
virtual/pkgconfig"
|
||||
|
||||
case "$PACKAGE_MANAGER" in
|
||||
(paludis)
|
||||
cave resolve --keep-targets if-possible $PACKAGES -x
|
||||
;;
|
||||
(pkgcore)
|
||||
pmerge --noreplace $PACKAGES
|
||||
;;
|
||||
(portage|*)
|
||||
emerge --noreplace $PACKAGES
|
||||
;;
|
||||
esac
|
||||
@@ -21,7 +21,6 @@ $tool install -y \
|
||||
python \
|
||||
python-devel \
|
||||
python-virtualenv \
|
||||
python-devel \
|
||||
gcc \
|
||||
dialog \
|
||||
augeas-libs \
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
_gentoo_common.sh
|
||||
@@ -23,6 +23,9 @@ elif [ -f /etc/arch-release ] ; then
|
||||
elif [ -f /etc/redhat-release ] ; then
|
||||
echo "Bootstrapping dependencies for RedHat-based OSes..."
|
||||
$SUDO $BOOTSTRAP/_rpm_common.sh
|
||||
elif [ -f /etc/gentoo-release ] ; then
|
||||
echo "Bootstrapping dependencies for Gentoo-based OSes..."
|
||||
$SUDO $BOOTSTRAP/_gentoo_common.sh
|
||||
elif uname | grep -iq FreeBSD ; then
|
||||
echo "Bootstrapping dependencies for FreeBSD..."
|
||||
$SUDO $BOOTSTRAP/freebsd.sh
|
||||
|
||||
+3
-3
@@ -25,9 +25,9 @@ pip install -U letsencrypt letsencrypt-apache # letsencrypt-nginx
|
||||
echo
|
||||
echo "Congratulations, Let's Encrypt has been successfully installed/updated!"
|
||||
echo
|
||||
echo -n "Your prompt should now be prepended with ($VENV_NAME). Next "
|
||||
echo -n "time, if the prompt is different, 'source' this script again "
|
||||
echo -n "before running 'letsencrypt'."
|
||||
printf "%s" "Your prompt should now be prepended with ($VENV_NAME). Next "
|
||||
printf "time, if the prompt is different, 'source' this script again "
|
||||
printf "before running 'letsencrypt'."
|
||||
echo
|
||||
echo
|
||||
echo "You can now run 'letsencrypt --help'."
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
============
|
||||
Ciphersuites
|
||||
============
|
||||
|
||||
.. contents:: Table of Contents
|
||||
:local:
|
||||
|
||||
|
||||
.. _ciphersuites:
|
||||
|
||||
Introduction
|
||||
============
|
||||
|
||||
Autoupdates
|
||||
-----------
|
||||
|
||||
Within certain limits, TLS server software can choose what kind of
|
||||
cryptography to use when a client connects. These choices can affect
|
||||
security, compatibility, and performance in complex ways. Most of
|
||||
these options are independent of a particular certificate. The Let's
|
||||
Encrypt client tries to provide defaults that we think are most useful
|
||||
to our users.
|
||||
|
||||
As described below, the Let's Encrypt client will default to modifying
|
||||
server software's cryptographic settings to keep these up-to-date with
|
||||
what we think are appropriate defaults when new versions of the Let's
|
||||
Encrypt client are installed (for example, by an operating system package
|
||||
manager).
|
||||
|
||||
When this feature is implemented, this document will be updated
|
||||
to describe how to disable these automatic changes.
|
||||
|
||||
|
||||
Cryptographic choices
|
||||
---------------------
|
||||
|
||||
Software that uses cryptography must inevitably make choices about what
|
||||
kind of cryptography to use and how. These choices entail assumptions
|
||||
about how well particular cryptographic mechanisms resist attack, and what
|
||||
trade-offs are available and appropriate. The choices are constrained
|
||||
by compatibility issues (in order to interoperate with other software,
|
||||
an implementation must agree to use cryptographic mechanisms that the
|
||||
other side also supports) and protocol issues (cryptographic mechanisms
|
||||
must be specified in protocols and there must be a way to agree to use
|
||||
them in a particular context).
|
||||
|
||||
The best choices for a particular application may change over time in
|
||||
response to new research, new standardization events, changes in computer
|
||||
hardware, and changes in the prevalence of legacy software. Much important
|
||||
research on cryptanalysis and cryptographic vulnerabilities is unpublished
|
||||
because many researchers have been working in the interest of improving
|
||||
some entities' communications security while weakening, or failing to
|
||||
improve, others' security. But important information that improves our
|
||||
understanding of the state of the art is published regularly.
|
||||
|
||||
When enabling TLS support in a compatible web server (which is a separate
|
||||
step from obtaining a certificate), Let's Encrypt has the ability to
|
||||
update that web server's TLS configuration. Again, this is *different
|
||||
from the cryptographic particulars of the certificate itself*; the
|
||||
certificate as of the initial release will be RSA-signed using one of
|
||||
Let's Encrypt's 2048-bit RSA keys, and will describe the subscriber's
|
||||
RSA public key ("subject public key") of at least 2048 bits, which is
|
||||
used for key establishment.
|
||||
|
||||
Note that the subscriber's RSA public key can be used in a wide variety
|
||||
of key establishment methods, most of which do not use RSA directly
|
||||
for key exchange, but only for authenticating the server! For example,
|
||||
in DHE and ECDHE key exchanges, the subject public key is just used to
|
||||
sign other parameters for authentication. You do not have to "use RSA"
|
||||
for other purposes just because you're using an RSA key for authentication.
|
||||
|
||||
The certificate doesn't specify other cryptographic or ciphersuite
|
||||
particulars; for example, it doesn't say whether or not parties should
|
||||
use a particular symmetric algorithm like 3DES, or what cipher modes
|
||||
they should use. All of these details are negotiated between client
|
||||
and server independent of the content of the ciphersuite. The
|
||||
Let's Encrypt project hopes to provide useful defaults that reflect
|
||||
good security choices with respect to the publicly-known state of the
|
||||
art. However, the Let's Encrypt certificate authority does *not*
|
||||
dictate end-users' security policy, and any site is welcome to change
|
||||
its preferences in accordance with its own policy or its administrators'
|
||||
preferences, and use different cryptographic mechanisms or parameters,
|
||||
or a different priority order, than the defaults provided by the Let's
|
||||
Encrypt client.
|
||||
|
||||
If you don't use the Let's Encrypt client to configure your server
|
||||
directly, because the client doesn't integrate with your server software
|
||||
or because you chose not to use this integration, then the cryptographic
|
||||
defaults haven't been modified, and the cryptography chosen by the server
|
||||
will still be whatever the default for your software was. For example,
|
||||
if you obtain a certificate using *standalone* mode and then manually
|
||||
install it in an IMAP or LDAP server, your cryptographic settings will
|
||||
not be modified by the client in any way.
|
||||
|
||||
|
||||
Sources of defaults
|
||||
-------------------
|
||||
|
||||
Initially, the Let's Encrypt client will configure users' servers to
|
||||
use the cryptographic defaults recommended by the Mozilla project.
|
||||
These settings are well-reasoned recommendations that carefully
|
||||
consider client software compatibility. They are described at
|
||||
|
||||
https://wiki.mozilla.org/Security/Server_Side_TLS
|
||||
|
||||
and the version implemented by the Let's Encrypt client will be the
|
||||
version that was most current as of the release date of each client
|
||||
version. Mozilla offers three seperate sets of cryptographic options,
|
||||
which trade off security and compatibility differently. These are
|
||||
referred to as as the "Modern", "Intermediate", and "Old" configurations
|
||||
(in order from most secure to least secure, and least-backwards compatible
|
||||
to most-backwards compatible). The client will follow the Mozilla defaults
|
||||
for the *Intermediate* configuration by default, at least with regards to
|
||||
ciphersuites and TLS versions. Mozilla's web site describes which client
|
||||
software will be compatible with each configuration. You can also use
|
||||
the Qualys SSL Labs site, which the Let's Encrypt software will suggest
|
||||
when installing a certificate, to test your server and see whether it
|
||||
will be compatible with particular software versions.
|
||||
|
||||
It will be possible to ask the Let's Encrypt client to instead apply
|
||||
(and track) Modern or Old configurations.
|
||||
|
||||
The Let's Encrypt project expects to follow the Mozilla recommendations
|
||||
in the future as those recommendations are updated. (For example, some
|
||||
users have proposed prioritizing a new ciphersuite known as ``0xcc13``
|
||||
which uses the ChaCha and Poly1305 algorithms, and which is already
|
||||
implemented by the Chrome browser. Mozilla has delayed recommending
|
||||
``0xcc13`` over compatibility and standardization concerns, but is likely
|
||||
to recommend it in the future once these concerns have been addressed. At
|
||||
that point, the Let's Encrypt client would likely follow the Mozilla
|
||||
recommendations and favor the use of this ciphersuite as well.)
|
||||
|
||||
The Let's Encrypt project may deviate from the Mozilla recommendations
|
||||
in the future if good cause is shown and we believe our users'
|
||||
priorities would be well-served by doing so. In general, please address
|
||||
relevant proposals for changing priorities to the Mozilla security
|
||||
team first, before asking the Let's Encrypt project to change the
|
||||
client's priorities. The Mozilla security team is likely to have more
|
||||
resources and expertise to bring to bear on evaluating reasons why its
|
||||
recommendations should be updated.
|
||||
|
||||
The Let's Encrpyt project will entertain proposals to create a *very*
|
||||
small number of alternative configurations (apart from Modern,
|
||||
Intermediate, and Old) that there's reason to believe would be widely
|
||||
used by sysadmins; this would usually be a preferable course to modifying
|
||||
an existing configuration. For example, if many sysadmins want their
|
||||
servers configured to track a different expert recommendation, Let's
|
||||
Encrypt could add an option to do so.
|
||||
|
||||
|
||||
Resources for recommendations
|
||||
-----------------------------
|
||||
|
||||
In the course of considering how to handle this issue, we received
|
||||
recommendations with sources of expert guidance on ciphersuites and other
|
||||
cryptographic parameters. We're grateful to everyone who contributed
|
||||
suggestions. The recommendations we received are available at
|
||||
|
||||
https://github.com/letsencrypt/letsencrypt/wiki/Ciphersuite-guidance
|
||||
|
||||
Let's Encrypt client users are welcome to review these authorities to
|
||||
better inform their own cryptographic parameter choices. We also
|
||||
welcome suggestions of other resources to add to this list. Please keep
|
||||
in mind that different recommendations may reflect different priorities
|
||||
or evaluations of trade-offs, especially related to compatibility!
|
||||
|
||||
|
||||
Changing your settings
|
||||
----------------------
|
||||
|
||||
This will probably look something like
|
||||
|
||||
..code-block: shell
|
||||
|
||||
letsencrypt --cipher-recommendations mozilla-secure
|
||||
letsencrypt --cipher-recommendations mozilla-intermediate
|
||||
letsencrypt --cipher-recommendations mozilla-old
|
||||
|
||||
to track Mozilla's *Secure*, *Intermediate*, or *Old* recommendations,
|
||||
and
|
||||
|
||||
..code-block: shell
|
||||
|
||||
letsencrypt --update-ciphers on
|
||||
|
||||
to enable updating ciphers with each new Let's Encrypt client release,
|
||||
or
|
||||
|
||||
..code-block: shell
|
||||
|
||||
letsencrypt --update-ciphers off
|
||||
|
||||
to disable automatic configuration updates. These features have not yet
|
||||
been implemented and this syntax may change then they are implemented.
|
||||
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
The status of this feature is tracked as part of issue #1123 in our
|
||||
bug tracker.
|
||||
|
||||
https://github.com/letsencrypt/letsencrypt/issues/1123
|
||||
|
||||
Prior to implementation of #1123, the client does not actually modify
|
||||
ciphersuites (this is intended to be implemented as a "configuration
|
||||
enhancement", but the only configuration enhancement implemented
|
||||
so far is redirecting HTTP requests to HTTPS in web servers, the
|
||||
"redirect" enhancement). The changes here would probably be either a new
|
||||
"ciphersuite" enhancement in each plugin that provides an installer,
|
||||
or a family of enhancements, one per selectable ciphersuite configuration.
|
||||
+1
-1
@@ -315,5 +315,5 @@ texinfo_documents = [
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ the ACME server. From the protocol, there are essentially two
|
||||
different types of challenges. Challenges that must be solved by
|
||||
individual plugins in order to satisfy domain validation (subclasses
|
||||
of `~.DVChallenge`, i.e. `~.challenges.DVSNI`,
|
||||
`~.challenges.SimpleHTTPS`, `~.challenges.DNS`) and continuity specific
|
||||
`~.challenges.HTTP01`, `~.challenges.DNS`) and continuity specific
|
||||
challenges (subclasses of `~.ContinuityChallenge`,
|
||||
i.e. `~.challenges.RecoveryToken`, `~.challenges.RecoveryContact`,
|
||||
`~.challenges.ProofOfPossession`). Continuity challenges are
|
||||
@@ -312,7 +312,6 @@ synced to ``/vagrant``, so you can get started with:
|
||||
|
||||
vagrant ssh
|
||||
cd /vagrant
|
||||
./venv/bin/pip install -r requirements.txt .[dev,docs,testing]
|
||||
sudo ./venv/bin/letsencrypt
|
||||
|
||||
Support for other Linux distributions coming soon.
|
||||
|
||||
+7
-4
@@ -73,7 +73,7 @@ to, `install Docker`_, then issue the following command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
sudo docker run -it --rm -p 443:443 --name letsencrypt \
|
||||
sudo docker run -it --rm -p 443:443 -p 80:80 --name letsencrypt \
|
||||
-v "/etc/letsencrypt:/etc/letsencrypt" \
|
||||
-v "/var/lib/letsencrypt:/var/lib/letsencrypt" \
|
||||
quay.io/letsencrypt/letsencrypt:latest auth
|
||||
@@ -127,15 +127,15 @@ Officially supported plugins:
|
||||
Plugin A I Notes and status
|
||||
========== = = ================================================================
|
||||
standalone Y N Very stable. Uses port 80 (force by
|
||||
``--standalone-supported-challenges simpleHttp``) or 443
|
||||
(force by ``standalone-supported-challenges dvsni``).
|
||||
``--standalone-supported-challenges http-01``) or 443
|
||||
(force by ``--standalone-supported-challenges dvsni``).
|
||||
apache Y Y Alpha. Automates Apache installation, works fairly well but on
|
||||
Debian-based distributions only for now.
|
||||
webroot Y N Works with already running webserver, by writing necessary files
|
||||
to the disk (``--webroot-path`` should be pointed to your
|
||||
``public_html``). Currently, when multiple domains are specified
|
||||
(`-d`), they must all use the same web root path.
|
||||
manual Y N Hidden from standard UI, use with ``--a manual``. Requires to
|
||||
manual Y N Hidden from standard UI, use with ``-a manual``. Requires to
|
||||
copy and paste commands into a new terminal session. Allows to
|
||||
run client on machine different than target webserver, e.g. your
|
||||
laptop.
|
||||
@@ -163,6 +163,9 @@ sure that UI doesn't prompt for any details you can add the command to
|
||||
``crontab`` (make it less than every 90 days to avoid problems, say
|
||||
every month).
|
||||
|
||||
Please note that the CA will send notification emails to the address
|
||||
you provide if you do not renew certificates that are about to expire.
|
||||
|
||||
Let's Encrypt is working hard on automating the renewal process. Until
|
||||
the tool is ready, we are sorry for the inconvenience!
|
||||
|
||||
|
||||
@@ -313,6 +313,6 @@ texinfo_documents = [
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class ApacheDvsni(common.Dvsni):
|
||||
larger array. ApacheDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within ApacheConfigurator
|
||||
who must return all responses in order. Imagine ApacheConfigurator
|
||||
maintaining state about where all of the SimpleHTTP Challenges,
|
||||
maintaining state about where all of the http-01 Challenges,
|
||||
Dvsni Challenges belong in the response array. This is an optional
|
||||
utility.
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ class ApacheParser(object):
|
||||
"""
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
[ctl, "-D", "DUMP_RUN_CFG"],
|
||||
[ctl, "-t", "-D", "DUMP_RUN_CFG"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
stdout, stderr = proc.communicate()
|
||||
|
||||
@@ -41,6 +41,7 @@ setup(
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
|
||||
+8
-5
@@ -47,6 +47,9 @@ then
|
||||
elif [ -f /etc/redhat-release ] ; then
|
||||
echo "Bootstrapping dependencies for RedHat-based OSes..."
|
||||
$SUDO $BOOTSTRAP/_rpm_common.sh
|
||||
elif [ -f /etc/gentoo-release ] ; then
|
||||
echo "Bootstrapping dependencies for Gentoo-based OSes..."
|
||||
$SUDO $BOOTSTRAP/_gentoo_common.sh
|
||||
elif uname | grep -iq FreeBSD ; then
|
||||
echo "Bootstrapping dependencies for FreeBSD..."
|
||||
$SUDO $BOOTSTRAP/freebsd.sh
|
||||
@@ -70,7 +73,7 @@ then
|
||||
fi
|
||||
fi
|
||||
|
||||
echo -n "Updating letsencrypt and virtual environment dependencies..."
|
||||
printf "Updating letsencrypt and virtual environment dependencies..."
|
||||
if [ "$VERBOSE" = 1 ] ; then
|
||||
echo
|
||||
$VENV_BIN/pip install -U setuptools
|
||||
@@ -83,15 +86,15 @@ if [ "$VERBOSE" = 1 ] ; then
|
||||
fi
|
||||
else
|
||||
$VENV_BIN/pip install -U setuptools > /dev/null
|
||||
echo -n .
|
||||
printf .
|
||||
$VENV_BIN/pip install -U pip > /dev/null
|
||||
echo -n .
|
||||
printf .
|
||||
# nginx is buggy / disabled for now...
|
||||
$VENV_BIN/pip install -U letsencrypt > /dev/null
|
||||
echo -n .
|
||||
printf .
|
||||
$VENV_BIN/pip install -U letsencrypt-apache > /dev/null
|
||||
if $VENV_BIN/pip freeze | grep -q letsencrypt-nginx ; then
|
||||
echo -n .
|
||||
printf .
|
||||
$VENV_BIN/pip install -U letsencrypt-nginx > /dev/null
|
||||
fi
|
||||
echo
|
||||
|
||||
@@ -307,8 +307,8 @@ texinfo_documents = [
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
|
||||
'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org', None),
|
||||
'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt-apache': ('https://letsencrypt-apache.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt-nginx': ('https://letsencrypt-nginx.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ setup(
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
|
||||
@@ -306,6 +306,6 @@ texinfo_documents = [
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ class NginxDvsni(common.Dvsni):
|
||||
larger array. NginxDvsni is capable of solving many challenges
|
||||
at once which causes an indexing issue within NginxConfigurator
|
||||
who must return all responses in order. Imagine NginxConfigurator
|
||||
maintaining state about where all of the SimpleHTTP Challenges,
|
||||
maintaining state about where all of the http-01 Challenges,
|
||||
Dvsni Challenges belong in the response array. This is an optional
|
||||
utility.
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ setup(
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
|
||||
@@ -45,6 +45,15 @@ class AnnotatedChallenge(jose.ImmutableMap):
|
||||
return getattr(self.challb, name)
|
||||
|
||||
|
||||
class KeyAuthorizationAnnotatedChallenge(AnnotatedChallenge):
|
||||
"""Client annotated `KeyAuthorizationChallenge` challenge."""
|
||||
__slots__ = ('challb', 'domain', 'account_key')
|
||||
|
||||
def response_and_validation(self):
|
||||
"""Generate response and validation."""
|
||||
return self.challb.chall.response_and_validation(self.account_key)
|
||||
|
||||
|
||||
class DVSNI(AnnotatedChallenge):
|
||||
"""Client annotated "dvsni" ACME challenge.
|
||||
|
||||
@@ -76,31 +85,6 @@ class DVSNI(AnnotatedChallenge):
|
||||
return response, cert, key
|
||||
|
||||
|
||||
class SimpleHTTP(AnnotatedChallenge):
|
||||
"""Client annotated "simpleHttp" ACME challenge."""
|
||||
__slots__ = ('challb', 'domain', 'account_key')
|
||||
acme_type = challenges.SimpleHTTP
|
||||
|
||||
def gen_response_and_validation(self, tls):
|
||||
"""Generates a SimpleHTTP response and validation.
|
||||
|
||||
:param bool tls: True if TLS should be used
|
||||
|
||||
:returns: ``(response, validation)`` tuple, where ``response`` is
|
||||
an instance of `acme.challenges.SimpleHTTPResponse` and
|
||||
``validation`` is an instance of
|
||||
`acme.challenges.SimpleHTTPProvisionedResource`.
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
response = challenges.SimpleHTTPResponse(tls=tls)
|
||||
|
||||
validation = response.gen_validation(
|
||||
self.challb.chall, self.account_key)
|
||||
logger.debug("Simple HTTP validation payload: %s", validation.payload)
|
||||
return response, validation
|
||||
|
||||
|
||||
class DNS(AnnotatedChallenge):
|
||||
"""Client annotated "dns" ACME challenge."""
|
||||
__slots__ = ('challb', 'domain')
|
||||
|
||||
+11
-12
@@ -347,9 +347,6 @@ def challb_to_achall(challb, account_key, domain):
|
||||
if isinstance(chall, challenges.DVSNI):
|
||||
return achallenges.DVSNI(
|
||||
challb=challb, domain=domain, account_key=account_key)
|
||||
elif isinstance(chall, challenges.SimpleHTTP):
|
||||
return achallenges.SimpleHTTP(
|
||||
challb=challb, domain=domain, account_key=account_key)
|
||||
elif isinstance(chall, challenges.DNS):
|
||||
return achallenges.DNS(challb=challb, domain=domain)
|
||||
elif isinstance(chall, challenges.RecoveryContact):
|
||||
@@ -358,7 +355,9 @@ def challb_to_achall(challb, account_key, domain):
|
||||
elif isinstance(chall, challenges.ProofOfPossession):
|
||||
return achallenges.ProofOfPossession(
|
||||
challb=challb, domain=domain)
|
||||
|
||||
elif isinstance(chall, challenges.KeyAuthorizationChallenge):
|
||||
return achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=challb, domain=domain, account_key=account_key)
|
||||
else:
|
||||
raise errors.Error(
|
||||
"Received unsupported challenge of type: %s", chall.typ)
|
||||
@@ -486,29 +485,29 @@ def is_preferred(offered_challb, satisfied,
|
||||
|
||||
_ERROR_HELP_COMMON = (
|
||||
"To fix these errors, please make sure that your domain name was entered "
|
||||
"correctly and the DNS A record(s) for that domain contains the "
|
||||
"correctly and the DNS A record(s) for that domain contain(s) the "
|
||||
"right IP address.")
|
||||
|
||||
|
||||
_ERROR_HELP = {
|
||||
"connection":
|
||||
_ERROR_HELP_COMMON + " Additionally, please check that your computer "
|
||||
"has publicly routable IP address and no firewalls are preventing the "
|
||||
"server from communicating with the client.",
|
||||
"has a publicly routable IP address and that no firewalls are preventing "
|
||||
"the server from communicating with the client.",
|
||||
"dnssec":
|
||||
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
|
||||
"your domain, please ensure the signature is valid.",
|
||||
"your domain, please ensure that the signature is valid.",
|
||||
"malformed":
|
||||
"To fix these errors, please make sure that you did not provide any "
|
||||
"invalid information to the client and try running Let's Encrypt "
|
||||
"invalid information to the client, and try running Let's Encrypt "
|
||||
"again.",
|
||||
"serverInternal":
|
||||
"Unfortunately, an error on the ACME server prevented you from completing "
|
||||
"authorization. Please try again later.",
|
||||
"tls":
|
||||
_ERROR_HELP_COMMON + " Additionally, please check that you have an up "
|
||||
"to date TLS configuration that allows the server to communicate with "
|
||||
"the Let's Encrypt client.",
|
||||
_ERROR_HELP_COMMON + " Additionally, please check that you have an "
|
||||
"up-to-date TLS configuration that allows the server to communicate "
|
||||
"with the Let's Encrypt client.",
|
||||
"unauthorized": _ERROR_HELP_COMMON,
|
||||
"unknownHost": _ERROR_HELP_COMMON,
|
||||
}
|
||||
|
||||
+69
-34
@@ -56,7 +56,7 @@ default, it will attempt to use a webserver both for obtaining and installing
|
||||
the cert. Major SUBCOMMANDS are:
|
||||
|
||||
(default) run Obtain & install a cert in your current webserver
|
||||
auth Authenticate & obtain cert, but do not install it
|
||||
certonly Obtain cert, but do not install it (aka "auth")
|
||||
install Install a previously obtained cert in a server
|
||||
revoke Revoke a previously obtained certificate
|
||||
rollback Rollback server configuration changes made during install
|
||||
@@ -64,27 +64,41 @@ the cert. Major SUBCOMMANDS are:
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# This is the short help for letsencrypt --help, where we disable argparse
|
||||
# altogether
|
||||
USAGE = SHORT_USAGE + """Choice of server for authentication/installation:
|
||||
USAGE = SHORT_USAGE + """Choice of server plugins for obtaining and installing cert:
|
||||
|
||||
--apache Use the Apache plugin for authentication & installation
|
||||
--nginx Use the Nginx plugin for authentication & installation
|
||||
%s
|
||||
--standalone Run a standalone webserver for authentication
|
||||
OR:
|
||||
--authenticator standalone --installer nginx
|
||||
%s
|
||||
|
||||
OR use different servers to obtain (authenticate) the cert and then install it:
|
||||
|
||||
--authenticator standalone --installer apache
|
||||
|
||||
More detailed help:
|
||||
|
||||
-h, --help [topic] print this message, or detailed help on a topic;
|
||||
the available topics are:
|
||||
|
||||
all, apache, automation, manual, nginx, paths, security, testing, or any of
|
||||
the subcommands
|
||||
all, automation, paths, security, testing, or any of the subcommands or
|
||||
plugins (certonly, install, nginx, apache, standalone, etc)
|
||||
"""
|
||||
|
||||
|
||||
def usage_strings(plugins):
|
||||
"""Make usage strings late so that plugins can be initialised late"""
|
||||
if "nginx" in plugins:
|
||||
nginx_doc = "--nginx Use the Nginx plugin for authentication & installation"
|
||||
else:
|
||||
nginx_doc = "(nginx support is experimental, buggy, and not installed by default)"
|
||||
if "apache" in plugins:
|
||||
apache_doc = "--apache Use the Apache plugin for authentication & installation"
|
||||
else:
|
||||
apache_doc = "(the apache plugin is not installed)"
|
||||
return USAGE % (apache_doc, nginx_doc), SHORT_USAGE
|
||||
|
||||
|
||||
def _find_domains(args, installer):
|
||||
if args.domains is None:
|
||||
domains = display_ops.choose_names(installer)
|
||||
@@ -358,11 +372,11 @@ def diagnose_configurator_problem(cfg_type, requested, plugins):
|
||||
if os.path.exists("/etc/debian_version"):
|
||||
# Debian... installers are at least possible
|
||||
msg = ('No installers seem to be present and working on your system; '
|
||||
'fix that or try running letsencrypt with the "auth" command')
|
||||
'fix that or try running letsencrypt with the "certonly" command')
|
||||
else:
|
||||
# XXX update this logic as we make progress on #788 and nginx support
|
||||
msg = ('No installers are available on your OS yet; try running '
|
||||
'"letsencrypt-auto auth" to get a cert you can install manually')
|
||||
'"letsencrypt-auto certonly" to get a cert you can install manually')
|
||||
else:
|
||||
msg = "{0} could not be determined or is not installed".format(cfg_type)
|
||||
raise PluginSelectionError(msg)
|
||||
@@ -377,7 +391,7 @@ def choose_configurator_plugins(args, config, plugins, verb):
|
||||
|
||||
# Which plugins do we need?
|
||||
need_inst = need_auth = (verb == "run")
|
||||
if verb == "auth":
|
||||
if verb == "certonly":
|
||||
need_auth = True
|
||||
if verb == "install":
|
||||
need_inst = True
|
||||
@@ -447,7 +461,7 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
|
||||
display_ops.success_renewal(domains)
|
||||
|
||||
|
||||
def auth(args, config, plugins):
|
||||
def obtaincert(args, config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it."""
|
||||
|
||||
if args.domains is not None and args.csr is not None:
|
||||
@@ -457,7 +471,7 @@ def auth(args, config, plugins):
|
||||
|
||||
try:
|
||||
# installers are used in auth mode to determine domain names
|
||||
installer, authenticator = choose_configurator_plugins(args, config, plugins, "auth")
|
||||
installer, authenticator = choose_configurator_plugins(args, config, plugins, "certonly")
|
||||
except PluginSelectionError, e:
|
||||
return e.message
|
||||
|
||||
@@ -481,7 +495,8 @@ def install(args, config, plugins):
|
||||
# XXX: Update for renewer/RenewableCert
|
||||
|
||||
try:
|
||||
installer, _ = choose_configurator_plugins(args, config, plugins, "auth")
|
||||
installer, _ = choose_configurator_plugins(args, config,
|
||||
plugins, "install")
|
||||
except PluginSelectionError, e:
|
||||
return e.message
|
||||
|
||||
@@ -609,7 +624,8 @@ class HelpfulArgumentParser(object):
|
||||
"""
|
||||
|
||||
# Maps verbs/subcommands to the functions that implement them
|
||||
VERBS = {"auth": auth, "config_changes": config_changes,
|
||||
VERBS = {"auth": obtaincert, "certonly": obtaincert,
|
||||
"config_changes": config_changes, "everything": run,
|
||||
"install": install, "plugins": plugins_cmd,
|
||||
"revoke": revoke, "rollback": rollback, "run": run}
|
||||
|
||||
@@ -620,8 +636,9 @@ class HelpfulArgumentParser(object):
|
||||
def __init__(self, args, plugins):
|
||||
plugin_names = [name for name, _p in plugins.iteritems()]
|
||||
self.help_topics = self.HELP_TOPICS + plugin_names + [None]
|
||||
usage, short_usage = usage_strings(plugins)
|
||||
self.parser = configargparse.ArgParser(
|
||||
usage=SHORT_USAGE,
|
||||
usage=short_usage,
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
args_for_setting_config_path=["-c", "--config"],
|
||||
default_config_files=flag_default("config_files"))
|
||||
@@ -635,12 +652,12 @@ class HelpfulArgumentParser(object):
|
||||
help1 = self.prescan_for_flag("-h", self.help_topics)
|
||||
help2 = self.prescan_for_flag("--help", self.help_topics)
|
||||
assert max(True, "a") == "a", "Gravity changed direction"
|
||||
help_arg = max(help1, help2)
|
||||
if help_arg is True:
|
||||
self.help_arg = max(help1, help2)
|
||||
if self.help_arg is True:
|
||||
# just --help with no topic; avoid argparse altogether
|
||||
print USAGE
|
||||
print usage
|
||||
sys.exit(0)
|
||||
self.visible_topics = self.determine_help_topics(help_arg)
|
||||
self.visible_topics = self.determine_help_topics(self.help_arg)
|
||||
#print self.visible_topics
|
||||
self.groups = {} # elements are added by .add_group()
|
||||
|
||||
@@ -669,7 +686,12 @@ class HelpfulArgumentParser(object):
|
||||
|
||||
for i, token in enumerate(self.args):
|
||||
if token in self.VERBS:
|
||||
self.verb = token
|
||||
verb = token
|
||||
if verb == "auth":
|
||||
verb = "certonly"
|
||||
if verb == "everything":
|
||||
verb = "run"
|
||||
self.verb = verb
|
||||
self.args.pop(i)
|
||||
return
|
||||
|
||||
@@ -754,6 +776,10 @@ class HelpfulArgumentParser(object):
|
||||
"""
|
||||
# topics maps each topic to whether it should be documented by
|
||||
# argparse on the command line
|
||||
if chosen_topic == "auth":
|
||||
chosen_topic = "certonly"
|
||||
if chosen_topic == "everything":
|
||||
chosen_topic = "run"
|
||||
if chosen_topic == "all":
|
||||
return dict([(t, True) for t in self.help_topics])
|
||||
elif not chosen_topic:
|
||||
@@ -828,8 +854,8 @@ def prepare_and_parse_args(plugins, args):
|
||||
helpful.add(
|
||||
"testing", "--dvsni-port", type=int, default=flag_default("dvsni_port"),
|
||||
help=config_help("dvsni_port"))
|
||||
helpful.add("testing", "--simple-http-port", type=int,
|
||||
help=config_help("simple_http_port"))
|
||||
helpful.add("testing", "--http-01-port", dest="http01_port", type=int,
|
||||
help=config_help("http01_port"))
|
||||
|
||||
helpful.add_group(
|
||||
"security", description="Security parameters & server settings")
|
||||
@@ -849,24 +875,24 @@ def prepare_and_parse_args(plugins, args):
|
||||
help="Require that all configuration files are owned by the current "
|
||||
"user; only needed if your config is somewhere unsafe like /tmp/")
|
||||
|
||||
_create_subparsers(helpful)
|
||||
_paths_parser(helpful)
|
||||
# _plugins_parsing should be the last thing to act upon the main
|
||||
# parser (--help should display plugin-specific options last)
|
||||
_plugins_parsing(helpful, plugins)
|
||||
|
||||
_create_subparsers(helpful)
|
||||
|
||||
return helpful.parse_args()
|
||||
|
||||
|
||||
def _create_subparsers(helpful):
|
||||
helpful.add_group("auth", description="Options for modifying how a cert is obtained")
|
||||
helpful.add_group("certonly", description="Options for modifying how a cert is obtained")
|
||||
helpful.add_group("install", description="Options for modifying how a cert is deployed")
|
||||
helpful.add_group("revoke", description="Options for revocation of certs")
|
||||
helpful.add_group("rollback", description="Options for reverting config changes")
|
||||
helpful.add_group("plugins", description="Plugin options")
|
||||
|
||||
helpful.add("auth",
|
||||
helpful.add("certonly",
|
||||
"--csr", type=read_file,
|
||||
help="Path to a Certificate Signing Request (CSR) in DER"
|
||||
" format; note that the .csr file *must* contain a Subject"
|
||||
@@ -890,24 +916,33 @@ def _create_subparsers(helpful):
|
||||
def _paths_parser(helpful):
|
||||
add = helpful.add
|
||||
verb = helpful.verb
|
||||
if verb == "help":
|
||||
verb = helpful.help_arg
|
||||
helpful.add_group(
|
||||
"paths", description="Arguments changing execution paths & servers")
|
||||
|
||||
cph = "Path to where cert is saved (with auth), installed (with install --csr) or revoked."
|
||||
if verb == "auth":
|
||||
add("paths", "--cert-path", default=flag_default("auth_cert_path"), help=cph)
|
||||
cph = "Path to where cert is saved (with auth --csr), installed from or revoked."
|
||||
section = "paths"
|
||||
if verb in ("install", "revoke", "certonly"):
|
||||
section = verb
|
||||
if verb == "certonly":
|
||||
add(section, "--cert-path", default=flag_default("auth_cert_path"), help=cph)
|
||||
elif verb == "revoke":
|
||||
add("paths", "--cert-path", type=read_file, required=True, help=cph)
|
||||
add(section, "--cert-path", type=read_file, required=True, help=cph)
|
||||
else:
|
||||
add("paths", "--cert-path", help=cph, required=(verb == "install"))
|
||||
add(section, "--cert-path", help=cph, required=(verb == "install"))
|
||||
|
||||
section = "paths"
|
||||
if verb in ("install", "revoke"):
|
||||
section = verb
|
||||
print helpful.help_arg, helpful.help_arg == "install"
|
||||
# revoke --key-path reads a file, install --key-path takes a string
|
||||
add("paths", "--key-path", type=((verb == "revoke" and read_file) or str),
|
||||
add(section, "--key-path", type=((verb == "revoke" and read_file) or str),
|
||||
required=(verb == "install"),
|
||||
help="Path to private key for cert creation or revocation (if account key is missing)")
|
||||
|
||||
default_cp = None
|
||||
if verb == "auth":
|
||||
if verb == "certonly":
|
||||
default_cp = flag_default("auth_chain_path")
|
||||
add("paths", "--fullchain-path", default=default_cp,
|
||||
help="Accompanying path to a full certificate chain (cert plus chain).")
|
||||
|
||||
@@ -326,6 +326,7 @@ class Client(object):
|
||||
key_path=os.path.abspath(privkey_path),
|
||||
chain_path=chain_path,
|
||||
fullchain_path=fullchain_path)
|
||||
self.installer.save() # needed by the Apache plugin
|
||||
|
||||
self.installer.save("Deployed Let's Encrypt Certificate")
|
||||
# sites may have been enabled / final cleanup
|
||||
|
||||
@@ -37,9 +37,9 @@ class NamespaceConfig(object):
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
if self.simple_http_port == self.dvsni_port:
|
||||
if self.http01_port == self.dvsni_port:
|
||||
raise errors.Error(
|
||||
"Trying to run SimpleHTTP and DVSNI "
|
||||
"Trying to run http-01 and DVSNI "
|
||||
"on the same port ({0})".format(self.dvsni_port))
|
||||
|
||||
def __getattr__(self, name):
|
||||
@@ -78,11 +78,11 @@ class NamespaceConfig(object):
|
||||
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
|
||||
|
||||
@property
|
||||
def simple_http_port(self): # pylint: disable=missing-docstring
|
||||
if self.namespace.simple_http_port is not None:
|
||||
return self.namespace.simple_http_port
|
||||
def http01_port(self): # pylint: disable=missing-docstring
|
||||
if self.namespace.http01_port is not None:
|
||||
return self.namespace.http01_port
|
||||
else:
|
||||
return challenges.SimpleHTTPResponse.PORT
|
||||
return challenges.HTTP01Response.PORT
|
||||
|
||||
|
||||
class RenewerConfiguration(object):
|
||||
|
||||
@@ -41,7 +41,7 @@ RENEWER_DEFAULTS = dict(
|
||||
|
||||
|
||||
EXCLUSIVE_CHALLENGES = frozenset([frozenset([
|
||||
challenges.DVSNI, challenges.SimpleHTTP])])
|
||||
challenges.DVSNI, challenges.HTTP01])])
|
||||
"""Mutually exclusive challenges."""
|
||||
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ class IConfig(zope.interface.Interface):
|
||||
"Port number to perform DVSNI challenge. "
|
||||
"Boulder in testing mode defaults to 5001.")
|
||||
|
||||
simple_http_port = zope.interface.Attribute(
|
||||
http01_port = zope.interface.Attribute(
|
||||
"Port used in the SimpleHttp challenge.")
|
||||
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ class PluginEntryPointTest(unittest.TestCase):
|
||||
|
||||
def test_description(self):
|
||||
self.assertEqual(
|
||||
"Automatically configure and run a simple webserver",
|
||||
"Automatically use a temporary webserver",
|
||||
self.plugin_ep.description)
|
||||
|
||||
def test_description_with_name(self):
|
||||
|
||||
@@ -26,18 +26,19 @@ logger = logging.getLogger(__name__)
|
||||
class Authenticator(common.Plugin):
|
||||
"""Manual Authenticator.
|
||||
|
||||
This plugin requires user's manual intervention in setting up a HTTP
|
||||
server for solving SimpleHTTP challenges and thus does not need to be
|
||||
run as a privilidged process. Alternatively shows instructions on how
|
||||
to use Python's built-in HTTP server.
|
||||
This plugin requires user's manual intervention in setting up a HTTP
|
||||
server for solving http-01 challenges and thus does not need to be
|
||||
run as a privileged process. Alternatively shows instructions on how
|
||||
to use Python's built-in HTTP server.
|
||||
|
||||
.. todo:: Support for `~.challenges.DVSNI`.
|
||||
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
hidden = True
|
||||
|
||||
description = "Manually configure and run a simple Python webserver"
|
||||
description = "Manually configure an HTTP server"
|
||||
|
||||
MESSAGE_TEMPLATE = """\
|
||||
Make sure your web server displays the following content at
|
||||
@@ -68,9 +69,9 @@ Are you OK with your IP being logged?
|
||||
# anything recursively under the cwd
|
||||
|
||||
CMD_TEMPLATE = """\
|
||||
mkdir -p {root}/public_html/{response.URI_ROOT_PATH}
|
||||
mkdir -p {root}/public_html/{achall.URI_ROOT_PATH}
|
||||
cd {root}/public_html
|
||||
echo -n {validation} > {response.URI_ROOT_PATH}/{encoded_token}
|
||||
printf "%s" {validation} > {achall.URI_ROOT_PATH}/{encoded_token}
|
||||
# run only once per server:
|
||||
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \\
|
||||
"import BaseHTTPServer, SimpleHTTPServer; \\
|
||||
@@ -95,14 +96,14 @@ s.serve_forever()" """
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring,no-self-use
|
||||
return ("This plugin requires user's manual intervention in setting "
|
||||
"up a HTTP server for solving SimpleHTTP challenges and thus "
|
||||
"does not need to be run as a privilidged process. "
|
||||
"up an HTTP server for solving http-01 challenges and thus "
|
||||
"does not need to be run as a privileged process. "
|
||||
"Alternatively shows instructions on how to use Python's "
|
||||
"built-in HTTP server.")
|
||||
|
||||
def get_chall_pref(self, domain):
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return [challenges.SimpleHTTP]
|
||||
return [challenges.HTTP01]
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
responses = []
|
||||
@@ -130,27 +131,26 @@ s.serve_forever()" """
|
||||
# same path for each challenge response would be easier for
|
||||
# users, but will not work if multiple domains point at the
|
||||
# same server: default command doesn't support virtual hosts
|
||||
response, validation = achall.gen_response_and_validation(
|
||||
tls=False) # SimpleHTTP TLS is dead: ietf-wg-acme/acme#7
|
||||
response, validation = achall.response_and_validation()
|
||||
|
||||
port = (response.port if self.config.simple_http_port is None
|
||||
else int(self.config.simple_http_port))
|
||||
port = (response.port if self.config.http01_port is None
|
||||
else int(self.config.http01_port))
|
||||
command = self.CMD_TEMPLATE.format(
|
||||
root=self._root, achall=achall, response=response,
|
||||
validation=pipes.quote(validation.json_dumps()),
|
||||
# TODO(kuba): pipes still necessary?
|
||||
validation=pipes.quote(validation),
|
||||
encoded_token=achall.chall.encode("token"),
|
||||
ct=response.CONTENT_TYPE, port=port)
|
||||
ct=achall.CONTENT_TYPE, port=port)
|
||||
if self.conf("test-mode"):
|
||||
logger.debug("Test mode. Executing the manual command: %s", command)
|
||||
# sh shipped with OS X does't support echo -n
|
||||
executable = "/bin/bash" if sys.platform == "darwin" else None
|
||||
# sh shipped with OS X does't support echo -n, but supports printf
|
||||
try:
|
||||
self._httpd = subprocess.Popen(
|
||||
command,
|
||||
# don't care about setting stdout and stderr,
|
||||
# we're in test mode anyway
|
||||
shell=True,
|
||||
executable=executable,
|
||||
executable=None,
|
||||
# "preexec_fn" is UNIX specific, but so is "command"
|
||||
preexec_fn=os.setsid)
|
||||
except OSError as error: # ValueError should not happen!
|
||||
@@ -169,13 +169,13 @@ s.serve_forever()" """
|
||||
raise errors.PluginError("Must agree to IP logging to proceed")
|
||||
|
||||
self._notify_and_wait(self.MESSAGE_TEMPLATE.format(
|
||||
validation=validation.json_dumps(), response=response,
|
||||
uri=response.uri(achall.domain, achall.challb.chall),
|
||||
ct=response.CONTENT_TYPE, command=command))
|
||||
validation=validation, response=response,
|
||||
uri=achall.chall.uri(achall.domain),
|
||||
ct=achall.CONTENT_TYPE, command=command))
|
||||
|
||||
if response.simple_verify(
|
||||
achall.chall, achall.domain,
|
||||
achall.account_key.public_key(), self.config.simple_http_port):
|
||||
achall.account_key.public_key(), self.config.http01_port):
|
||||
return response
|
||||
else:
|
||||
logger.error(
|
||||
|
||||
@@ -23,13 +23,13 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.manual import Authenticator
|
||||
self.config = mock.MagicMock(
|
||||
simple_http_port=8080, manual_test_mode=False)
|
||||
http01_port=8080, manual_test_mode=False)
|
||||
self.auth = Authenticator(config=self.config, name="manual")
|
||||
self.achalls = [achallenges.SimpleHTTP(
|
||||
challb=acme_util.SIMPLE_HTTP_P, domain="foo.com", account_key=KEY)]
|
||||
self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)]
|
||||
|
||||
config_test_mode = mock.MagicMock(
|
||||
simple_http_port=8080, manual_test_mode=True)
|
||||
http01_port=8080, manual_test_mode=True)
|
||||
self.auth_test_mode = Authenticator(
|
||||
config=config_test_mode, name="manual")
|
||||
|
||||
@@ -45,13 +45,13 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
|
||||
@mock.patch("letsencrypt.plugins.manual.zope.component.getUtility")
|
||||
@mock.patch("letsencrypt.plugins.manual.sys.stdout")
|
||||
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify")
|
||||
@mock.patch("acme.challenges.HTTP01Response.simple_verify")
|
||||
@mock.patch("__builtin__.raw_input")
|
||||
def test_perform(self, mock_raw_input, mock_verify, mock_stdout, mock_interaction):
|
||||
mock_verify.return_value = True
|
||||
mock_interaction().yesno.return_value = True
|
||||
|
||||
resp = challenges.SimpleHTTPResponse(tls=False)
|
||||
resp = self.achalls[0].response(KEY)
|
||||
self.assertEqual([resp], self.auth.perform(self.achalls))
|
||||
self.assertEqual(1, mock_raw_input.call_count)
|
||||
mock_verify.assert_called_with(
|
||||
@@ -89,7 +89,7 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
|
||||
@mock.patch("letsencrypt.plugins.manual.socket.socket")
|
||||
@mock.patch("letsencrypt.plugins.manual.time.sleep", autospec=True)
|
||||
@mock.patch("acme.challenges.SimpleHTTPResponse.simple_verify",
|
||||
@mock.patch("acme.challenges.HTTP01Response.simple_verify",
|
||||
autospec=True)
|
||||
@mock.patch("letsencrypt.plugins.manual.subprocess.Popen", autospec=True)
|
||||
def test_perform_test_mode(self, mock_popen, mock_verify, mock_sleep,
|
||||
|
||||
@@ -14,7 +14,6 @@ from acme import challenges
|
||||
from acme import crypto_util as acme_crypto_util
|
||||
from acme import standalone as acme_standalone
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import interfaces
|
||||
|
||||
@@ -33,7 +32,7 @@ class ServerManager(object):
|
||||
`acme.crypto_util.SSLSocket.certs` and
|
||||
`acme.crypto_util.SSLSocket.simple_http_resources` respectively. All
|
||||
created servers share the same certificates and resources, so if
|
||||
you're running both TLS and non-TLS instances, SimpleHTTP handlers
|
||||
you're running both TLS and non-TLS instances, HTTP01 handlers
|
||||
will serve the same URLs!
|
||||
|
||||
"""
|
||||
@@ -52,13 +51,13 @@ class ServerManager(object):
|
||||
|
||||
:param int port: Port to run the server on.
|
||||
:param challenge_type: Subclass of `acme.challenges.Challenge`,
|
||||
either `acme.challenge.SimpleHTTP` or `acme.challenges.DVSNI`.
|
||||
either `acme.challenge.HTTP01` or `acme.challenges.DVSNI`.
|
||||
|
||||
:returns: Server instance.
|
||||
:rtype: ACMEServerMixin
|
||||
|
||||
"""
|
||||
assert challenge_type in (challenges.DVSNI, challenges.SimpleHTTP)
|
||||
assert challenge_type in (challenges.DVSNI, challenges.HTTP01)
|
||||
if port in self._instances:
|
||||
return self._instances[port].server
|
||||
|
||||
@@ -66,13 +65,15 @@ class ServerManager(object):
|
||||
try:
|
||||
if challenge_type is challenges.DVSNI:
|
||||
server = acme_standalone.DVSNIServer(address, self.certs)
|
||||
else: # challenges.SimpleHTTP
|
||||
server = acme_standalone.SimpleHTTPServer(
|
||||
else: # challenges.HTTP01
|
||||
server = acme_standalone.HTTP01Server(
|
||||
address, self.simple_http_resources)
|
||||
except socket.error as error:
|
||||
raise errors.StandaloneBindError(error, port)
|
||||
|
||||
thread = threading.Thread(target=server.serve_forever2)
|
||||
thread = threading.Thread(
|
||||
# pylint: disable=no-member
|
||||
target=server.serve_forever)
|
||||
thread.start()
|
||||
|
||||
# if port == 0, then random free port on OS is taken
|
||||
@@ -90,7 +91,7 @@ class ServerManager(object):
|
||||
instance = self._instances[port]
|
||||
logger.debug("Stopping server at %s:%d...",
|
||||
*instance.server.socket.getsockname()[:2])
|
||||
instance.server.shutdown2()
|
||||
instance.server.shutdown()
|
||||
instance.thread.join()
|
||||
del self._instances[port]
|
||||
|
||||
@@ -108,7 +109,7 @@ class ServerManager(object):
|
||||
in six.iteritems(self._instances))
|
||||
|
||||
|
||||
SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.SimpleHTTP])
|
||||
SUPPORTED_CHALLENGES = set([challenges.DVSNI, challenges.HTTP01])
|
||||
|
||||
|
||||
def supported_challenges_validator(data):
|
||||
@@ -137,22 +138,22 @@ class Authenticator(common.Plugin):
|
||||
"""Standalone Authenticator.
|
||||
|
||||
This authenticator creates its own ephemeral TCP listener on the
|
||||
necessary port in order to respond to incoming DVSNI and SimpleHTTP
|
||||
necessary port in order to respond to incoming DVSNI and HTTP01
|
||||
challenges from the certificate authority. Therefore, it does not
|
||||
rely on any existing server program.
|
||||
"""
|
||||
zope.interface.implements(interfaces.IAuthenticator)
|
||||
zope.interface.classProvides(interfaces.IPluginFactory)
|
||||
|
||||
description = "Automatically configure and run a simple webserver"
|
||||
description = "Automatically use a temporary webserver"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
|
||||
# one self-signed key for all DVSNI and SimpleHTTP certificates
|
||||
# one self-signed key for all DVSNI and HTTP01 certificates
|
||||
self.key = OpenSSL.crypto.PKey()
|
||||
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, bits=2048)
|
||||
# TODO: generate only when the first SimpleHTTP challenge is solved
|
||||
# TODO: generate only when the first HTTP01 challenge is solved
|
||||
self.simple_http_cert = acme_crypto_util.gen_ss_cert(
|
||||
self.key, domains=["temp server"])
|
||||
|
||||
@@ -183,17 +184,17 @@ class Authenticator(common.Plugin):
|
||||
@property
|
||||
def _necessary_ports(self):
|
||||
necessary_ports = set()
|
||||
if challenges.SimpleHTTP in self.supported_challenges:
|
||||
necessary_ports.add(self.config.simple_http_port)
|
||||
if challenges.HTTP01 in self.supported_challenges:
|
||||
necessary_ports.add(self.config.http01_port)
|
||||
if challenges.DVSNI in self.supported_challenges:
|
||||
necessary_ports.add(self.config.dvsni_port)
|
||||
return necessary_ports
|
||||
|
||||
def more_info(self): # pylint: disable=missing-docstring
|
||||
return("This authenticator creates its own ephemeral TCP listener "
|
||||
"on the necessary port in order to respond to incoming DVSNI "
|
||||
"and SimpleHTTP challenges from the certificate authority. "
|
||||
"Therefore, it does not rely on any existing server program.")
|
||||
"on the necessary port in order to respond to incoming DVSNI "
|
||||
"and HTTP01 challenges from the certificate authority. "
|
||||
"Therefore, it does not rely on any existing server program.")
|
||||
|
||||
def prepare(self): # pylint: disable=missing-docstring
|
||||
pass
|
||||
@@ -235,13 +236,12 @@ class Authenticator(common.Plugin):
|
||||
responses = []
|
||||
|
||||
for achall in achalls:
|
||||
if isinstance(achall, achallenges.SimpleHTTP):
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
server = self.servers.run(
|
||||
self.config.simple_http_port, challenges.SimpleHTTP)
|
||||
response, validation = achall.gen_response_and_validation(
|
||||
tls=False)
|
||||
self.config.http01_port, challenges.HTTP01)
|
||||
response, validation = achall.response_and_validation()
|
||||
self.simple_http_resources.add(
|
||||
acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource(
|
||||
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
chall=achall.chall, response=response,
|
||||
validation=validation))
|
||||
cert = self.simple_http_cert
|
||||
|
||||
@@ -43,12 +43,12 @@ class ServerManagerTest(unittest.TestCase):
|
||||
self._test_run_stop(challenges.DVSNI)
|
||||
|
||||
def test_run_stop_simplehttp(self):
|
||||
self._test_run_stop(challenges.SimpleHTTP)
|
||||
self._test_run_stop(challenges.HTTP01)
|
||||
|
||||
def test_run_idempotent(self):
|
||||
server = self.mgr.run(port=0, challenge_type=challenges.SimpleHTTP)
|
||||
server = self.mgr.run(port=0, challenge_type=challenges.HTTP01)
|
||||
port = server.socket.getsockname()[1] # pylint: disable=no-member
|
||||
server2 = self.mgr.run(port=port, challenge_type=challenges.SimpleHTTP)
|
||||
server2 = self.mgr.run(port=port, challenge_type=challenges.HTTP01)
|
||||
self.assertEqual(self.mgr.running(), {port: server})
|
||||
self.assertTrue(server is server2)
|
||||
self.mgr.stop(port)
|
||||
@@ -60,7 +60,7 @@ class ServerManagerTest(unittest.TestCase):
|
||||
port = some_server.getsockname()[1]
|
||||
self.assertRaises(
|
||||
errors.StandaloneBindError, self.mgr.run, port,
|
||||
challenge_type=challenges.SimpleHTTP)
|
||||
challenge_type=challenges.HTTP01)
|
||||
self.assertEqual(self.mgr.running(), {})
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@ class SupportedChallengesValidatorTest(unittest.TestCase):
|
||||
|
||||
def test_correct(self):
|
||||
self.assertEqual("dvsni", self._call("dvsni"))
|
||||
self.assertEqual("simpleHttp", self._call("simpleHttp"))
|
||||
self.assertEqual("dvsni,simpleHttp", self._call("dvsni,simpleHttp"))
|
||||
self.assertEqual("simpleHttp,dvsni", self._call("simpleHttp,dvsni"))
|
||||
self.assertEqual("http-01", self._call("http-01"))
|
||||
self.assertEqual("dvsni,http-01", self._call("dvsni,http-01"))
|
||||
self.assertEqual("http-01,dvsni", self._call("http-01,dvsni"))
|
||||
|
||||
def test_unrecognized(self):
|
||||
assert "foo" not in challenges.Challenge.TYPES
|
||||
@@ -91,25 +91,26 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.standalone import Authenticator
|
||||
self.config = mock.MagicMock(dvsni_port=1234, simple_http_port=4321,
|
||||
standalone_supported_challenges="dvsni,simpleHttp")
|
||||
self.config = mock.MagicMock(
|
||||
dvsni_port=1234, http01_port=4321,
|
||||
standalone_supported_challenges="dvsni,http-01")
|
||||
self.auth = Authenticator(self.config, name="standalone")
|
||||
|
||||
def test_supported_challenges(self):
|
||||
self.assertEqual(self.auth.supported_challenges,
|
||||
set([challenges.DVSNI, challenges.SimpleHTTP]))
|
||||
set([challenges.DVSNI, challenges.HTTP01]))
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue(isinstance(self.auth.more_info(), six.string_types))
|
||||
|
||||
def test_get_chall_pref(self):
|
||||
self.assertEqual(set(self.auth.get_chall_pref(domain=None)),
|
||||
set([challenges.DVSNI, challenges.SimpleHTTP]))
|
||||
set([challenges.DVSNI, challenges.HTTP01]))
|
||||
|
||||
@mock.patch("letsencrypt.plugins.standalone.util")
|
||||
def test_perform_alredy_listening(self, mock_util):
|
||||
for chall, port in ((challenges.DVSNI.typ, 1234),
|
||||
(challenges.SimpleHTTP.typ, 4321)):
|
||||
(challenges.HTTP01.typ, 4321)):
|
||||
mock_util.already_listening.return_value = True
|
||||
self.config.standalone_supported_challenges = chall
|
||||
self.assertRaises(
|
||||
@@ -152,8 +153,8 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
def test_perform2(self):
|
||||
domain = b'localhost'
|
||||
key = jose.JWK.load(test_util.load_vector('rsa512_key.pem'))
|
||||
simple_http = achallenges.SimpleHTTP(
|
||||
challb=acme_util.SIMPLE_HTTP_P, domain=domain, account_key=key)
|
||||
simple_http = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.HTTP01_P, domain=domain, account_key=key)
|
||||
dvsni = achallenges.DVSNI(
|
||||
challb=acme_util.DVSNI_P, domain=domain, account_key=key)
|
||||
|
||||
@@ -167,11 +168,11 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
|
||||
self.assertTrue(isinstance(responses, list))
|
||||
self.assertEqual(2, len(responses))
|
||||
self.assertTrue(isinstance(responses[0], challenges.SimpleHTTPResponse))
|
||||
self.assertTrue(isinstance(responses[0], challenges.HTTP01Response))
|
||||
self.assertTrue(isinstance(responses[1], challenges.DVSNIResponse))
|
||||
|
||||
self.assertEqual(self.auth.servers.run.mock_calls, [
|
||||
mock.call(4321, challenges.SimpleHTTP),
|
||||
mock.call(4321, challenges.HTTP01),
|
||||
mock.call(1234, challenges.DVSNI),
|
||||
])
|
||||
self.assertEqual(self.auth.served, {
|
||||
@@ -181,8 +182,8 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
self.assertEqual(1, len(self.auth.simple_http_resources))
|
||||
self.assertEqual(2, len(self.auth.certs))
|
||||
self.assertEqual(list(self.auth.simple_http_resources), [
|
||||
acme_standalone.SimpleHTTPRequestHandler.SimpleHTTPResource(
|
||||
acme_util.SIMPLE_HTTP, responses[0], mock.ANY)])
|
||||
acme_standalone.HTTP01RequestHandler.HTTP01Resource(
|
||||
acme_util.HTTP01, responses[0], mock.ANY)])
|
||||
|
||||
def test_cleanup(self):
|
||||
self.auth.servers = mock.Mock()
|
||||
|
||||
@@ -1,4 +1,43 @@
|
||||
"""Webroot plugin."""
|
||||
"""Webroot plugin.
|
||||
|
||||
Content-Type
|
||||
------------
|
||||
|
||||
This plugin requires your webserver to use a specific `Content-Type`
|
||||
header in the HTTP response.
|
||||
|
||||
Apache2
|
||||
~~~~~~~
|
||||
|
||||
.. note:: Instructions written and tested for Debian Jessie. Other
|
||||
operating systems might use something very similar, but you might
|
||||
still need to readjust some commands.
|
||||
|
||||
Create ``/etc/apache2/conf-available/letsencrypt-simplehttp.conf``, with
|
||||
the following contents::
|
||||
|
||||
<IfModule mod_headers.c>
|
||||
<LocationMatch "/.well-known/acme-challenge/*">
|
||||
Header set Content-Type "application/jose+json"
|
||||
</LocationMatch>
|
||||
</IfModule>
|
||||
|
||||
and then run ``a2enmod headers; a2enconf letsencrypt``; depending on the
|
||||
output you will have to either ``service apache2 restart`` or ``service
|
||||
apache2 reload``.
|
||||
|
||||
nginx
|
||||
~~~~~
|
||||
|
||||
Use the following snippet in your ``server{...}`` stanza::
|
||||
|
||||
location ~ /.well-known/acme-challenge/(.*) {
|
||||
default_type application/jose+json;
|
||||
}
|
||||
|
||||
and reload your daemon.
|
||||
|
||||
"""
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
@@ -23,7 +62,7 @@ class Authenticator(common.Plugin):
|
||||
description = "Webroot Authenticator"
|
||||
|
||||
MORE_INFO = """\
|
||||
Authenticator plugin that performs SimpleHTTP challenge by saving
|
||||
Authenticator plugin that performs http-01 challenge by saving
|
||||
necessary validation resources to appropriate paths on the file
|
||||
system. It expects that there is some other HTTP server configured
|
||||
to serve all files under specified web root ({0})."""
|
||||
@@ -37,7 +76,7 @@ to serve all files under specified web root ({0})."""
|
||||
|
||||
def get_chall_pref(self, domain): # pragma: no cover
|
||||
# pylint: disable=missing-docstring,no-self-use,unused-argument
|
||||
return [challenges.SimpleHTTP]
|
||||
return [challenges.HTTP01]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Authenticator, self).__init__(*args, **kwargs)
|
||||
@@ -51,8 +90,7 @@ to serve all files under specified web root ({0})."""
|
||||
if not os.path.isdir(path):
|
||||
raise errors.PluginError(
|
||||
path + " does not exist or is not a directory")
|
||||
self.full_root = os.path.join(
|
||||
path, challenges.SimpleHTTPResponse.URI_ROOT_PATH)
|
||||
self.full_root = os.path.join(path, challenges.HTTP01.URI_ROOT_PATH)
|
||||
|
||||
logger.debug("Creating root challenges validation dir at %s",
|
||||
self.full_root)
|
||||
@@ -61,7 +99,7 @@ to serve all files under specified web root ({0})."""
|
||||
except OSError as exception:
|
||||
if exception.errno != errno.EEXIST:
|
||||
raise errors.PluginError(
|
||||
"Couldn't create root for SimpleHTTP "
|
||||
"Couldn't create root for http-01 "
|
||||
"challenge responses: {0}", exception)
|
||||
|
||||
def perform(self, achalls): # pylint: disable=missing-docstring
|
||||
@@ -72,11 +110,11 @@ to serve all files under specified web root ({0})."""
|
||||
return os.path.join(self.full_root, achall.chall.encode("token"))
|
||||
|
||||
def _perform_single(self, achall):
|
||||
response, validation = achall.gen_response_and_validation(tls=False)
|
||||
response, validation = achall.response_and_validation()
|
||||
path = self._path_for_achall(achall)
|
||||
logger.debug("Attempting to save validation to %s", path)
|
||||
with open(path, "w") as validation_file:
|
||||
validation_file.write(validation.json_dumps())
|
||||
validation_file.write(validation.encode())
|
||||
return response
|
||||
|
||||
def cleanup(self, achalls): # pylint: disable=missing-docstring
|
||||
|
||||
@@ -6,6 +6,7 @@ import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
|
||||
from letsencrypt import achallenges
|
||||
@@ -21,8 +22,8 @@ KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
class AuthenticatorTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.plugins.webroot.Authenticator."""
|
||||
|
||||
achall = achallenges.SimpleHTTP(
|
||||
challb=acme_util.SIMPLE_HTTP_P, domain=None, account_key=KEY)
|
||||
achall = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.HTTP01_P, domain=None, account_key=KEY)
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt.plugins.webroot import Authenticator
|
||||
@@ -70,9 +71,11 @@ class AuthenticatorTest(unittest.TestCase):
|
||||
self.assertEqual(1, len(responses))
|
||||
self.assertTrue(os.path.exists(self.validation_path))
|
||||
with open(self.validation_path) as validation_f:
|
||||
validation = jose.JWS.json_loads(validation_f.read())
|
||||
self.assertTrue(responses[0].check_validation(
|
||||
validation, self.achall.chall, KEY.public_key()))
|
||||
validation = validation_f.read()
|
||||
self.assertTrue(
|
||||
challenges.KeyAuthorizationChallengeResponse(
|
||||
key_authorization=validation).verify(
|
||||
self.achall.chall, KEY.public_key()))
|
||||
|
||||
self.auth.cleanup([self.achall])
|
||||
self.assertFalse(os.path.exists(self.validation_path))
|
||||
|
||||
@@ -76,7 +76,7 @@ def renew(cert, old_version):
|
||||
# was an int, not a str)
|
||||
config.rsa_key_size = int(config.rsa_key_size)
|
||||
config.dvsni_port = int(config.dvsni_port)
|
||||
config.namespace.simple_http_port = int(config.namespace.simple_http_port)
|
||||
config.namespace.http01_port = int(config.namespace.http01_port)
|
||||
zope.component.provideUtility(config)
|
||||
try:
|
||||
authenticator = plugins[renewalparams["authenticator"]]
|
||||
|
||||
+14
-15
@@ -2,7 +2,6 @@
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
import configobj
|
||||
import parsedatetime
|
||||
@@ -24,8 +23,8 @@ def config_with_defaults(config=None):
|
||||
return defaults_copy
|
||||
|
||||
|
||||
def parse_time_interval(interval, textparser=parsedatetime.Calendar()):
|
||||
"""Parse the time specified time interval.
|
||||
def add_time_interval(base_time, interval, textparser=parsedatetime.Calendar()):
|
||||
"""Parse the time specified time interval, and add it to the base_time
|
||||
|
||||
The interval can be in the English-language format understood by
|
||||
parsedatetime, e.g., '10 days', '3 weeks', '6 months', '9 hours', or
|
||||
@@ -33,15 +32,19 @@ def parse_time_interval(interval, textparser=parsedatetime.Calendar()):
|
||||
hours'. If an integer is found with no associated unit, it is
|
||||
interpreted by default as a number of days.
|
||||
|
||||
:param datetime.datetime base_time: The time to be added with the interval.
|
||||
:param str interval: The time interval to parse.
|
||||
|
||||
:returns: The interpretation of the time interval.
|
||||
:rtype: :class:`datetime.timedelta`"""
|
||||
:returns: The base_time plus the interpretation of the time interval.
|
||||
:rtype: :class:`datetime.datetime`"""
|
||||
|
||||
if interval.strip().isdigit():
|
||||
interval += " days"
|
||||
return datetime.timedelta(0, time.mktime(textparser.parse(
|
||||
interval, time.localtime(0))[0]))
|
||||
|
||||
# try to use the same timezone, but fallback to UTC
|
||||
tzinfo = base_time.tzinfo or pytz.UTC
|
||||
|
||||
return textparser.parseDT(interval, base_time, tzinfo=tzinfo)[0]
|
||||
|
||||
|
||||
class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
@@ -465,11 +468,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
if self.has_pending_deployment():
|
||||
interval = self.configuration.get("deploy_before_expiry",
|
||||
"5 days")
|
||||
autodeploy_interval = parse_time_interval(interval)
|
||||
expiry = crypto_util.notAfter(self.current_target("cert"))
|
||||
now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
|
||||
remaining = expiry - now
|
||||
if remaining < autodeploy_interval:
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
if expiry < add_time_interval(now, interval):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -532,12 +533,10 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
# Renewals on the basis of expiry time
|
||||
interval = self.configuration.get("renew_before_expiry", "10 days")
|
||||
autorenew_interval = parse_time_interval(interval)
|
||||
expiry = crypto_util.notAfter(self.version(
|
||||
"cert", self.latest_common_version()))
|
||||
now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
|
||||
remaining = expiry - now
|
||||
if remaining < autorenew_interval:
|
||||
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
|
||||
if expiry < add_time_interval(now, interval):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from letsencrypt.tests import test_util
|
||||
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
|
||||
# Challenges
|
||||
SIMPLE_HTTP = challenges.SimpleHTTP(
|
||||
HTTP01 = challenges.HTTP01(
|
||||
token="evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA")
|
||||
DVSNI = challenges.DVSNI(
|
||||
token=jose.b64decode(b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJyPCt92wrDoA"))
|
||||
@@ -41,7 +41,7 @@ POP = challenges.ProofOfPossession(
|
||||
)
|
||||
)
|
||||
|
||||
CHALLENGES = [SIMPLE_HTTP, DVSNI, DNS, RECOVERY_CONTACT, POP]
|
||||
CHALLENGES = [HTTP01, DVSNI, DNS, RECOVERY_CONTACT, POP]
|
||||
DV_CHALLENGES = [chall for chall in CHALLENGES
|
||||
if isinstance(chall, challenges.DVChallenge)]
|
||||
CONT_CHALLENGES = [chall for chall in CHALLENGES
|
||||
@@ -80,12 +80,12 @@ def chall_to_challb(chall, status): # pylint: disable=redefined-outer-name
|
||||
|
||||
# Pending ChallengeBody objects
|
||||
DVSNI_P = chall_to_challb(DVSNI, messages.STATUS_PENDING)
|
||||
SIMPLE_HTTP_P = chall_to_challb(SIMPLE_HTTP, messages.STATUS_PENDING)
|
||||
HTTP01_P = chall_to_challb(HTTP01, messages.STATUS_PENDING)
|
||||
DNS_P = chall_to_challb(DNS, messages.STATUS_PENDING)
|
||||
RECOVERY_CONTACT_P = chall_to_challb(RECOVERY_CONTACT, messages.STATUS_PENDING)
|
||||
POP_P = chall_to_challb(POP, messages.STATUS_PENDING)
|
||||
|
||||
CHALLENGES_P = [SIMPLE_HTTP_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
|
||||
CHALLENGES_P = [HTTP01_P, DVSNI_P, DNS_P, RECOVERY_CONTACT_P, POP_P]
|
||||
DV_CHALLENGES_P = [challb for challb in CHALLENGES_P
|
||||
if isinstance(challb.chall, challenges.DVChallenge)]
|
||||
CONT_CHALLENGES_P = [
|
||||
|
||||
@@ -9,21 +9,13 @@ from acme import challenges
|
||||
from acme import client as acme_client
|
||||
from acme import messages
|
||||
|
||||
from letsencrypt import achallenges
|
||||
from letsencrypt import errors
|
||||
from letsencrypt import le_util
|
||||
|
||||
from letsencrypt.tests import acme_util
|
||||
|
||||
|
||||
TRANSLATE = {
|
||||
"dvsni": "DVSNI",
|
||||
"simpleHttp": "SimpleHTTP",
|
||||
"dns": "DNS",
|
||||
"recoveryContact": "RecoveryContact",
|
||||
"proofOfPossession": "ProofOfPossession",
|
||||
}
|
||||
|
||||
|
||||
class ChallengeFactoryTest(unittest.TestCase):
|
||||
# pylint: disable=protected-access
|
||||
|
||||
@@ -283,6 +275,22 @@ class PollChallengesTest(unittest.TestCase):
|
||||
return (new_authzr, "response")
|
||||
|
||||
|
||||
class ChallbToAchallTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.auth_handler.challb_to_achall."""
|
||||
|
||||
def _call(self, challb):
|
||||
from letsencrypt.auth_handler import challb_to_achall
|
||||
return challb_to_achall(challb, "account_key", "domain")
|
||||
|
||||
def test_it(self):
|
||||
self.assertEqual(
|
||||
self._call(acme_util.HTTP01_P),
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.HTTP01_P, account_key="account_key",
|
||||
domain="domain"),
|
||||
)
|
||||
|
||||
|
||||
class GenChallengePathTest(unittest.TestCase):
|
||||
"""Tests for letsencrypt.auth_handler.gen_challenge_path.
|
||||
|
||||
@@ -301,8 +309,8 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
return gen_challenge_path(challbs, preferences, combinations)
|
||||
|
||||
def test_common_case(self):
|
||||
"""Given DVSNI and SimpleHTTP with appropriate combos."""
|
||||
challbs = (acme_util.DVSNI_P, acme_util.SIMPLE_HTTP_P)
|
||||
"""Given DVSNI and HTTP01 with appropriate combos."""
|
||||
challbs = (acme_util.DVSNI_P, acme_util.HTTP01_P)
|
||||
prefs = [challenges.DVSNI]
|
||||
combos = ((0,), (1,))
|
||||
|
||||
@@ -317,7 +325,7 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
challbs = (acme_util.POP_P,
|
||||
acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.SIMPLE_HTTP_P)
|
||||
acme_util.HTTP01_P)
|
||||
prefs = [challenges.ProofOfPossession, challenges.DVSNI]
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
self.assertEqual(self._call(challbs, prefs, combos), (0, 2))
|
||||
@@ -329,12 +337,12 @@ class GenChallengePathTest(unittest.TestCase):
|
||||
challbs = (acme_util.RECOVERY_CONTACT_P,
|
||||
acme_util.POP_P,
|
||||
acme_util.DVSNI_P,
|
||||
acme_util.SIMPLE_HTTP_P,
|
||||
acme_util.HTTP01_P,
|
||||
acme_util.DNS_P)
|
||||
# Typical webserver client that can do everything except DNS
|
||||
# Attempted to make the order realistic
|
||||
prefs = [challenges.ProofOfPossession,
|
||||
challenges.SimpleHTTP,
|
||||
challenges.HTTP01,
|
||||
challenges.DVSNI,
|
||||
challenges.RecoveryContact]
|
||||
combos = acme_util.gen_combos(challbs)
|
||||
@@ -403,8 +411,8 @@ class IsPreferredTest(unittest.TestCase):
|
||||
def _call(cls, chall, satisfied):
|
||||
from letsencrypt.auth_handler import is_preferred
|
||||
return is_preferred(chall, satisfied, exclusive_groups=frozenset([
|
||||
frozenset([challenges.DVSNI, challenges.SimpleHTTP]),
|
||||
frozenset([challenges.DNS, challenges.SimpleHTTP]),
|
||||
frozenset([challenges.DVSNI, challenges.HTTP01]),
|
||||
frozenset([challenges.DNS, challenges.HTTP01]),
|
||||
]))
|
||||
|
||||
def test_empty_satisfied(self):
|
||||
@@ -413,7 +421,7 @@ class IsPreferredTest(unittest.TestCase):
|
||||
def test_mutually_exclusvie(self):
|
||||
self.assertFalse(
|
||||
self._call(
|
||||
acme_util.DVSNI_P, frozenset([acme_util.SIMPLE_HTTP_P])))
|
||||
acme_util.DVSNI_P, frozenset([acme_util.HTTP01_P])))
|
||||
|
||||
def test_mutually_exclusive_same_type(self):
|
||||
self.assertTrue(
|
||||
@@ -425,16 +433,14 @@ class ReportFailedChallsTest(unittest.TestCase):
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self):
|
||||
from letsencrypt import achallenges
|
||||
|
||||
kwargs = {
|
||||
"chall": acme_util.SIMPLE_HTTP,
|
||||
"chall": acme_util.HTTP01,
|
||||
"uri": "uri",
|
||||
"status": messages.STATUS_INVALID,
|
||||
"error": messages.Error(typ="tls", detail="detail"),
|
||||
}
|
||||
|
||||
self.simple_http = achallenges.SimpleHTTP(
|
||||
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
# pylint: disable=star-args
|
||||
challb=messages.ChallengeBody(**kwargs),
|
||||
domain="example.com",
|
||||
@@ -458,7 +464,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
||||
def test_same_error_and_domain(self, mock_zope):
|
||||
from letsencrypt import auth_handler
|
||||
|
||||
auth_handler._report_failed_challs([self.simple_http, self.dvsni_same])
|
||||
auth_handler._report_failed_challs([self.http01, self.dvsni_same])
|
||||
call_list = mock_zope().add_message.call_args_list
|
||||
self.assertTrue(len(call_list) == 1)
|
||||
self.assertTrue("Domains: example.com\n" in call_list[0][0][0])
|
||||
@@ -467,7 +473,7 @@ class ReportFailedChallsTest(unittest.TestCase):
|
||||
def test_different_errors_and_domains(self, mock_zope):
|
||||
from letsencrypt import auth_handler
|
||||
|
||||
auth_handler._report_failed_challs([self.simple_http, self.dvsni_diff])
|
||||
auth_handler._report_failed_challs([self.http01, self.dvsni_diff])
|
||||
self.assertTrue(mock_zope().add_message.call_count == 2)
|
||||
|
||||
|
||||
|
||||
@@ -64,35 +64,62 @@ class CLITest(unittest.TestCase):
|
||||
self._call([])
|
||||
self.assertEqual(1, mock_run.call_count)
|
||||
|
||||
def _help_output(self, args):
|
||||
"Run a help command, and return the help string for scrutiny"
|
||||
output = StringIO.StringIO()
|
||||
with mock.patch('letsencrypt.cli.sys.stdout', new=output):
|
||||
self.assertRaises(SystemExit, self._call_stdout, args)
|
||||
out = output.getvalue()
|
||||
return out
|
||||
|
||||
def test_help(self):
|
||||
self.assertRaises(SystemExit, self._call, ['--help'])
|
||||
self.assertRaises(SystemExit, self._call, ['--help', 'all'])
|
||||
output = StringIO.StringIO()
|
||||
with mock.patch('letsencrypt.cli.sys.stdout', new=output):
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['--help', 'all'])
|
||||
out = output.getvalue()
|
||||
self.assertTrue("--configurator" in out)
|
||||
self.assertTrue("how a cert is deployed" in out)
|
||||
self.assertTrue("--manual-test-mode" in out)
|
||||
output.truncate(0)
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['-h', 'nginx'])
|
||||
out = output.getvalue()
|
||||
if "nginx" in disco.PluginsRegistry.find_all():
|
||||
# may be false while building distributions without plugins
|
||||
self.assertTrue("--nginx-ctl" in out)
|
||||
self.assertTrue("--manual-test-mode" not in out)
|
||||
self.assertTrue("--checkpoints" not in out)
|
||||
output.truncate(0)
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['--help', 'plugins'])
|
||||
out = output.getvalue()
|
||||
self.assertTrue("--manual-test-mode" not in out)
|
||||
self.assertTrue("--prepare" in out)
|
||||
self.assertTrue("Plugin options" in out)
|
||||
output.truncate(0)
|
||||
self.assertRaises(SystemExit, self._call_stdout, ['-h'])
|
||||
out = output.getvalue()
|
||||
from letsencrypt import cli
|
||||
self.assertTrue(cli.USAGE in out)
|
||||
plugins = disco.PluginsRegistry.find_all()
|
||||
out = self._help_output(['--help', 'all'])
|
||||
self.assertTrue("--configurator" in out)
|
||||
self.assertTrue("how a cert is deployed" in out)
|
||||
self.assertTrue("--manual-test-mode" in out)
|
||||
|
||||
out = self._help_output(['-h', 'nginx'])
|
||||
if "nginx" in plugins:
|
||||
# may be false while building distributions without plugins
|
||||
self.assertTrue("--nginx-ctl" in out)
|
||||
self.assertTrue("--manual-test-mode" not in out)
|
||||
self.assertTrue("--checkpoints" not in out)
|
||||
|
||||
out = self._help_output(['-h'])
|
||||
if "nginx" in plugins:
|
||||
self.assertTrue("Use the Nginx plugin" in out)
|
||||
else:
|
||||
self.assertTrue("(nginx support is experimental" in out)
|
||||
|
||||
out = self._help_output(['--help', 'plugins'])
|
||||
self.assertTrue("--manual-test-mode" not in out)
|
||||
self.assertTrue("--prepare" in out)
|
||||
self.assertTrue("Plugin options" in out)
|
||||
|
||||
out = self._help_output(['--help', 'install'])
|
||||
self.assertTrue("--cert-path" in out)
|
||||
self.assertTrue("--key-path" in out)
|
||||
|
||||
out = self._help_output(['--help', 'revoke'])
|
||||
self.assertTrue("--cert-path" in out)
|
||||
self.assertTrue("--key-path" in out)
|
||||
|
||||
out = self._help_output(['-h', 'config_changes'])
|
||||
self.assertTrue("--cert-path" not in out)
|
||||
self.assertTrue("--key-path" not in out)
|
||||
|
||||
out = self._help_output(['-h'])
|
||||
from letsencrypt import cli
|
||||
self.assertTrue(cli.usage_strings(plugins)[0] in out)
|
||||
|
||||
@mock.patch('letsencrypt.cli.display_ops')
|
||||
def test_installer_selection(self, mock_display_ops):
|
||||
self._call(['install', '--domain', 'foo.bar', '--cert-path', 'cert',
|
||||
'--key-path', 'key', '--chain-path', 'chain'])
|
||||
self.assertEqual(mock_display_ops.pick_installer.call_count, 1)
|
||||
|
||||
def test_configurator_selection(self):
|
||||
real_plugins = disco.PluginsRegistry.find_all()
|
||||
@@ -115,6 +142,12 @@ class CLITest(unittest.TestCase):
|
||||
# (we can only do that if letsencrypt-nginx is actually present)
|
||||
ret, _, _, _ = self._call(args)
|
||||
self.assertTrue("The nginx plugin is not working" in ret)
|
||||
self.assertTrue("Could not find configuration root" in ret)
|
||||
self.assertTrue("NoInstallationError" in ret)
|
||||
|
||||
with MockedVerb("certonly") as mock_certonly:
|
||||
self._call(["auth", "--standalone"])
|
||||
self.assertEqual(1, mock_certonly.call_count)
|
||||
|
||||
def test_rollback(self):
|
||||
_, _, _, client = self._call(['rollback'])
|
||||
@@ -135,16 +168,16 @@ class CLITest(unittest.TestCase):
|
||||
for r in xrange(len(flags)))):
|
||||
self._call(['plugins'] + list(args))
|
||||
|
||||
def test_auth_bad_args(self):
|
||||
ret, _, _, _ = self._call(['-d', 'foo.bar', 'auth', '--csr', CSR])
|
||||
def test_certonly_bad_args(self):
|
||||
ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
|
||||
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
|
||||
|
||||
ret, _, _, _ = self._call(['-a', 'bad_auth', 'auth'])
|
||||
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
|
||||
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
|
||||
|
||||
@mock.patch('letsencrypt.crypto_util.notAfter')
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
def test_auth_new_request_success(self, mock_get_utility, mock_notAfter):
|
||||
def test_certonly_new_request_success(self, mock_get_utility, mock_notAfter):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar'
|
||||
date = '1970-01-01'
|
||||
mock_notAfter().date.return_value = date
|
||||
@@ -152,7 +185,7 @@ class CLITest(unittest.TestCase):
|
||||
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=cert_path)
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_and_enroll_certificate.return_value = mock_lineage
|
||||
self._auth_new_request_common(mock_client)
|
||||
self._certonly_new_request_common(mock_client)
|
||||
self.assertEqual(
|
||||
mock_client.obtain_and_enroll_certificate.call_count, 1)
|
||||
self.assertTrue(
|
||||
@@ -160,23 +193,23 @@ class CLITest(unittest.TestCase):
|
||||
self.assertTrue(
|
||||
date in mock_get_utility().add_message.call_args[0][0])
|
||||
|
||||
def test_auth_new_request_failure(self):
|
||||
def test_certonly_new_request_failure(self):
|
||||
mock_client = mock.MagicMock()
|
||||
mock_client.obtain_and_enroll_certificate.return_value = False
|
||||
self.assertRaises(errors.Error,
|
||||
self._auth_new_request_common, mock_client)
|
||||
self._certonly_new_request_common, mock_client)
|
||||
|
||||
def _auth_new_request_common(self, mock_client):
|
||||
def _certonly_new_request_common(self, mock_client):
|
||||
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
|
||||
mock_renewal.return_value = None
|
||||
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
|
||||
mock_init.return_value = mock_client
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth'])
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
|
||||
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
@mock.patch('letsencrypt.cli._treat_as_renewal')
|
||||
@mock.patch('letsencrypt.cli._init_le_client')
|
||||
def test_auth_renewal(self, mock_init, mock_renewal, mock_get_utility):
|
||||
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility):
|
||||
cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
|
||||
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
|
||||
|
||||
@@ -190,7 +223,7 @@ class CLITest(unittest.TestCase):
|
||||
mock_init.return_value = mock_client
|
||||
with mock.patch('letsencrypt.cli.OpenSSL'):
|
||||
with mock.patch('letsencrypt.cli.crypto_util'):
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'auth'])
|
||||
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
|
||||
mock_client.obtain_certificate.assert_called_once_with(['foo.bar'])
|
||||
self.assertEqual(mock_lineage.save_successor.call_count, 1)
|
||||
mock_lineage.update_all_links_to.assert_called_once_with(
|
||||
@@ -202,8 +235,8 @@ class CLITest(unittest.TestCase):
|
||||
@mock.patch('letsencrypt.cli.display_ops.pick_installer')
|
||||
@mock.patch('letsencrypt.cli.zope.component.getUtility')
|
||||
@mock.patch('letsencrypt.cli._init_le_client')
|
||||
def test_auth_csr(self, mock_init, mock_get_utility,
|
||||
mock_pick_installer, mock_notAfter):
|
||||
def test_certonly_csr(self, mock_init, mock_get_utility,
|
||||
mock_pick_installer, mock_notAfter):
|
||||
cert_path = '/etc/letsencrypt/live/blahcert.pem'
|
||||
date = '1970-01-01'
|
||||
mock_notAfter().date.return_value = date
|
||||
@@ -216,7 +249,7 @@ class CLITest(unittest.TestCase):
|
||||
|
||||
installer = 'installer'
|
||||
self._call(
|
||||
['-a', 'standalone', '-i', installer, 'auth', '--csr', CSR,
|
||||
['-a', 'standalone', '-i', installer, 'certonly', '--csr', CSR,
|
||||
'--cert-path', cert_path, '--fullchain-path', '/',
|
||||
'--chain-path', '/'])
|
||||
self.assertEqual(mock_pick_installer.call_args[0][1], installer)
|
||||
|
||||
@@ -163,7 +163,7 @@ class ClientTest(unittest.TestCase):
|
||||
domain='foo.bar',
|
||||
fullchain_path='fullchain',
|
||||
key_path=os.path.abspath("key"))
|
||||
self.assertEqual(installer.save.call_count, 1)
|
||||
self.assertEqual(installer.save.call_count, 2)
|
||||
installer.restart.assert_called_once_with()
|
||||
|
||||
@mock.patch("letsencrypt.client.enhancements")
|
||||
|
||||
@@ -14,7 +14,7 @@ class NamespaceConfigTest(unittest.TestCase):
|
||||
self.namespace = mock.MagicMock(
|
||||
config_dir='/tmp/config', work_dir='/tmp/foo', foo='bar',
|
||||
server='https://acme-server.org:443/new',
|
||||
dvsni_port=1234, simple_http_port=4321)
|
||||
dvsni_port=1234, http01_port=4321)
|
||||
from letsencrypt.configuration import NamespaceConfig
|
||||
self.config = NamespaceConfig(self.namespace)
|
||||
|
||||
@@ -54,10 +54,10 @@ class NamespaceConfigTest(unittest.TestCase):
|
||||
self.assertEqual(self.config.key_dir, '/tmp/config/keys')
|
||||
self.assertEqual(self.config.temp_checkpoint_dir, '/tmp/foo/t')
|
||||
|
||||
def test_simple_http_port(self):
|
||||
self.assertEqual(4321, self.config.simple_http_port)
|
||||
self.namespace.simple_http_port = None
|
||||
self.assertEqual(80, self.config.simple_http_port)
|
||||
def test_http01_port(self):
|
||||
self.assertEqual(4321, self.config.http01_port)
|
||||
self.namespace.http01_port = None
|
||||
self.assertEqual(80, self.config.http01_port)
|
||||
|
||||
|
||||
class RenewerConfigurationTest(unittest.TestCase):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Tests for letsencrypt.renewer."""
|
||||
import datetime
|
||||
import pytz
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
@@ -622,18 +623,47 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
# OCSP server to test against.
|
||||
self.assertFalse(self.test_rc.ocsp_revoked())
|
||||
|
||||
def test_parse_time_interval(self):
|
||||
def test_add_time_interval(self):
|
||||
from letsencrypt import storage
|
||||
# XXX: I'm not sure if intervals related to years and months
|
||||
# take account of the current date (if so, some of these
|
||||
# may fail in the future, like in leap years or even in
|
||||
# months of different lengths!)
|
||||
intended = {"": 0, "17 days": 17, "23": 23, "1 month": 31,
|
||||
"7 weeks": 49, "1 year 1 day": 366, "1 year-1 day": 364,
|
||||
"4 years": 1461}
|
||||
for time in intended:
|
||||
self.assertEqual(storage.parse_time_interval(time),
|
||||
datetime.timedelta(intended[time]))
|
||||
|
||||
# this month has 30 days, and the next year is a leap year
|
||||
time_1 = pytz.UTC.fromutc(datetime.datetime(2003, 11, 20, 11, 59, 21))
|
||||
|
||||
# this month has 31 days, and the next year is not a leap year
|
||||
time_2 = pytz.UTC.fromutc(datetime.datetime(2012, 10, 18, 21, 31, 16))
|
||||
|
||||
# in different time zone (GMT+8)
|
||||
time_3 = pytz.timezone('Asia/Shanghai').fromutc(
|
||||
datetime.datetime(2015, 10, 26, 22, 25, 41))
|
||||
|
||||
intended = {
|
||||
(time_1, ""): time_1,
|
||||
(time_2, ""): time_2,
|
||||
(time_3, ""): time_3,
|
||||
(time_1, "17 days"): time_1 + datetime.timedelta(17),
|
||||
(time_2, "17 days"): time_2 + datetime.timedelta(17),
|
||||
(time_1, "30"): time_1 + datetime.timedelta(30),
|
||||
(time_2, "30"): time_2 + datetime.timedelta(30),
|
||||
(time_1, "7 weeks"): time_1 + datetime.timedelta(49),
|
||||
(time_2, "7 weeks"): time_2 + datetime.timedelta(49),
|
||||
# 1 month is always 30 days, no matter which month it is
|
||||
(time_1, "1 month"): time_1 + datetime.timedelta(30),
|
||||
(time_2, "1 month"): time_2 + datetime.timedelta(31),
|
||||
# 1 year could be 365 or 366 days, depends on the year
|
||||
(time_1, "1 year"): time_1 + datetime.timedelta(366),
|
||||
(time_2, "1 year"): time_2 + datetime.timedelta(365),
|
||||
(time_1, "1 year 1 day"): time_1 + datetime.timedelta(367),
|
||||
(time_2, "1 year 1 day"): time_2 + datetime.timedelta(366),
|
||||
(time_1, "1 year-1 day"): time_1 + datetime.timedelta(365),
|
||||
(time_2, "1 year-1 day"): time_2 + datetime.timedelta(364),
|
||||
(time_1, "4 years"): time_1 + datetime.timedelta(1461),
|
||||
(time_2, "4 years"): time_2 + datetime.timedelta(1461),
|
||||
}
|
||||
|
||||
for parameters, excepted in intended.items():
|
||||
base_time, interval = parameters
|
||||
self.assertEqual(storage.add_time_interval(base_time, interval),
|
||||
excepted)
|
||||
|
||||
@mock.patch("letsencrypt.renewer.plugins_disco")
|
||||
@mock.patch("letsencrypt.account.AccountFileStorage")
|
||||
@@ -659,7 +689,7 @@ class RenewableCertTests(BaseRenewableCertTest):
|
||||
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"]["simple_http_port"] = "1234"
|
||||
self.test_rc.configfile["renewalparams"]["http01_port"] = "1234"
|
||||
self.test_rc.configfile["renewalparams"]["account"] = "abcde"
|
||||
mock_auth = mock.MagicMock()
|
||||
mock_pd.PluginsRegistry.find_all.return_value = {"apache": mock_auth}
|
||||
|
||||
@@ -306,6 +306,6 @@ texinfo_documents = [
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'acme': ('https://acme-python.readthedocs.org', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org', None),
|
||||
'acme': ('https://acme-python.readthedocs.org/en/latest/', None),
|
||||
'letsencrypt': ('https://letsencrypt.readthedocs.org/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ setup(
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet :: WWW/HTTP',
|
||||
'Topic :: Security',
|
||||
|
||||
@@ -28,7 +28,7 @@ common() {
|
||||
}
|
||||
|
||||
common --domains le1.wtf --standalone-supported-challenges dvsni auth
|
||||
common --domains le2.wtf --standalone-supported-challenges simpleHttp run
|
||||
common --domains le2.wtf --standalone-supported-challenges http-01 run
|
||||
common -a manual -d le.wtf auth
|
||||
|
||||
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
|
||||
|
||||
@@ -16,7 +16,7 @@ letsencrypt_test () {
|
||||
--server "${SERVER:-http://localhost:4000/directory}" \
|
||||
--no-verify-ssl \
|
||||
--dvsni-port 5001 \
|
||||
--simple-http-port 5002 \
|
||||
--http-01-port 5002 \
|
||||
--manual-test-mode \
|
||||
$store_flags \
|
||||
--text \
|
||||
|
||||
+3
-3
@@ -2,9 +2,9 @@
|
||||
#
|
||||
# Find all Python imports.
|
||||
#
|
||||
# ./deps.sh letsencrypt
|
||||
# ./deps.sh acme
|
||||
# ./deps.sh letsencrypt-apache
|
||||
# ./tools/deps.sh letsencrypt
|
||||
# ./tools/deps.sh acme
|
||||
# ./tools/deps.sh letsencrypt-apache
|
||||
# ...
|
||||
#
|
||||
# Manually compare the output with deps in setup.py.
|
||||
|
||||
Reference in New Issue
Block a user