mirror of
https://github.com/certbot/certbot.git
synced 2026-01-21 19:01:07 +03:00
works with boulder
This commit is contained in:
@@ -5,24 +5,20 @@ import hashlib
|
||||
|
||||
import Crypto.Random
|
||||
|
||||
from letsencrypt.acme import fields
|
||||
from letsencrypt.acme import jose
|
||||
from letsencrypt.acme import messages2
|
||||
from letsencrypt.acme import other
|
||||
|
||||
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
class Challenge(jose.TypedJSONObjectWithFields):
|
||||
# _fields_to_json | pylint: disable=abstract-method
|
||||
"""ACME challenge."""
|
||||
TYPES = {}
|
||||
|
||||
|
||||
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
|
||||
class ContinuityChallenge(messages2.Challenge): # pylint: disable=abstract-method
|
||||
"""Client validation challenges."""
|
||||
|
||||
|
||||
class DVChallenge(Challenge): # pylint: disable=abstract-method
|
||||
class DVChallenge(messages2.Challenge): # pylint: disable=abstract-method
|
||||
"""Domain validation challenges."""
|
||||
|
||||
|
||||
@@ -41,7 +37,7 @@ class ChallengeResponse(jose.TypedJSONObjectWithFields):
|
||||
return super(ChallengeResponse, cls).from_json(jobj)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
@messages2.Challenge.register
|
||||
class SimpleHTTPS(DVChallenge):
|
||||
"""ACME "simpleHttps" challenge."""
|
||||
typ = "simpleHttps"
|
||||
@@ -69,7 +65,7 @@ class SimpleHTTPSResponse(ChallengeResponse):
|
||||
return self.URI_TEMPLATE.format(domain=domain, path=self.path)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
@messages2.Challenge.register
|
||||
class DVSNI(DVChallenge):
|
||||
"""ACME "dvsni" challenge.
|
||||
|
||||
@@ -93,6 +89,9 @@ class DVSNI(DVChallenge):
|
||||
nonce = jose.Field("nonce", encoder=binascii.hexlify,
|
||||
decoder=functools.partial(functools.partial(
|
||||
jose.decode_hex16, size=NONCE_SIZE)))
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=messages2.Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
@property
|
||||
def nonce_domain(self):
|
||||
@@ -138,7 +137,7 @@ class DVSNIResponse(ChallengeResponse):
|
||||
"""Domain name for certificate subjectAltName."""
|
||||
return self.z(chall) + self.DOMAIN_SUFFIX
|
||||
|
||||
@Challenge.register
|
||||
@messages2.Challenge.register
|
||||
class RecoveryContact(ContinuityChallenge):
|
||||
"""ACME "recoveryContact" challenge."""
|
||||
typ = "recoveryContact"
|
||||
@@ -147,6 +146,10 @@ class RecoveryContact(ContinuityChallenge):
|
||||
success_url = jose.Field("successURL", omitempty=True)
|
||||
contact = jose.Field("contact", omitempty=True)
|
||||
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=messages2.Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
class RecoveryContactResponse(ChallengeResponse):
|
||||
@@ -155,11 +158,15 @@ class RecoveryContactResponse(ChallengeResponse):
|
||||
token = jose.Field("token", omitempty=True)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
@messages2.Challenge.register
|
||||
class RecoveryToken(ContinuityChallenge):
|
||||
"""ACME "recoveryToken" challenge."""
|
||||
typ = "recoveryToken"
|
||||
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=messages2.Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
|
||||
@ChallengeResponse.register
|
||||
class RecoveryTokenResponse(ChallengeResponse):
|
||||
@@ -168,7 +175,7 @@ class RecoveryTokenResponse(ChallengeResponse):
|
||||
token = jose.Field("token", omitempty=True)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
@messages2.Challenge.register
|
||||
class ProofOfPossession(ContinuityChallenge):
|
||||
"""ACME "proofOfPossession" challenge.
|
||||
|
||||
@@ -180,6 +187,10 @@ class ProofOfPossession(ContinuityChallenge):
|
||||
|
||||
NONCE_SIZE = 16
|
||||
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=messages2.Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
class Hints(jose.JSONObjectWithFields):
|
||||
"""Hints for "proofOfPossession" challenge.
|
||||
|
||||
@@ -236,12 +247,15 @@ class ProofOfPossessionResponse(ChallengeResponse):
|
||||
return self.signature.verify(self.nonce)
|
||||
|
||||
|
||||
@Challenge.register
|
||||
@messages2.Challenge.register
|
||||
class DNS(DVChallenge):
|
||||
"""ACME "dns" challenge."""
|
||||
typ = "dns"
|
||||
token = jose.Field("token")
|
||||
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=messages2.Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
@ChallengeResponse.register
|
||||
class DNSResponse(ChallengeResponse):
|
||||
|
||||
@@ -216,7 +216,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
||||
value = getattr(self, slot)
|
||||
|
||||
if field.omit(value):
|
||||
logging.debug('Ommiting empty field "%s" (%s)', slot, value)
|
||||
logging.debug('Omitting empty field "%s" (%s)', slot, value)
|
||||
else:
|
||||
try:
|
||||
jobj[field.json_name] = field.encode(value)
|
||||
@@ -246,6 +246,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
||||
"""Deserialize fields from JSON."""
|
||||
cls._check_required(jobj)
|
||||
fields = {}
|
||||
|
||||
for slot, field in cls._fields.iteritems():
|
||||
if field.json_name not in jobj and field.omitempty:
|
||||
fields[slot] = field.default
|
||||
@@ -372,17 +373,15 @@ class TypedJSONObjectWithFields(JSONObjectWithFields):
|
||||
raise errors.DeserializationError("missing type field")
|
||||
|
||||
try:
|
||||
type_cls = cls.TYPES[typ]
|
||||
return cls.TYPES[typ]
|
||||
except KeyError:
|
||||
raise errors.UnrecognizedTypeError(typ, jobj)
|
||||
|
||||
return type_cls
|
||||
|
||||
def to_json(self):
|
||||
"""Get JSON serializable object.
|
||||
|
||||
:returns: Serializable JSON object representing ACME typed object.
|
||||
:meth:`validate` will almost certianly not work, due to reasons
|
||||
:meth:`validate` will almost certainly not work, due to reasons
|
||||
explained in :class:`letsencrypt.acme.interfaces.IJSONSerializable`.
|
||||
:rtype: dict
|
||||
|
||||
|
||||
@@ -115,6 +115,14 @@ class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body."""
|
||||
|
||||
|
||||
class TypedResourceBody(jose.TypedJSONObjectWithFields):
|
||||
"""ACME Resource Body with type."""
|
||||
|
||||
|
||||
class ResourceBody(jose.JSONObjectWithFields):
|
||||
"""ACME Resource Body"""
|
||||
|
||||
|
||||
class RegistrationResource(Resource):
|
||||
"""Registration Resource.
|
||||
|
||||
@@ -130,7 +138,7 @@ class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
|
||||
:ivar tuple contact:
|
||||
:ivar tuple contact: Contact information following ACME spec
|
||||
|
||||
"""
|
||||
|
||||
@@ -158,41 +166,23 @@ class ChallengeResource(Resource, jose.JSONObjectWithFields):
|
||||
return self.body.uri
|
||||
|
||||
|
||||
class ChallengeBody(ResourceBody):
|
||||
class Challenge(TypedResourceBody):
|
||||
"""Challenge Resource Body.
|
||||
|
||||
.. todo::
|
||||
Confusingly, this has a similar name to `.challenges.Challenge`,
|
||||
as well as `.achallenges.AnnotatedChallenge` or
|
||||
`.achallenges.Indexed`... Once `messages2` and `network2` is
|
||||
integrated with the rest of the client, this class functionality
|
||||
will be merged with `.challenges.Challenge`. Meanwhile,
|
||||
separation allows the ``master`` to be still interoperable with
|
||||
Node.js server (protocol v00). For the time being use names such
|
||||
as ``challb`` to distinguish instances of this class from
|
||||
``achall`` or ``ichall``.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Status status:
|
||||
:ivar datetime.datetime validated:
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ('chall',)
|
||||
TYPES = {}
|
||||
# __slots__ = ('chall',)
|
||||
uri = jose.Field('uri')
|
||||
status = jose.Field('status', decoder=Status.from_json)
|
||||
validated = fields.RFC3339Field('validated', omitempty=True)
|
||||
|
||||
def to_json(self):
|
||||
jobj = super(ChallengeBody, self).to_json()
|
||||
jobj.update(self.chall.to_json())
|
||||
jobj = super(Challenge, self).to_json()
|
||||
return jobj
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj)
|
||||
jobj_fields['chall'] = challenges.Challenge.from_json(jobj)
|
||||
return jobj_fields
|
||||
|
||||
|
||||
class AuthorizationResource(Resource):
|
||||
"""Authorization Resource.
|
||||
@@ -208,7 +198,7 @@ class Authorization(ResourceBody):
|
||||
"""Authorization Resource Body.
|
||||
|
||||
:ivar letsencrypt.acme.messages2.Identifier identifier:
|
||||
:ivar list challenges: `list` of `Challenge`
|
||||
:ivar list challenges: `list` of `ChallengeBody`
|
||||
:ivar tuple combinations: Challenge combinations (`tuple` of `tuple`
|
||||
of `int`, as opposed to `list` of `list` from the spec).
|
||||
:ivar letsencrypt.acme.jose.jwk.JWK key: Public key.
|
||||
@@ -235,7 +225,7 @@ class Authorization(ResourceBody):
|
||||
|
||||
@challenges.decoder
|
||||
def challenges(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return tuple(ChallengeBody.from_json(chall) for chall in value)
|
||||
return tuple(challenges.Challenge.from_json(chall) for chall in value)
|
||||
|
||||
@property
|
||||
def resolved_combinations(self):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Client annotated ACME challenges.
|
||||
|
||||
Please use names such as ``achall`` and ``ichall`` (respectively ``achalls``
|
||||
and ``ichalls`` for collections) to distiguish from variables "of type"
|
||||
Please use names such as ``achall`` to distiguish from variables "of type"
|
||||
:class:`letsencrypt.acme.challenges.Challenge` (denoted by ``chall``)::
|
||||
|
||||
from letsencrypt.acme import challenges
|
||||
@@ -9,11 +8,10 @@ and ``ichalls`` for collections) to distiguish from variables "of type"
|
||||
|
||||
chall = challenges.DNS(token='foo')
|
||||
achall = achallenges.DNS(chall=chall, domain='example.com')
|
||||
ichall = achallenges.Indexed(achall=achall, index=0)
|
||||
|
||||
Note, that all annotated challenges act as a proxy objects::
|
||||
|
||||
ichall.token == achall.token == chall.token
|
||||
achall.token == chall.token
|
||||
|
||||
"""
|
||||
from letsencrypt.acme import challenges
|
||||
@@ -86,17 +84,3 @@ class ProofOfPossession(AnnotatedChallenge):
|
||||
"""Client annotated "proofOfPossession" ACME challenge."""
|
||||
__slots__ = ('chall', 'domain')
|
||||
acme_type = challenges.ProofOfPossession
|
||||
|
||||
|
||||
class Indexed(jose_util.ImmutableMap):
|
||||
"""Indexed and annotated ACME challenge.
|
||||
|
||||
Wraps around :class:`AnnotatedChallenge` and annotates with an
|
||||
``index`` in order to maintain the proper position of the response
|
||||
within a larger challenge list.
|
||||
|
||||
"""
|
||||
__slots__ = ('achall', 'index')
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.achall, name)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import itertools
|
||||
import logging
|
||||
import sys
|
||||
import time
|
||||
|
||||
import Crypto.PublicKey.RSA
|
||||
|
||||
@@ -51,7 +52,7 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
self.dv_c = []
|
||||
self.cont_c = []
|
||||
|
||||
def get_authorizations(self, domains):
|
||||
def get_authorizations(self, domains, new_authz_uri):
|
||||
"""Retrieve all authorizations for challenges.
|
||||
|
||||
:param set domains: Domains for authorization
|
||||
@@ -64,6 +65,9 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
authorizations
|
||||
|
||||
"""
|
||||
for domain in domains:
|
||||
self.authzr[domain] = self.network.request_domain_challenges(
|
||||
domain, new_authz_uri)
|
||||
self._choose_challenges(domains)
|
||||
cont_resp, dv_resp = self._get_responses()
|
||||
logging.info("Ready for verification...")
|
||||
@@ -71,13 +75,15 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
# Send all Responses
|
||||
self._respond(cont_resp, dv_resp)
|
||||
|
||||
return self._verify_auths()
|
||||
|
||||
def _choose_challenges(self, domains):
|
||||
logging.info("Performing the following challenges:")
|
||||
for dom in domains:
|
||||
path = gen_challenge_path(
|
||||
self.authzr[dom].challenges,
|
||||
self.authzr[dom].body.challenges,
|
||||
self._get_chall_pref(dom),
|
||||
self.authzr[dom].combinations)
|
||||
self.authzr[dom].body.combinations)
|
||||
|
||||
dom_dv_c, dom_cont_c = self._challenge_factory(
|
||||
dom, path)
|
||||
@@ -106,51 +112,65 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
return cont_resp, dv_resp
|
||||
|
||||
def acme_authorization(self, domain):
|
||||
"""Handle ACME "authorization" phase.
|
||||
def _verify_auths(self):
|
||||
time.sleep(6)
|
||||
for domain in self.authzr:
|
||||
self.authzr[domain], resp = self.network.poll(self.authzr[domain])
|
||||
if self.authzr[domain].body.status == messages2.STATUS_INVALID:
|
||||
raise errors.AuthHandlerError(
|
||||
"Unable to retrieve authorization for %s" % domain)
|
||||
|
||||
:param str domain: domain that is requesting authorization
|
||||
|
||||
:returns: ACME "authorization" message.
|
||||
:rtype: :class:`letsencrypt.acme.messages.Authorization`
|
||||
|
||||
"""
|
||||
try:
|
||||
auth = self.network.send_and_receive_expected(
|
||||
messages.AuthorizationRequest.create(
|
||||
session_id=self.msgs[domain].session_id,
|
||||
nonce=self.msgs[domain].nonce,
|
||||
responses=self.responses[domain],
|
||||
name=domain,
|
||||
key=jose.HashableRSAKey(Crypto.PublicKey.RSA.importKey(
|
||||
self.authkey[domain].pem))),
|
||||
messages.Authorization)
|
||||
logging.info("Received Authorization for %s", domain)
|
||||
return auth
|
||||
except errors.LetsEncryptClientError as err:
|
||||
logging.fatal(str(err))
|
||||
logging.fatal(
|
||||
"Failed Authorization procedure - cleaning up challenges")
|
||||
sys.exit(1)
|
||||
finally:
|
||||
self._cleanup_challenges(domain)
|
||||
self._cleanup_challenges()
|
||||
return [self.authzr[domain] for domain in self.authzr]
|
||||
|
||||
def _respond(self, cont_resp, dv_resp):
|
||||
"""Send/Recieve confirmation of all challenges.
|
||||
"""Send/Receive confirmation of all challenges.
|
||||
|
||||
.. note:: This method also cleans up the auth_handler state.
|
||||
|
||||
"""
|
||||
to_check = self._send_responses(self.dv_c, dv_resp)
|
||||
to_check.update(self._send_responses(self.cont_c, cont_resp))
|
||||
chall_update = dict()
|
||||
self._send_responses(self.dv_c, dv_resp, chall_update)
|
||||
self._send_responses(self.cont_c, cont_resp, chall_update)
|
||||
|
||||
def _send_responses(self, achalls, resps):
|
||||
# self._poll_challenges(chall_update)
|
||||
|
||||
def _send_responses(self, achalls, resps, chall_update):
|
||||
"""Send responses and make sure errors are handled."""
|
||||
to_check = dict()
|
||||
for achall, resp in itertools.izip(achalls, resps):
|
||||
if resp:
|
||||
to_check[achall.domain] = self.network.answer_challenge(
|
||||
achall.chall, resp)
|
||||
challr = self.network.answer_challenge(achall.chall, resp)
|
||||
chall_update[achall.domain] = chall_update.get(
|
||||
achall.domain, []).append(challr)
|
||||
|
||||
# def _poll_challenges(self, chall_update):
|
||||
# to_check = chall_update.keys()
|
||||
# completed = []
|
||||
# while to_check:
|
||||
#
|
||||
# def _handle_to_check(self):
|
||||
# for domain in to_check:
|
||||
# self.authzr[domain] = self.network.poll(self.authzr[domain])
|
||||
# if self.authzr[domain].status == messages2.STATUS_VALID:
|
||||
# completed.append(domain)
|
||||
# if self.authzr[domain].status == messages2.STATUS_INVALID:
|
||||
# logging.error("Failed authorization for %s", domain)
|
||||
# raise errors.AuthHandlerError(
|
||||
# "Failed Authorization for %s" % domain)
|
||||
# for challr in chall_update[domain]:
|
||||
# status = self._get_status_of_chall(self.authzr[domain], challr)
|
||||
# if status == messages2.STATUS_VALID:
|
||||
# chall_update[domain].remove(challr)
|
||||
# elif status == messages2.STATUS_INVALID:
|
||||
# raise errors.AuthHandlerError(
|
||||
# "Failed %s challenge for domain %s" % (
|
||||
# challr.body.chall.typ, domain))
|
||||
#
|
||||
# def _get_status_of_chall(self, authzr, challr):
|
||||
# for challb in authzr.challenges:
|
||||
# # TODO: Use better identifiers... instead of type
|
||||
# if isinstance(challb.chall, challr.body.chall):
|
||||
# return challb.status
|
||||
|
||||
def _get_chall_pref(self, domain):
|
||||
"""Return list of challenge preferences.
|
||||
@@ -192,16 +212,16 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
cont_chall = []
|
||||
|
||||
for index in path:
|
||||
chall = self.authzr[domain].challenges[index]
|
||||
chall = self.authzr[domain].body.challenges[index]
|
||||
|
||||
if isinstance(chall, challenges.DVSNI):
|
||||
logging.info(" DVSNI challenge for %s.", domain)
|
||||
achall = achallenges.DVSNI(
|
||||
chall=chall, domain=domain, key=self.authkey[domain])
|
||||
chall=chall, domain=domain, key=self.authkey)
|
||||
elif isinstance(chall, challenges.SimpleHTTPS):
|
||||
logging.info(" SimpleHTTPS challenge for %s.", domain)
|
||||
achall = achallenges.SimpleHTTPS(
|
||||
chall=chall, domain=domain, key=self.authkey[domain])
|
||||
chall=chall, domain=domain, key=self.authkey)
|
||||
elif isinstance(chall, challenges.DNS):
|
||||
logging.info(" DNS challenge for %s.", domain)
|
||||
achall = achallenges.DNS(chall=chall, domain=domain)
|
||||
@@ -211,7 +231,8 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
achall = achallenges.RecoveryToken(chall=chall, domain=domain)
|
||||
elif isinstance(chall, challenges.RecoveryContact):
|
||||
logging.info(" Recovery Contact Challenge for %s.", domain)
|
||||
achall = achallenges.RecoveryContact(chall=chall, domain=domain)
|
||||
achall = achallenges.RecoveryContact(
|
||||
chall=chall, domain=domain)
|
||||
elif isinstance(chall, challenges.ProofOfPossession):
|
||||
logging.info(" Proof-of-Possession Challenge for %s", domain)
|
||||
achall = achallenges.ProofOfPossession(
|
||||
@@ -219,14 +240,13 @@ class AuthHandler(object): # pylint: disable=too-many-instance-attributes
|
||||
|
||||
else:
|
||||
raise errors.LetsEncryptClientError(
|
||||
"Received unsupported challenge of type: %s", chall.typ)
|
||||
|
||||
ichall = achallenges.Indexed(achall=achall, index=index)
|
||||
"Received unsupported challenge of type: %s",
|
||||
chall.typ)
|
||||
|
||||
if isinstance(chall, challenges.ContinuityChallenge):
|
||||
cont_chall.append(ichall)
|
||||
cont_chall.append(achall)
|
||||
elif isinstance(chall, challenges.DVChallenge):
|
||||
dv_chall.append(ichall)
|
||||
dv_chall.append(achall)
|
||||
|
||||
return dv_chall, cont_chall
|
||||
|
||||
|
||||
@@ -65,8 +65,9 @@ class Client(object):
|
||||
|
||||
# TODO: Allow for other alg types besides RS256
|
||||
self.network = network2.Network(
|
||||
config.server+"/acme/new-registration",
|
||||
"https://%s/acme/new-reg" % config.server,
|
||||
jwk.JWKRSA.load(authkey.pem))
|
||||
|
||||
self.config = config
|
||||
|
||||
if dv_auth is not None:
|
||||
@@ -88,9 +89,18 @@ class Client(object):
|
||||
"mailto:" + email if email is not None else None,
|
||||
"tel:" + phone if phone is not None else None
|
||||
)
|
||||
contact_tuple = tuple(detail for detail in details if detail is not None)
|
||||
|
||||
self.regr = self.network.register(
|
||||
tuple(detail for detail in details if detail is not None))
|
||||
# TODO: Replace with real info once through testing.
|
||||
if not contact_tuple:
|
||||
contact_tuple = ("mailto:letsencrypt-client@letsencrypt.org",
|
||||
"tel:+12025551212")
|
||||
self.regr = self.network.register(contact=contact_tuple)
|
||||
|
||||
# If terms of service exist... we need to sign it.
|
||||
# TODO: Replace the `preview EULA` with this...
|
||||
if self.regr.terms_of_service:
|
||||
self.network.agree_to_tos(self.regr)
|
||||
|
||||
def set_regr(self, regr):
|
||||
"""Set a preexisting registration resource."""
|
||||
@@ -122,21 +132,26 @@ class Client(object):
|
||||
|
||||
# Perform Challenges/Get Authorizations
|
||||
if self.regr.new_authzr_uri:
|
||||
self.auth_handler.get_authorizations(domains, self.regr)
|
||||
authzr = self.auth_handler.get_authorizations(
|
||||
domains, self.regr.new_authzr_uri)
|
||||
else:
|
||||
self.auth_handler.get_authorizations(
|
||||
domains, self.config.server + "/acme/new-authorization")
|
||||
authzr = self.auth_handler.get_authorizations(
|
||||
domains,
|
||||
"https://%s/acme/new-authz" % self.config.server)
|
||||
|
||||
# Create CSR from names
|
||||
if csr is None:
|
||||
csr = init_csr(self.authkey, domains, self.config.cert_dir)
|
||||
|
||||
# Retrieve certificate
|
||||
certificate_msg = self.acme_certificate(csr.data)
|
||||
certr = self.network.request_issuance(
|
||||
jose.ComparableX509(
|
||||
M2Crypto.X509.load_request_der_string(csr.data)),
|
||||
authzr)
|
||||
|
||||
# Save Certificate
|
||||
cert_file, chain_file = self.save_certificate(
|
||||
certificate_msg, self.config.cert_path, self.config.chain_path)
|
||||
certr, self.config.cert_path, self.config.chain_path)
|
||||
|
||||
revoker.Revoker.store_cert_key(
|
||||
cert_file, self.authkey.file, self.config)
|
||||
@@ -172,12 +187,12 @@ class Client(object):
|
||||
self.authkey.pem))),
|
||||
messages.Certificate)
|
||||
|
||||
def save_certificate(self, certificate_msg, cert_path, chain_path):
|
||||
def save_certificate(self, certr, cert_path, chain_path):
|
||||
# pylint: disable=no-self-use
|
||||
"""Saves the certificate received from the ACME server.
|
||||
|
||||
:param certificate_msg: ACME "certificate" message from server.
|
||||
:type certificate_msg: :class:`letsencrypt.acme.messages.Certificate`
|
||||
:param certr: ACME "certificate" resource.
|
||||
:type certr: :class:`letsencrypt.acme.messages.Certificate`
|
||||
|
||||
:param str cert_path: Path to attempt to save the cert file
|
||||
:param str chain_path: Path to attempt to save the chain file
|
||||
@@ -188,17 +203,19 @@ class Client(object):
|
||||
:raises IOError: If unable to find room to write the cert files
|
||||
|
||||
"""
|
||||
# try finally close
|
||||
cert_chain_abspath = None
|
||||
cert_fd, cert_file = le_util.unique_file(cert_path, 0o644)
|
||||
cert_fd.write(certificate_msg.certificate.as_pem())
|
||||
cert_fd.write(certr.body.as_pem())
|
||||
cert_fd.close()
|
||||
logging.info(
|
||||
"Server issued certificate; certificate written to %s", cert_file)
|
||||
|
||||
if certificate_msg.chain:
|
||||
if certr.cert_chain_uri:
|
||||
# try finally close
|
||||
chain_cert = self.network.fetch_chain(certr.cert_chain_uri)
|
||||
chain_fd, chain_fn = le_util.unique_file(chain_path, 0o644)
|
||||
for cert in certificate_msg.chain:
|
||||
chain_fd.write(cert.to_pem())
|
||||
chain_fd.write(chain_cert.to_pem())
|
||||
chain_fd.close()
|
||||
|
||||
logging.info("Cert chain written to %s", chain_fn)
|
||||
|
||||
@@ -50,6 +50,7 @@ class Network(object):
|
||||
"""
|
||||
dumps = obj.json_dumps()
|
||||
logging.debug('Serialized JSON: %s', dumps)
|
||||
print "json_dumps:", dumps
|
||||
return jose.JWS.sign(
|
||||
payload=dumps, key=self.key, alg=self.alg).json_dumps()
|
||||
|
||||
@@ -74,7 +75,6 @@ class Network(object):
|
||||
|
||||
"""
|
||||
response_ct = response.headers.get('Content-Type')
|
||||
|
||||
try:
|
||||
# TODO: response.json() is called twice, once here, and
|
||||
# once in _get and _post clients
|
||||
@@ -83,6 +83,9 @@ class Network(object):
|
||||
jobj = None
|
||||
|
||||
if not response.ok:
|
||||
print response
|
||||
print response.headers
|
||||
print response.content
|
||||
if jobj is not None:
|
||||
if response_ct != cls.JSON_ERROR_CONTENT_TYPE:
|
||||
logging.debug(
|
||||
@@ -167,7 +170,7 @@ class Network(object):
|
||||
'contact'].default):
|
||||
"""Register.
|
||||
|
||||
:param contact: Contact list, as accepted by `.RegistrationResource`
|
||||
:param contact: Contact list, as accepted by `.Registration`
|
||||
:type contact: `tuple`
|
||||
|
||||
:returns: Registration Resource.
|
||||
@@ -213,6 +216,21 @@ class Network(object):
|
||||
raise errors.UnexpectedUpdate(regr)
|
||||
return updated_regr
|
||||
|
||||
def agree_to_tos(self, regr):
|
||||
"""Agree to the terms-of-service.
|
||||
|
||||
Agree to the terms-of-service in a Registration Resource.
|
||||
|
||||
:param regr: Registration Resource.
|
||||
:type regr: `.RegistrationResource`
|
||||
|
||||
:returns: Updated Registration Resource.
|
||||
:rtype: `.RegistrationResource`
|
||||
|
||||
"""
|
||||
self.update_registration(
|
||||
regr.update(body=regr.body.update(agreement=regr.terms_of_service)))
|
||||
|
||||
def _authzr_from_response(self, response, identifier,
|
||||
uri=None, new_cert_uri=None):
|
||||
if new_cert_uri is None:
|
||||
@@ -279,20 +297,23 @@ class Network(object):
|
||||
:raises errors.UnexpectedUpdate:
|
||||
|
||||
"""
|
||||
print "sendinging challenge to:", challb.uri
|
||||
response = self._post(challb.uri, self._wrap_in_jws(response))
|
||||
try:
|
||||
authzr_uri = response.links['up']['url']
|
||||
except KeyError:
|
||||
raise errors.NetworkError('"up" Link header missing')
|
||||
challr = messages2.ChallengeResource(
|
||||
# TODO: Right now Boulder responds with the authorization resource
|
||||
# instead of a challenge resource... this can be uncommented
|
||||
# once the error is fixed.
|
||||
return challb
|
||||
# raise errors.NetworkError('"up" Link header missing')
|
||||
challr2 = messages2.ChallengeResource(
|
||||
authzr_uri=authzr_uri,
|
||||
body=messages2.ChallengeBody.from_json(response.json()))
|
||||
# TODO: check that challr.uri == response.headers['Location']?
|
||||
if challr.uri != challb.uri:
|
||||
raise errors.UnexpectedUpdate(challr.uri)
|
||||
return challr
|
||||
|
||||
def poll_challenge(self, chall):
|
||||
if challr2.uri != challb.uri:
|
||||
raise errors.UnexpectedUpdate(challb.uri)
|
||||
return challr2
|
||||
|
||||
@classmethod
|
||||
def retry_after(cls, response, default):
|
||||
@@ -352,6 +373,8 @@ class Network(object):
|
||||
|
||||
"""
|
||||
assert authzrs, "Authorizations list is empty"
|
||||
logging.debug("Requesting issuance...")
|
||||
print "Requesting issuance: ", authzrs[0]
|
||||
|
||||
# TODO: assert len(authzrs) == number of SANs
|
||||
req = messages2.CertificateRequest(
|
||||
@@ -402,7 +425,7 @@ class Network(object):
|
||||
:rtype: `tuple`
|
||||
|
||||
"""
|
||||
# priority queue with datetime (based od Retry-After) as key,
|
||||
# priority queue with datetime (based on Retry-After) as key,
|
||||
# and original Authorization Resource as value
|
||||
waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs]
|
||||
# mapping between original Authorization Resource and the most
|
||||
|
||||
@@ -57,7 +57,7 @@ def create_parser():
|
||||
config_help = lambda name: interfaces.IConfig[name].__doc__
|
||||
|
||||
add("-d", "--domains", metavar="DOMAIN", nargs="+")
|
||||
add("-s", "--server", default="letsencrypt-demo.org:443",
|
||||
add("-s", "--server", default="www.letsencrypt-demo.org",
|
||||
help=config_help("server"))
|
||||
|
||||
add("-k", "--authkey", type=read_file,
|
||||
@@ -202,6 +202,7 @@ def main(): # pylint: disable=too-many-branches, too-many-statements
|
||||
# but this code should be safe on all environments.
|
||||
cert_file = None
|
||||
if auth is not None:
|
||||
acme.register()
|
||||
cert_file, chain_file = acme.obtain_certificate(doms)
|
||||
if installer is not None and cert_file is not None:
|
||||
acme.deploy_certificate(doms, authkey, cert_file, chain_file)
|
||||
|
||||
Reference in New Issue
Block a user