Merge remote-tracking branch 'upstream/master' into multios_apache

This commit is contained in:
Joona Hoikkala
2015-12-14 09:26:07 +02:00
23 changed files with 645 additions and 131 deletions
+4 -2
View File
@@ -20,7 +20,9 @@ from acme import messages
logger = logging.getLogger(__name__)
# Python does not validate certificates by default before version 2.7.9
# Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure
# many important security related options. On these platforms we use PyOpenSSL
# for SSL, which does allow these options to be configured.
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
@@ -338,7 +340,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
`PollError` with non-empty ``waiting`` is raised.
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
the issued certificate (`.messages.CertificateResource.),
the issued certificate (`.messages.CertificateResource`),
and ``updated_authzrs`` is a `tuple` consisting of updated
Authorization Resources (`.AuthorizationResource`) as
present in the responses from server, and in the same order
+1 -2
View File
@@ -28,8 +28,7 @@ acme = client.Client(DIRECTORY_URL, key)
regr = acme.register()
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
acme.update_registration(regr.update(
body=regr.body.update(agreement=regr.terms_of_service)))
acme.agree_to_tos(regr)
logging.debug(regr)
authzr = acme.request_challenges(
+5 -3
View File
@@ -224,21 +224,23 @@ The following files are available:
``cert.pem``
Server certificate only.
This is what Apache needs for `SSLCertificateFile
This is what Apache < 2.4.8 needs for `SSLCertificateFile
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_.
``chain.pem``
All certificates that need to be served by the browser **excluding**
server certificate, i.e. root and intermediate certificates only.
This is what Apache needs for `SSLCertificateChainFile
This is what Apache < 2.4.8 needs for `SSLCertificateChainFile
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatechainfile>`_.
``fullchain.pem``
All certificates, **including** server certificate. This is
concatenation of ``chain.pem`` and ``cert.pem``.
This is what nginx needs for `ssl_certificate
This is what Apache >= 2.4.8 needs for `SSLCertificateFile
<https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslcertificatefile>`_,
and what nginx needs for `ssl_certificate
<http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate>`_.
@@ -51,7 +51,7 @@ let sep_osp = Sep.opt_space
let sep_eq = del /[ \t]*=[ \t]*/ "="
let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
let word = /[a-zA-Z][a-zA-Z0-9._-]*/
let word = /[a-z][a-z0-9._-]*/i
let comment = Util.comment
let eol = Util.doseol
@@ -68,6 +68,9 @@ let cl = /\\\\\n/
let dquot =
let no_dquot = /[^"\\\r\n]/
in /"/ . (no_dquot|cdot|cl)* . /"/
let dquot_msg =
let no_dquot = /([^ \t"\\\r\n]|[^"\\\r\n]+[^ \t"\\\r\n])/
in /"/ . (no_dquot|cdot|cl)*
let squot =
let no_squot = /[^'\\\r\n]/
in /'/ . (no_squot|cdot|cl)* . /'/
@@ -78,6 +81,8 @@ let comp = /[<>=]?=/
*****************************************************************)
let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ]
(* message argument starts with " but ends at EOL *)
let arg_dir_msg = [ label "arg" . store dquot_msg ]
let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
let arg_wl = [ label "arg" . store (char_arg_wl+|dquot|squot) ]
@@ -90,8 +95,10 @@ let arg_wordlist =
let argv (l:lens) = l . (sep_spc . l)*
let directive = [ indent . label "directive" . store word .
(sep_spc . argv (arg_dir|arg_wordlist))? . eol ]
let directive =
(* arg_dir_msg may be the last or only argument *)
let dir_args = (argv (arg_dir|arg_wordlist) . (sep_spc . arg_dir_msg)?) | arg_dir_msg
in [ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
let section (body:lens) =
(* opt_eol includes empty lines *)
@@ -1,5 +1,3 @@
<VirtualHost 1.1.1.1>
ServerName invalid.net
</virtualHost>
+178 -65
View File
@@ -205,76 +205,131 @@ def _find_duplicative_certs(config, domains):
if candidate_names == set(domains):
identical_names_cert = candidate_lineage
elif candidate_names.issubset(set(domains)):
subset_names_cert = candidate_lineage
# This logic finds and returns the largest subset-names cert
# in the case where there are several available.
if subset_names_cert is None:
subset_names_cert = candidate_lineage
elif len(candidate_names) > len(subset_names_cert.names()):
subset_names_cert = candidate_lineage
return identical_names_cert, subset_names_cert
def _treat_as_renewal(config, domains):
"""Determine whether or not the call should be treated as a renewal.
"""Determine whether there are duplicated names and how to handle them.
:returns: RenewableCert or None if renewal shouldn't occur.
:rtype: :class:`.storage.RenewableCert`
:returns: Two-element tuple containing desired new-certificate behavior as
a string token ("reinstall", "renew", or "newcert"), plus either
a RenewableCert instance or None if renewal shouldn't occur.
:raises .Error: If the user would like to rerun the client again.
"""
renewal = False
# Considering the possibility that the requested certificate is
# related to an existing certificate. (config.duplicate, which
# is set with --duplicate, skips all of this logic and forces any
# kind of certificate to be obtained with renewal = False.)
if not config.duplicate:
ident_names_cert, subset_names_cert = _find_duplicative_certs(
config, domains)
# I am not sure whether that correctly reads the systemwide
# configuration file.
question = None
if ident_names_cert is not None:
question = (
"You have an existing certificate that contains exactly the "
"same domains you requested (ref: {0}){br}{br}Do you want to "
"renew and replace this certificate with a newly-issued one?"
).format(ident_names_cert.configfile.filename, br=os.linesep)
elif subset_names_cert is not None:
question = (
"You have an existing certificate that contains a portion of "
"the domains you requested (ref: {0}){br}{br}It contains these "
"names: {1}{br}{br}You requested these names for the new "
"certificate: {2}.{br}{br}Do you want to replace this existing "
"certificate with the new certificate?"
).format(subset_names_cert.configfile.filename,
", ".join(subset_names_cert.names()),
", ".join(domains),
br=os.linesep)
if question is None:
# We aren't in a duplicative-names situation at all, so we don't
# have to tell or ask the user anything about this.
pass
elif config.renew_by_default or zope.component.getUtility(
interfaces.IDisplay).yesno(question, "Replace", "Cancel"):
renewal = True
else:
reporter_util = zope.component.getUtility(interfaces.IReporter)
reporter_util.add_message(
"To obtain a new certificate that {0} an existing certificate "
"in its domain-name coverage, you must use the --duplicate "
"option.{br}{br}For example:{br}{br}{1} --duplicate {2}".format(
"duplicates" if ident_names_cert is not None else
"overlaps with",
sys.argv[0], " ".join(sys.argv[1:]),
br=os.linesep
),
reporter_util.HIGH_PRIORITY)
raise errors.Error(
"User did not use proper CLI and would like "
"to reinvoke the client.")
if config.duplicate:
return "newcert", None
# TODO: Also address superset case
ident_names_cert, subset_names_cert = _find_duplicative_certs(config, domains)
# XXX ^ schoen is not sure whether that correctly reads the systemwide
# configuration file.
if ident_names_cert is None and subset_names_cert is None:
return "newcert", None
if renewal:
return ident_names_cert if ident_names_cert is not None else subset_names_cert
if ident_names_cert is not None:
return _handle_identical_cert_request(config, ident_names_cert)
elif subset_names_cert is not None:
return _handle_subset_cert_request(config, domains, subset_names_cert)
return None
def _handle_identical_cert_request(config, cert):
"""Figure out what to do if a cert has the same names as a perviously obtained one
:param storage.RenewableCert cert:
:returns: Tuple of (string, cert_or_None) as per _treat_as_renewal
:rtype: tuple
"""
if config.renew_by_default:
logger.info("Auto-renewal forced with --renew-by-default...")
return "renew", cert
if cert.should_autorenew(interactive=True):
logger.info("Cert is due for renewal, auto-renewing...")
return "renew", cert
if config.reinstall:
# Set with --reinstall, force an identical certificate to be
# reinstalled without further prompting.
return "reinstall", cert
question = (
"You have an existing certificate that contains exactly the same "
"domains you requested and isn't close to expiry."
"{br}(ref: {0}){br}{br}What would you like to do?"
).format(cert.configfile.filename, br=os.linesep)
if config.verb == "run":
keep_opt = "Attempt to reinstall this existing certificate"
elif config.verb == "certonly":
keep_opt = "Keep the existing certificate for now"
choices = [keep_opt,
"Renew & replace the cert (limit ~5 per 7 days)",
"Cancel this operation and do nothing"]
display = zope.component.getUtility(interfaces.IDisplay)
response = display.menu(question, choices, "OK", "Cancel")
if response[0] == "cancel" or response[1] == 2:
# TODO: Add notification related to command-line options for
# skipping the menu for this case.
raise errors.Error(
"User chose to cancel the operation and may "
"reinvoke the client.")
elif response[1] == 0:
return "reinstall", cert
elif response[1] == 1:
return "renew", cert
else:
assert False, "This is impossible"
def _handle_subset_cert_request(config, domains, cert):
"""Figure out what to do if a previous cert had a subset of the names now requested
:param storage.RenewableCert cert:
:returns: Tuple of (string, cert_or_None) as per _treat_as_renewal
:rtype: tuple
"""
existing = ", ".join(cert.names())
question = (
"You have an existing certificate that contains a portion of "
"the domains you requested (ref: {0}){br}{br}It contains these "
"names: {1}{br}{br}You requested these names for the new "
"certificate: {2}.{br}{br}Do you want to expand and replace this existing "
"certificate with the new certificate?"
).format(cert.configfile.filename,
existing,
", ".join(domains),
br=os.linesep)
if config.expand or config.renew_by_default or zope.component.getUtility(
interfaces.IDisplay).yesno(question, "Expand", "Cancel"):
return "renew", cert
else:
reporter_util = zope.component.getUtility(interfaces.IReporter)
reporter_util.add_message(
"To obtain a new certificate that contains these names without "
"replacing your existing certificate for {0}, you must use the "
"--duplicate option.{br}{br}"
"For example:{br}{br}{1} --duplicate {2}".format(
existing,
sys.argv[0], " ".join(sys.argv[1:]),
br=os.linesep
),
reporter_util.HIGH_PRIORITY)
raise errors.Error(
"User chose to cancel the operation and may "
"reinvoke the client.")
def _report_new_cert(cert_path, fullchain_path):
@@ -314,10 +369,21 @@ def _suggest_donate():
def _auth_from_domains(le_client, config, domains):
"""Authenticate and enroll certificate."""
# Note: This can raise errors... caught above us though.
lineage = _treat_as_renewal(config, domains)
# Note: This can raise errors... caught above us though. This is now
# a three-way case: reinstall (which results in a no-op here because
# although there is a relevant lineage, we don't do anything to it
# inside this function -- we don't obtain a new certificate), renew
# (which results in treating the request as a renewal), or newcert
# (which results in treating the request as a new certificate request).
if lineage is not None:
action, lineage = _treat_as_renewal(config, domains)
if action == "reinstall":
# The lineage already exists; allow the caller to try installing
# it without getting a new certificate at all.
return lineage
elif action == "renew":
original_server = lineage.configuration["renewalparams"]["server"]
_avoid_invalidating_lineage(config, lineage, original_server)
# TODO: schoen wishes to reuse key - discussion
# https://github.com/letsencrypt/letsencrypt/pull/777/files#r40498574
new_certr, new_chain, new_key, _ = le_client.obtain_certificate(domains)
@@ -331,7 +397,7 @@ def _auth_from_domains(le_client, config, domains):
# TODO: Check return value of save_successor
# TODO: Also update lineage renewal config with any relevant
# configuration values from this attempt? <- Absolutely (jdkasten)
else:
elif action == "newcert":
# TREAT AS NEW REQUEST
lineage = le_client.obtain_and_enroll_certificate(domains)
if not lineage:
@@ -341,6 +407,27 @@ def _auth_from_domains(le_client, config, domains):
return lineage
def _avoid_invalidating_lineage(config, lineage, original_server):
"Do not renew a valid cert with one from a staging server!"
def _is_staging(srv):
return srv == constants.STAGING_URI or "staging" in srv
# Some lineages may have begun with --staging, but then had production certs
# added to them
latest_cert = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
open(lineage.cert).read())
# all our test certs are from happy hacker fake CA, though maybe one day
# we should test more methodically
now_valid = not "fake" in repr(latest_cert.get_issuer()).lower()
if _is_staging(config.server):
if not _is_staging(original_server) or now_valid:
if not config.break_my_certs:
names = ", ".join(lineage.names())
raise errors.Error(
"You've asked to renew/replace a seemingly valid certificate with "
"a test certificate (domains: {0}). We will not do that "
"unless you use the --break-my-certs flag!".format(names))
def set_configurator(previously, now):
"""
@@ -485,7 +572,6 @@ def run(args, config, plugins): # pylint: disable=too-many-branches,too-many-lo
def obtain_cert(args, config, plugins):
"""Authenticate & obtain cert, but do not install it."""
if args.domains and args.csr is not None:
# TODO: --csr could have a priority, when --domains is
# supplied, check if CSR matches given domains?
@@ -695,6 +781,15 @@ class HelpfulArgumentParser(object):
"""
parsed_args = self.parser.parse_args(self.args)
parsed_args.func = self.VERBS[self.verb]
parsed_args.verb = self.verb
# Do any post-parsing homework here
# argparse seemingly isn't flexible enough to give us this behaviour easily...
if parsed_args.staging:
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
raise errors.Error("--server value conflicts with --staging")
parsed_args.server = constants.STAGING_URI
return parsed_args
@@ -869,13 +964,19 @@ def prepare_and_parse_args(plugins, args):
help="Domain names to apply. For multiple domains you can use "
"multiple -d flags or enter a comma separated list of domains "
"as a parameter.")
helpful.add(
None, "--duplicate", dest="duplicate", action="store_true",
help="Allow getting a certificate that duplicates an existing one")
helpful.add_group(
"automation",
description="Arguments for automating execution & other tweaks")
helpful.add(
"automation", "--keep-until-expiring", "--keep", "--reinstall",
dest="reinstall", action="store_true",
help="If the requested cert matches an existing cert, always keep the "
"existing one until it is due for renewal (for the "
"'run' subcommand this means reinstall the existing cert)")
helpful.add(
"automation", "--expand", action="store_true",
help="If an existing cert covers some subset of the requested names, "
"always expand and replace it with the additional names.")
helpful.add(
"automation", "--version", action="version",
version="%(prog)s {0}".format(letsencrypt.__version__),
@@ -883,13 +984,18 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
"automation", "--renew-by-default", action="store_true",
help="Select renewal by default when domains are a superset of a "
"previously attained cert")
"previously attained cert (often --keep-until-expiring is "
"more appropriate). Implies --expand.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
help="Agree to the Let's Encrypt Subscriber Agreement")
helpful.add(
"automation", "--account", metavar="ACCOUNT_ID",
help="Account ID to use")
helpful.add(
"automation", "--duplicate", dest="duplicate", action="store_true",
help="Allow making a certificate lineage that duplicates an existing one "
"(both can be renewed in parallel)")
helpful.add_group(
"testing", description="The following flags are meant for "
@@ -910,7 +1016,10 @@ def prepare_and_parse_args(plugins, args):
helpful.add(
"testing", "--http-01-port", type=int, dest="http01_port",
default=flag_default("http01_port"), help=config_help("http01_port"))
helpful.add(
"testing", "--break-my-certs", action="store_true",
help="Be willing to replace or renew valid certs with invalid "
"(testing/staging) certs")
helpful.add_group(
"security", description="Security parameters & server settings")
helpful.add(
@@ -1037,6 +1146,10 @@ def _paths_parser(helpful):
help="Logs directory.")
add("paths", "--server", default=flag_default("server"),
help=config_help("server"))
# overwrites server, handled in HelpfulArgumentParser.parse_args()
add("testing", "--test-cert", "--staging", action='store_true', dest='staging',
help='Use the staging server to obtain test (invalid) certs; equivalent'
' to --server ' + constants.STAGING_URI)
def _plugins_parsing(helpful, plugins):
+2 -1
View File
@@ -30,8 +30,9 @@ CLI_DEFAULTS = dict(
auth_chain_path="./chain.pem",
strict_permissions=False,
)
"""Defaults for CLI flags and `.IConfig` attributes."""
STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
"""Defaults for CLI flags and `.IConfig` attributes."""
RENEWER_DEFAULTS = dict(
renewer_enabled="yes",
+4
View File
@@ -10,6 +10,8 @@ import stat
import subprocess
import sys
import configargparse
from letsencrypt import errors
@@ -278,9 +280,11 @@ def add_deprecated_argument(add_argument, argument_name, nargs):
sys.stderr.write(
"Use of {0} is deprecated.\n".format(option_string))
configargparse.ACTION_TYPES_THAT_DONT_NEED_A_VALUE.add(ShowWarning)
add_argument(argument_name, action=ShowWarning,
help=argparse.SUPPRESS, nargs=nargs)
def check_domain_sanity(domain):
"""Method which validates domain value and errors out if
the requirements are not met.
+8 -11
View File
@@ -2,7 +2,6 @@
import argparse
import collections
import logging
import random
import socket
import threading
@@ -108,7 +107,7 @@ class ServerManager(object):
in six.iteritems(self._instances))
SUPPORTED_CHALLENGES = set([challenges.TLSSNI01, challenges.HTTP01])
SUPPORTED_CHALLENGES = [challenges.TLSSNI01, challenges.HTTP01]
def supported_challenges_validator(data):
@@ -166,16 +165,16 @@ class Authenticator(common.Plugin):
@classmethod
def add_parser_arguments(cls, add):
add("supported-challenges", help="Supported challenges, "
"order preferences are randomly chosen.",
type=supported_challenges_validator, default=",".join(
sorted(chall.typ for chall in SUPPORTED_CHALLENGES)))
add("supported-challenges",
help="Supported challenges. Preferred in the order they are listed.",
type=supported_challenges_validator,
default=",".join(chall.typ for chall in SUPPORTED_CHALLENGES))
@property
def supported_challenges(self):
"""Challenges supported by this plugin."""
return set(challenges.Challenge.TYPES[name] for name in
self.conf("supported-challenges").split(","))
return [challenges.Challenge.TYPES[name] for name in
self.conf("supported-challenges").split(",")]
@property
def _necessary_ports(self):
@@ -198,9 +197,7 @@ class Authenticator(common.Plugin):
def get_chall_pref(self, domain):
# pylint: disable=unused-argument,missing-docstring
chall_pref = list(self.supported_challenges)
random.shuffle(chall_pref) # 50% for each challenge
return chall_pref
return self.supported_challenges
def perform(self, achalls): # pylint: disable=missing-docstring
if any(util.already_listening(port) for port in self._necessary_ports):
+14 -4
View File
@@ -98,17 +98,27 @@ class AuthenticatorTest(unittest.TestCase):
def test_supported_challenges(self):
self.assertEqual(self.auth.supported_challenges,
set([challenges.TLSSNI01, challenges.HTTP01]))
[challenges.TLSSNI01, challenges.HTTP01])
def test_supported_challenges_configured(self):
self.config.standalone_supported_challenges = "tls-sni-01"
self.assertEqual(self.auth.supported_challenges,
[challenges.TLSSNI01])
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.TLSSNI01, challenges.HTTP01]))
self.assertEqual(self.auth.get_chall_pref(domain=None),
[challenges.TLSSNI01, challenges.HTTP01])
def test_get_chall_pref_configured(self):
self.config.standalone_supported_challenges = "tls-sni-01"
self.assertEqual(self.auth.get_chall_pref(domain=None),
[challenges.TLSSNI01])
@mock.patch("letsencrypt.plugins.standalone.util")
def test_perform_alredy_listening(self, mock_util):
def test_perform_already_listening(self, mock_util):
for chall, port in ((challenges.TLSSNI01.typ, 1234),
(challenges.HTTP01.typ, 4321)):
mock_util.already_listening.return_value = True
+35 -22
View File
@@ -2,7 +2,6 @@
import errno
import logging
import os
import stat
import zope.interface
@@ -59,24 +58,38 @@ to serve all files under specified web root ({0})."""
logger.debug("Creating root challenges validation dir at %s",
self.full_roots[name])
# Change the permissions to be writable (GH #1389)
# Umask is used instead of chmod to ensure the client can also
# run as non-root (GH #1795)
old_umask = os.umask(0o022)
try:
os.makedirs(self.full_roots[name])
# Set permissions as parent directory (GH #1389)
# We don't use the parameters in makedirs because it
# may not always work
# This is coupled with the "umask" call above because
# os.makedirs's "mode" parameter may not always work:
# https://stackoverflow.com/questions/5231901/permission-problems-when-creating-a-dir-with-os-makedirs-python
stat_path = os.stat(path)
filemode = stat.S_IMODE(stat_path.st_mode)
os.chmod(self.full_roots[name], filemode)
# Set owner and group, too
os.chown(self.full_roots[name], stat_path.st_uid,
stat_path.st_gid)
os.makedirs(self.full_roots[name], 0o0755)
# Set owner as parent directory if possible
try:
stat_path = os.stat(path)
os.chown(self.full_roots[name], stat_path.st_uid,
stat_path.st_gid)
except OSError as exception:
if exception.errno == errno.EACCES:
logger.debug("Insufficient permissions to change owner and uid - ignoring")
else:
raise errors.PluginError(
"Couldn't create root for {0} http-01 "
"challenge responses: {1}", name, exception)
except OSError as exception:
if exception.errno != errno.EEXIST:
raise errors.PluginError(
"Couldn't create root for {0} http-01 "
"challenge responses: {1}", name, exception)
finally:
os.umask(old_umask)
def perform(self, achalls): # pylint: disable=missing-docstring
assert self.full_roots, "Webroot plugin appears to be missing webroot map"
@@ -87,26 +100,26 @@ to serve all files under specified web root ({0})."""
path = self.full_roots[achall.domain]
except IndexError:
raise errors.PluginError("Missing --webroot-path for domain: {1}"
.format(achall.domain))
.format(achall.domain))
if not os.path.exists(path):
raise errors.PluginError("Mysteriously missing path {0} for domain: {1}"
.format(path, achall.domain))
.format(path, achall.domain))
return os.path.join(path, achall.chall.encode("token"))
def _perform_single(self, achall):
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.encode())
# Set permissions as parent directory (GH #1389)
parent_path = self.full_roots[achall.domain]
stat_parent_path = os.stat(parent_path)
filemode = stat.S_IMODE(stat_parent_path.st_mode)
# Remove execution bit (not needed for this file)
os.chmod(path, filemode & ~stat.S_IEXEC)
os.chown(path, stat_parent_path.st_uid, stat_parent_path.st_gid)
# Change permissions to be world-readable, owner-writable (GH #1795)
old_umask = os.umask(0o022)
try:
with open(path, "w") as validation_file:
validation_file.write(validation.encode())
finally:
os.umask(old_umask)
return response
+24 -7
View File
@@ -1,9 +1,10 @@
"""Tests for letsencrypt.plugins.webroot."""
import errno
import os
import shutil
import stat
import tempfile
import unittest
import stat
import mock
@@ -35,7 +36,6 @@ class AuthenticatorTest(unittest.TestCase):
self.config = mock.MagicMock(webroot_path=self.path,
webroot_map={"thing.com": self.path})
self.auth = Authenticator(self.config, "webroot")
self.auth.prepare()
def tearDown(self):
shutil.rmtree(self.path)
@@ -48,7 +48,7 @@ class AuthenticatorTest(unittest.TestCase):
def test_add_parser_arguments(self):
add = mock.MagicMock()
self.auth.add_parser_arguments(add)
self.assertEqual(0, add.call_count) # became 0 when we moved the args to cli.py!
self.assertEqual(0, add.call_count) # args moved to cli.py!
def test_prepare_bad_root(self):
self.config.webroot_path = os.path.join(self.path, "null")
@@ -70,17 +70,33 @@ class AuthenticatorTest(unittest.TestCase):
self.assertRaises(errors.PluginError, self.auth.prepare)
os.chmod(self.path, 0o700)
@mock.patch("letsencrypt.plugins.webroot.os.chown")
def test_failed_chown_eacces(self, mock_chown):
mock_chown.side_effect = OSError(errno.EACCES, "msg")
self.auth.prepare() # exception caught and logged
@mock.patch("letsencrypt.plugins.webroot.os.chown")
def test_failed_chown_not_eacces(self, mock_chown):
mock_chown.side_effect = OSError()
self.assertRaises(errors.PluginError, self.auth.prepare)
def test_prepare_permissions(self):
self.auth.prepare()
# Remove exec bit from permission check, so that it
# matches the file
self.auth.perform([self.achall])
parent_permissions = (stat.S_IMODE(os.stat(self.path).st_mode) &
~stat.S_IEXEC)
path_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
self.assertEqual(path_permissions, 0o644)
actual_permissions = stat.S_IMODE(os.stat(self.validation_path).st_mode)
# Check permissions of the directories
for dirpath, dirnames, _ in os.walk(self.path):
for directory in dirnames:
full_path = os.path.join(dirpath, directory)
dir_permissions = stat.S_IMODE(os.stat(full_path).st_mode)
self.assertEqual(dir_permissions, 0o755)
self.assertEqual(parent_permissions, actual_permissions)
parent_gid = os.stat(self.path).st_gid
parent_uid = os.stat(self.path).st_uid
@@ -88,6 +104,7 @@ class AuthenticatorTest(unittest.TestCase):
self.assertEqual(os.stat(self.validation_path).st_uid, parent_uid)
def test_perform_cleanup(self):
self.auth.prepare()
responses = self.auth.perform([self.achall])
self.assertEqual(1, len(responses))
self.assertTrue(os.path.exists(self.validation_path))
+14 -6
View File
@@ -471,7 +471,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
return ("autodeploy" not in self.configuration or
self.configuration.as_bool("autodeploy"))
def should_autodeploy(self):
def should_autodeploy(self, interactive=False):
"""Should this lineage now automatically deploy a newer version?
This is a policy question and does not only depend on whether
@@ -480,12 +480,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
exists, and whether the time interval for autodeployment has
been reached.)
:param bool interactive: set to True to examine the question
regardless of whether the renewal configuration allows
automated deployment (for interactive use). Default False.
:returns: whether the lineage now ought to autodeploy an
existing newer cert version
:rtype: bool
"""
if self.autodeployment_is_enabled():
if interactive or self.autodeployment_is_enabled():
if self.has_pending_deployment():
interval = self.configuration.get("deploy_before_expiry",
"5 days")
@@ -529,7 +533,7 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
return ("autorenew" not in self.configuration or
self.configuration.as_bool("autorenew"))
def should_autorenew(self):
def should_autorenew(self, interactive=False):
"""Should we now try to autorenew the most recent cert version?
This is a policy question and does not only depend on whether
@@ -540,12 +544,16 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
Note that this examines the numerically most recent cert version,
not the currently deployed version.
:param bool interactive: set to True to examine the question
regardless of whether the renewal configuration allows
automated renewal (for interactive use). Default False.
:returns: whether an attempt should now be made to autorenew the
most current cert version in this lineage
:rtype: bool
"""
if self.autorenewal_is_enabled():
if interactive or self.autorenewal_is_enabled():
# Consider whether to attempt to autorenew this cert now
# Renewals on the basis of revocation
@@ -559,8 +567,8 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
"cert", self.latest_common_version()))
now = pytz.UTC.fromutc(datetime.datetime.utcnow())
if expiry < add_time_interval(now, interval):
logger.debug("Should renew, certificate "
"has been expired since %s.",
logger.debug("Should renew, less than %s before certificate "
"expiry %s.", interval,
expiry.strftime("%Y-%m-%d %H:%M:%S %Z"))
return True
return False
+22 -3
View File
@@ -15,6 +15,7 @@ from acme import jose
from letsencrypt import account
from letsencrypt import cli
from letsencrypt import configuration
from letsencrypt import constants
from letsencrypt import crypto_util
from letsencrypt import errors
from letsencrypt import le_util
@@ -343,6 +344,19 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
namespace = cli.prepare_and_parse_args(plugins, long_args)
self.assertEqual(namespace.domains, ['example.com', 'another.net'])
def test_parse_server(self):
plugins = disco.PluginsRegistry.find_all()
short_args = ['--server', 'example.com']
namespace = cli.prepare_and_parse_args(plugins, short_args)
self.assertEqual(namespace.server, 'example.com')
short_args = ['--staging']
namespace = cli.prepare_and_parse_args(plugins, short_args)
self.assertEqual(namespace.server, constants.STAGING_URI)
short_args = ['--staging', '--server', 'example.com']
self.assertRaises(errors.Error, cli.prepare_and_parse_args, plugins, short_args)
def test_parse_webroot(self):
plugins = disco.PluginsRegistry.find_all()
webroot_args = ['--webroot', '-w', '/var/www/example',
@@ -389,7 +403,7 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
def _certonly_new_request_common(self, mock_client):
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
mock_renewal.return_value = None
mock_renewal.return_value = ("newcert", None)
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
mock_init.return_value = mock_client
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
@@ -399,13 +413,13 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
@mock.patch('letsencrypt.cli._treat_as_renewal')
@mock.patch('letsencrypt.cli._init_le_client')
def test_certonly_renewal(self, mock_init, mock_renewal, mock_get_utility, _suggest):
cert_path = '/etc/letsencrypt/live/foo.bar/cert.pem'
cert_path = 'letsencrypt/tests/testdata/cert.pem'
chain_path = '/etc/letsencrypt/live/foo.bar/fullchain.pem'
mock_lineage = mock.MagicMock(cert=cert_path, fullchain=chain_path)
mock_cert = mock.MagicMock(body='body')
mock_key = mock.MagicMock(pem='pem_key')
mock_renewal.return_value = mock_lineage
mock_renewal.return_value = ("renew", mock_lineage)
mock_client = mock.MagicMock()
mock_client.obtain_certificate.return_value = (mock_cert, 'chain',
mock_key, 'csr')
@@ -537,6 +551,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(path, os.path.abspath(path))
self.assertEqual(contents, test_contents)
def test_agree_dev_preview_config(self):
with MockedVerb('run') as mocked_run:
self._call(['-c', test_util.vector_path('cli.ini')])
self.assertTrue(mocked_run.called)
class DetermineAccountTest(unittest.TestCase):
"""Tests for letsencrypt.cli._determine_account."""
+1
View File
@@ -0,0 +1 @@
agree-dev-preview = True
@@ -0,0 +1,52 @@
<VirtualHost *:443>
ServerAdmin webmaster@localhost
ServerAlias www.example.com
ServerName example.com
DocumentRoot /var/www/example.com/www/
SSLEngine on
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
<Directory /var/www/example.com/www>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
# This directive allows us to have apache2's default start page
# in /apache2-default/, but still have / go to the right place
</Directory>
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>
ErrorLog /var/log/apache2/error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access.log combined
ServerSignature On
Alias /apache_doc/ "/usr/share/doc/"
<Directory "/usr/share/doc/">
Options Indexes MultiViews FollowSymLinks
AllowOverride None
Order deny,allow
Deny from all
Allow from 127.0.0.0/255.0.0.0 ::1/128
</Directory>
</VirtualHost>
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
# A hackish script to see if the client is behaving as expected
# with each of the "passing" conf files.
# TODO presently this requires interaction and human judgement to
# assess, but it should be automated
export EA=/etc/apache2/
TESTDIR="`dirname $0`"
LEROOT="`realpath \"$TESTDIR/../../\"`"
cd $TESTDIR/passing
function CleanupExit() {
echo control c, exiting tests...
if [ "$f" != "" ] ; then
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
fi
exit 1
}
trap CleanupExit INT
for f in *.conf ; do
echo testing "$f"
sudo cp "$f" "$EA"/sites-available/
sudo ln -s "$EA/sites-available/$f" "$EA/sites-enabled/$f"
sudo "$LEROOT"/venv/bin/letsencrypt --apache certonly -t
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
done
@@ -0,0 +1,37 @@
<VirtualHost *:80>
ServerAdmin denver@ossguy.com
ServerName c-beta.ossguy.com
Alias /robots.txt /home/denver/www/c-beta.ossguy.com/static/robots.txt
Alias /favicon.ico /home/denver/www/c-beta.ossguy.com/static/favicon.ico
AliasMatch /(.*\.css) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.js) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.png) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.gif) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.jpg) /home/denver/www/c-beta.ossguy.com/static/$1
WSGIScriptAlias / /home/denver/www/c-beta.ossguy.com/django.wsgi
WSGIDaemonProcess c-beta-ossguy user=www-data group=www-data home=/var/www processes=5 threads=10 maximum-requests=1000 umask=0007 display-name=c-beta-ossguy
WSGIProcessGroup c-beta-ossguy
WSGIApplicationGroup %{GLOBAL}
DocumentRoot /home/denver/www/c-beta.ossguy.com/static
<Directory /home/denver/www/c-beta.ossguy.com/static>
Options -Indexes +FollowSymLinks -MultiViews
Require all granted
AllowOverride None
</Directory>
<Directory /home/denver/www/c-beta.ossguy.com/static/source>
Options +Indexes +FollowSymLinks -MultiViews
Require all granted
AllowOverride None
</Directory>
# Custom log file locations
LogLevel warn
ErrorLog /tmp/error.log
CustomLog /tmp/access.log combined
</VirtualHost>
@@ -3,3 +3,5 @@ Modules required to parse these conf files:
ssl
rewrite
macro
wsgi
deflate
@@ -0,0 +1,116 @@
#
# Apache/PHP/Drupal settings:
#
# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl|svn-base)$|^(code-style\.pl|Entries.*|Repository|Root|Tag|Template|all-wcprops|entries|format)$">
Order allow,deny
</FilesMatch>
# Don't show directory listings for URLs which map to a directory.
Options -Indexes
# Follow symbolic links in this directory.
Options +FollowSymLinks
# Make Drupal handle any 404 errors.
ErrorDocument 404 /index.php
# Force simple error message for requests for non-existent favicon.ico.
<Files favicon.ico>
# There is no end quote below, for compatibility with Apache 1.3.
ErrorDocument 404 "The requested file favicon.ico was not found.
</Files>
# Set the default handler.
DirectoryIndex index.php
# Override PHP settings. More in sites/default/settings.php
# but the following cannot be changed at runtime.
# PHP 4, Apache 1.
<IfModule mod_php4.c>
php_value magic_quotes_gpc 0
php_value register_globals 0
php_value session.auto_start 0
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_value mbstring.encoding_translation 0
</IfModule>
# PHP 4, Apache 2.
<IfModule sapi_apache2.c>
php_value magic_quotes_gpc 0
php_value register_globals 0
php_value session.auto_start 0
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_value mbstring.encoding_translation 0
</IfModule>
# PHP 5, Apache 1 and 2.
<IfModule mod_php5.c>
php_value magic_quotes_gpc 0
php_value register_globals 0
php_value session.auto_start 0
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_value mbstring.encoding_translation 0
</IfModule>
# Requires mod_expires to be enabled.
<IfModule mod_expires.c>
# Enable expirations.
ExpiresActive On
# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600
<FilesMatch \.php$>
# Do not allow PHP scripts to be cached unless they explicitly send cache
# headers themselves. Otherwise all scripts would have to overwrite the
# headers set by mod_expires if they want another caching behavior. This may
# fail if an error occurs early in the bootstrap process, and it may cause
# problems if a non-Drupal PHP file is installed in a subdirectory.
ExpiresActive Off
</FilesMatch>
</IfModule>
# Various rewrite rules.
<IfModule mod_rewrite.c>
RewriteEngine on
# If your site can be accessed both with and without the 'www.' prefix, you
# can use one of the following settings to redirect users to your preferred
# URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
#
# To redirect all users to access the site WITH the 'www.' prefix,
# (http://example.com/... will be redirected to http://www.example.com/...)
# adapt and uncomment the following:
# RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
# RewriteRule ^(.*)$ http://www.example.com/$1 [L,R=301]
#
# To redirect all users to access the site WITHOUT the 'www.' prefix,
# (http://www.example.com/... will be redirected to http://example.com/...)
# uncomment and adapt the following:
# RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
# RewriteRule ^(.*)$ http://example.com/$1 [L,R=301]
# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
# VirtualDocumentRoot and the rewrite rules are not working properly.
# For example if your site is at http://example.com/drupal uncomment and
# modify the following line:
# RewriteBase /drupal
#
# If your site is running in a VirtualDocumentRoot at http://example.com/,
# uncomment the following line:
# RewriteBase /
# Rewrite URLs of the form 'x' to the form 'index.php?q=x'.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
</IfModule>
# $Id$
@@ -0,0 +1,36 @@
<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
ServerName www.example.com
ServerAlias example.com
SetOutputFilter DEFLATE
# Do not attempt to compress the following extensions
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png|swf|flv|zip|gz|tar|mp3|mp4|m4v)$ no-gzip dont-vary
ServerAdmin webmaster@localhost
DocumentRoot /var/www/proof
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
@@ -0,0 +1,52 @@
<VirtualHost *:443>
ServerAdmin webmaster@localhost
ServerAlias www.example.com
ServerName example.com
DocumentRoot /var/www/example.com/www/
SSLEngine on
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
<Directory /var/www/example.com/www>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
# This directive allows us to have apache2's default start page
# in /apache2-default/, but still have / go to the right place
</Directory>
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>
ErrorLog /var/log/apache2/error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access.log combined
ServerSignature On
Alias /apache_doc/ "/usr/share/doc/"
<Directory "/usr/share/doc/">
Options Indexes MultiViews FollowSymLinks
AllowOverride None
Order deny,allow
Deny from all
Allow from 127.0.0.0/255.0.0.0 ::1/128
</Directory>
</VirtualHost>