mirror of
https://github.com/certbot/certbot.git
synced 2026-06-08 00:02:14 -04:00
commit
15fa5af934
31 changed files with 478 additions and 507 deletions
|
|
@ -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
|
||||
# abstract-class-not-used cannot be disabled locally (at least in
|
||||
# pylint 1.4.1), same for abstract-class-little-used
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
"""Content-Type header that must be used for provisioned resource."""
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,22 +315,22 @@ 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)
|
||||
|
||||
|
|
@ -363,7 +338,7 @@ class DVSNIResponseTest(unittest.TestCase):
|
|||
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')),
|
||||
|
|
|
|||
|
|
@ -58,26 +58,26 @@ class DVSNIServer(TLSServer, ACMEServerMixin):
|
|||
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):
|
||||
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())
|
||||
|
|
@ -86,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()
|
||||
|
|
@ -106,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")
|
||||
|
|
|
|||
|
|
@ -54,16 +54,16 @@ class DVSNIServerTest(unittest.TestCase):
|
|||
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]
|
||||
|
|
@ -86,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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -127,7 +127,7 @@ 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
|
||||
``--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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -854,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")
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ 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 privileged process. Alternatively shows instructions on
|
||||
how to use Python's built-in HTTP server.
|
||||
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`.
|
||||
|
||||
|
|
@ -69,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
|
||||
printf "%s" {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; \\
|
||||
|
|
@ -96,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 an HTTP server for solving SimpleHTTP challenges and thus "
|
||||
"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 = []
|
||||
|
|
@ -131,16 +131,16 @@ 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, but supports printf
|
||||
|
|
@ -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,8 +65,8 @@ 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)
|
||||
|
|
@ -110,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):
|
||||
|
|
@ -139,7 +138,7 @@ 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.
|
||||
"""
|
||||
|
|
@ -151,10 +150,10 @@ class Authenticator(common.Plugin):
|
|||
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"])
|
||||
|
||||
|
|
@ -185,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
|
||||
|
|
@ -237,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()
|
||||
|
|
|
|||
|
|
@ -62,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})."""
|
||||
|
|
@ -76,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)
|
||||
|
|
@ -90,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)
|
||||
|
|
@ -100,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
|
||||
|
|
@ -111,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"]]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -689,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}
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Reference in a new issue