Merge remote-tracking branch 'letsencrypt/master'

This commit is contained in:
TheNavigat 2016-02-12 13:38:13 +02:00
commit 5db2c75506
80 changed files with 1351 additions and 940 deletions

View file

@ -72,7 +72,7 @@ addons:
apt:
sources:
- augeas
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
- python-dev
- python-virtualenv
- gcc

View file

@ -5,23 +5,7 @@ Please note:
the change log will only get updated after first release - for now please use the
`commit log <https://github.com/letsencrypt/letsencrypt/commits/master>`_.
To see the changes in a given release, inspect the github milestone for the
release. For instance:
Release 0.1.0 (not released yet)
--------------------------------
New Features:
* ...
Fixes:
* ...
Other changes:
* ...
Release 0.0.0 (not released yet)
--------------------------------
Initial release.
https://github.com/letsencrypt/letsencrypt/issues?utf8=%E2%9C%93&q=milestone%3A0.3.0

View file

@ -22,8 +22,8 @@ WORKDIR /opt/letsencrypt
# directories in its path.
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
RUN /opt/letsencrypt/src/ubuntu.sh && \
COPY letsencrypt-auto-source/letsencrypt-auto /opt/letsencrypt/src/letsencrypt-auto-source/letsencrypt-auto
RUN /opt/letsencrypt/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \

View file

@ -22,8 +22,8 @@ WORKDIR /opt/letsencrypt
# TODO: Install non-default Python versions for tox.
# TODO: Install Apache/Nginx for plugin development.
COPY letsencrypt-auto-source/letsencrypt-auto /opt/letsencrypt/src/letsencrypt-auto
RUN /opt/letsencrypt/src/letsencrypt-auto --os-packages-only && \
COPY letsencrypt-auto-source/letsencrypt-auto /opt/letsencrypt/src/letsencrypt-auto-source/letsencrypt-auto
RUN /opt/letsencrypt/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \

2
Vagrantfile vendored
View file

@ -8,7 +8,7 @@ VAGRANTFILE_API_VERSION = "2"
$ubuntu_setup_script = <<SETUP_SCRIPT
cd /vagrant
./letsencrypt-auto-source/letsencrypt-auto --os-packages-only
./bootstrap/dev/venv.sh
./tools/venv.sh
SETUP_SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

View file

@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.4.0.dev0'
version = '0.5.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -1,6 +0,0 @@
This directory contains scripts that install necessary OS-specific
prerequisite dependencies (see docs/using.rst).
General dependencies:
- ca-certificates: communication with demo ACMO server at
https://www.letsencrypt-demo.org

View file

@ -1,26 +0,0 @@
#!/bin/sh
# Tested with:
# - ArchLinux (x86_64)
#
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
# only "virtualenv2" binary, not "virtualenv" necessary in
# ./bootstrap/dev/_common_venv.sh
deps="
python2
python-virtualenv
gcc
dialog
augeas
openssl
libffi
ca-certificates
pkg-config
"
missing=$(pacman -T $deps)
if [ "$missing" ]; then
pacman -S --needed $missing
fi

View file

@ -1,94 +0,0 @@
#!/bin/sh
# Current version tested with:
#
# - Ubuntu
# - 14.04 (x64)
# - 15.04 (x64)
# - Debian
# - 7.9 "wheezy" (x64)
# - sid (2015-10-21) (x64)
# Past versions tested with:
#
# - Debian 8.0 "jessie" (x64)
# - Raspbian 7.8 (armhf)
# Believed not to work:
#
# - Debian 6.0.10 "squeeze" (x64)
apt-get update
# virtualenv binary can be found in different packages depending on
# distro version (#346)
virtualenv=
if apt-cache show virtualenv > /dev/null 2>&1; then
virtualenv="virtualenv"
fi
if apt-cache show python-virtualenv > /dev/null 2>&1; then
virtualenv="$virtualenv python-virtualenv"
fi
augeas_pkg="libaugeas0 augeas-lenses"
AUGVERSION=`apt-cache show --no-all-versions libaugeas0 | grep ^Version: | cut -d" " -f2`
AddBackportRepo() {
# ARGS:
BACKPORT_NAME="$1"
BACKPORT_SOURCELINE="$2"
if ! grep -v -e ' *#' /etc/apt/sources.list | grep -q "$BACKPORT_NAME" ; then
# This can theoretically error if sources.list.d is empty, but in that case we don't care.
if ! grep -v -e ' *#' /etc/apt/sources.list.d/* 2>/dev/null | grep -q "$BACKPORT_NAME"; then
/bin/echo -n "Installing augeas from $BACKPORT_NAME in 3 seconds..."
sleep 1s
/bin/echo -ne "\e[0K\rInstalling augeas from $BACKPORT_NAME in 2 seconds..."
sleep 1s
/bin/echo -e "\e[0K\rInstalling augeas from $BACKPORT_NAME in 1 second ..."
sleep 1s
if echo $BACKPORT_NAME | grep -q wheezy ; then
/bin/echo '(Backports are only installed if explicitly requested via "apt-get install -t wheezy-backports")'
fi
echo $BACKPORT_SOURCELINE >> /etc/apt/sources.list.d/"$BACKPORT_NAME".list
apt-get update
fi
fi
apt-get install -y --no-install-recommends -t "$BACKPORT_NAME" $augeas_pkg
augeas_pkg=
}
if dpkg --compare-versions 1.0 gt "$AUGVERSION" ; then
if lsb_release -a | grep -q wheezy ; then
AddBackportRepo wheezy-backports "deb http://http.debian.net/debian wheezy-backports main"
elif lsb_release -a | grep -q precise ; then
# XXX add ARM case
AddBackportRepo precise-backports "deb http://archive.ubuntu.com/ubuntu precise-backports main restricted universe multiverse"
else
echo "No libaugeas0 version is available that's new enough to run the"
echo "Let's Encrypt apache plugin..."
fi
# XXX add a case for ubuntu PPAs
fi
apt-get install -y --no-install-recommends \
python \
python-dev \
$virtualenv \
gcc \
dialog \
$augeas_pkg \
libssl-dev \
libffi-dev \
ca-certificates \
if ! command -v virtualenv > /dev/null ; then
echo Failed to install a working \"virtualenv\" command, exiting
exit 1
fi

View file

@ -1,23 +0,0 @@
#!/bin/sh
PACKAGES="
dev-lang/python:2.7
dev-python/virtualenv
dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
case "$PACKAGE_MANAGER" in
(paludis)
cave resolve --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
pmerge --noreplace $PACKAGES
;;
(portage|*)
emerge --noreplace $PACKAGES
;;
esac

View file

@ -1,60 +0,0 @@
#!/bin/sh
# Tested with:
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: on DigitalOcean droplet)
# - CentOS 7 Minimal install in a Hyper-V VM
if type dnf 2>/dev/null
then
tool=dnf
elif type yum 2>/dev/null
then
tool=yum
else
echo "Neither yum nor dnf found. Aborting bootstrap!"
exit 1
fi
# Some distros and older versions of current distros use a "python27"
# instead of "python" naming convention. Try both conventions.
if ! $tool install -y \
python \
python-devel \
python-virtualenv \
python-tools \
python-pip
then
if ! $tool install -y \
python27 \
python27-devel \
python27-virtualenv \
python27-tools \
python27-pip
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1
fi
fi
if ! $tool install -y \
gcc \
dialog \
augeas-libs \
openssl-devel \
libffi-devel \
redhat-rpm-config \
ca-certificates
then
echo "Could not install additional dependencies. Aborting bootstrap!"
exit 1
fi
if $tool list installed "httpd" >/dev/null 2>&1; then
if ! $tool install -y mod_ssl
then
echo "Apache found, but mod_ssl could not be installed."
fi
fi

View file

@ -1,14 +0,0 @@
#!/bin/sh
# SLE12 don't have python-virtualenv
zypper -nq in -l \
python \
python-devel \
python-virtualenv \
gcc \
dialog \
augeas-lenses \
libopenssl-devel \
libffi-devel \
ca-certificates \

View file

@ -1 +0,0 @@
_arch_common.sh

View file

@ -1 +0,0 @@
_rpm_common.sh

View file

@ -1 +0,0 @@
_deb_common.sh

View file

@ -1 +0,0 @@
This directory contains developer setup.

View file

@ -1 +0,0 @@
_rpm_common.sh

View file

@ -1,7 +0,0 @@
#!/bin/sh -xe
pkg install -Ay \
python \
py27-virtualenv \
augeas \
libffi \

View file

@ -1 +0,0 @@
_gentoo_common.sh

View file

@ -1,46 +0,0 @@
#!/bin/sh -e
#
# Install OS dependencies. In the glorious future, letsencrypt-auto will
# source this...
if test "`id -u`" -ne "0" ; then
SUDO=sudo
else
SUDO=
fi
BOOTSTRAP=`dirname $0`
if [ ! -f $BOOTSTRAP/debian.sh ] ; then
echo "Cannot find the letsencrypt bootstrap scripts in $BOOTSTRAP"
exit 1
fi
if [ -f /etc/debian_version ] ; then
echo "Bootstrapping dependencies for Debian-based OSes..."
$SUDO $BOOTSTRAP/_deb_common.sh
elif [ -f /etc/arch-release ] ; then
echo "Bootstrapping dependencies for Archlinux..."
$SUDO $BOOTSTRAP/archlinux.sh
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO $BOOTSTRAP/_rpm_common.sh
elif [ -f /etc/gentoo-release ] ; then
echo "Bootstrapping dependencies for Gentoo-based OSes..."
$SUDO $BOOTSTRAP/_gentoo_common.sh
elif uname | grep -iq FreeBSD ; then
echo "Bootstrapping dependencies for FreeBSD..."
$SUDO $BOOTSTRAP/freebsd.sh
elif `grep -qs openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE.."
$SUDO $BOOTSTRAP/suse.sh
elif uname | grep -iq Darwin ; then
echo "Bootstrapping dependencies for Mac OS X..."
echo "WARNING: Mac support is very experimental at present..."
$BOOTSTRAP/mac.sh
else
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
echo
echo "You will need to bootstrap, configure virtualenv, and run a pip install manually"
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
echo "for more info"
exit 1
fi

View file

@ -1,18 +0,0 @@
#!/bin/sh -e
if ! hash brew 2>/dev/null; then
echo "Homebrew Not Installed\nDownloading..."
ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
fi
brew install augeas
brew install dialog
if ! hash pip 2>/dev/null; then
echo "pip Not Installed\nInstalling python from Homebrew..."
brew install python
fi
if ! hash virtualenv 2>/dev/null; then
echo "virtualenv Not Installed\nInstalling with pip"
pip install virtualenv
fi

View file

@ -1 +0,0 @@
_arch_common.sh

View file

@ -1 +0,0 @@
_suse_common.sh

View file

@ -1 +0,0 @@
_deb_common.sh

View file

@ -1,33 +0,0 @@
#!/bin/sh -e
#
# Installs and updates letencrypt virtualenv
#
# USAGE: source ./dev/venv.sh
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
# virtualenv call is not idempotent: it overwrites pip upgraded in
# later steps, causing "ImportError: cannot import name unpack_url"
if [ ! -d $VENV_PATH ]
then
virtualenv --no-site-packages --python ${LE_PYTHON:-python2} $VENV_PATH
fi
. $VENV_PATH/bin/activate
pip install -U setuptools
pip install -U pip
pip install -U letsencrypt letsencrypt-apache # letsencrypt-nginx
echo
echo "Congratulations, Let's Encrypt has been successfully installed/updated!"
echo
printf "%s" "Your prompt should now be prepended with ($VENV_NAME). Next "
printf "time, if the prompt is different, 'source' this script again "
printf "before running 'letsencrypt'."
echo
echo
echo "You can now run 'letsencrypt --help'."

View file

@ -23,7 +23,7 @@ once:
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
./letsencrypt-auto-source/letsencrypt-auto --os-packages-only
./bootstrap/dev/venv.sh
./tools/venv.sh
Then in each shell where you're working on the client, do:
@ -157,7 +157,7 @@ Plugin-architecture
Let's Encrypt has a plugin architecture to facilitate support for
different webservers, other TLS servers, and operating systems.
The interfaces available for plugins to implement are defined in
`interfaces.py`_.
`interfaces.py`_ and `plugins/common.py`_.
The most common kind of plugin is a "Configurator", which is likely to
implement the `~letsencrypt.interfaces.IAuthenticator` and
@ -168,6 +168,7 @@ There are also `~letsencrypt.interfaces.IDisplay` plugins,
which implement bindings to alternative UI libraries.
.. _interfaces.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/interfaces.py
.. _plugins/common.py: https://github.com/letsencrypt/letsencrypt/blob/master/letsencrypt/plugins/common.py#L34
Authenticators
@ -300,7 +301,7 @@ Steps:
1. Write your code!
2. Make sure your environment is set up properly and that you're in your
virtualenv. You can do this by running ``./bootstrap/dev/venv.sh``.
virtualenv. You can do this by running ``./tools/venv.sh``.
(this is a **very important** step)
3. Run ``./pep8.travis.sh`` to do a cursory check of your code style.
Fix any errors.

View file

@ -16,27 +16,12 @@ letsencrypt-auto
----------------
``letsencrypt-auto`` is a wrapper which installs some dependencies
from your OS standard package repositories (e.g using `apt-get` or
from your OS standard package repositories (e.g. using `apt-get` or
`yum`), and for other dependencies it sets up a virtualized Python
environment with packages downloaded from PyPI [#venv]_. It also
provides automated updates.
Firstly, please `install Git`_ and run the following commands:
.. code-block:: shell
git clone https://github.com/letsencrypt/letsencrypt
cd letsencrypt
.. _`install Git`: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git
.. note:: On RedHat/CentOS 6 you will need to enable the EPEL_
repository before install.
.. _EPEL: http://fedoraproject.org/wiki/EPEL
To install and run the client you just need to type:
To install and run the client, just type...
.. code-block:: shell
@ -64,7 +49,7 @@ or for full help, type:
``letsencrypt-auto`` is the recommended method of running the Let's Encrypt
client beta releases on systems that don't have a packaged version. Debian,
Arch linux, FreeBSD, and OpenBSD now have native packages, so on those
Arch Linux, Gentoo, FreeBSD, and OpenBSD now have native packages, so on those
systems you can just install ``letsencrypt`` (and perhaps
``letsencrypt-apache``). If you'd like to run the latest copy from Git, or
run your own locally modified copy of the client, follow the instructions in
@ -86,7 +71,9 @@ Plugin Auth Inst Notes
=========== ==== ==== ===============================================================
apache_ Y Y Automates obtaining and installing a cert with Apache 2.4 on
Debian-based distributions with ``libaugeas0`` 1.0+.
standalone_ Y N Uses a "standalone" webserver to obtain a cert.
standalone_ Y N Uses a "standalone" webserver to obtain a cert. This is useful
on systems with no webserver, or when direct integration with
the local webserver is not supported or not desired.
webroot_ Y N Obtains a cert by writing to the webroot directory of an
already running webserver.
manual_ Y N Helps you obtain a cert by giving you instructions to perform
@ -106,36 +93,27 @@ This automates both obtaining *and* installing certs on an Apache
webserver. To specify this plugin on the command line, simply include
``--apache``.
Standalone
----------
To obtain a cert using a "standalone" webserver, you can use the
standalone plugin by including ``certonly`` and ``--standalone``
on the command line. This plugin needs to bind to port 80 or 443 in
order to perform domain validation, so you may need to stop your
existing webserver. To control which port the plugin uses, include
one of the options shown below on the command line.
* ``--standalone-supported-challenges http-01`` to use port 80
* ``--standalone-supported-challenges tls-sni-01`` to use port 443
Webroot
-------
If you're running a webserver that you don't want to stop to use
standalone, you can use the webroot plugin to obtain a cert by
including ``certonly`` and ``--webroot`` on the command line. In
addition, you'll need to specify ``--webroot-path`` or ``-w`` with the root
directory of the files served by your webserver. For example,
``--webroot-path /var/www/html`` or
``--webroot-path /usr/share/nginx/html`` are two common webroot paths.
If you're running a local webserver for which you have the ability
to modify the content being served, and you'd prefer not to stop the
webserver during the certificate issuance process, you can use the webroot
plugin to obtain a cert by including ``certonly`` and ``--webroot`` on
the command line. In addition, you'll need to specify ``--webroot-path``
or ``-w`` with the top-level directory ("web root") containing the files
served by your webserver. For example, ``--webroot-path /var/www/html``
or ``--webroot-path /usr/share/nginx/html`` are two common webroot paths.
If you're getting a certificate for many domains at once, each domain will use
the most recent ``--webroot-path``. So for instance:
If you're getting a certificate for many domains at once, the plugin
needs to know where each domain's files are served from, which could
potentially be a separate directory for each domain. When requested a
certificate for multiple domains, each domain will use the most recently
specified ``--webroot-path``. So, for instance,
``letsencrypt certonly --webroot -w /var/www/example/ -d www.example.com -d example.com -w /var/www/eg -d eg.is -d www.eg.is``
Would obtain a single certificate for all of those names, using the
would obtain a single certificate for all of those names, using the
``/var/www/example`` webroot directory for the first two, and
``/var/www/eg`` for the second two.
@ -150,8 +128,28 @@ made to your web server would look like:
66.133.109.36 - - [05/Jan/2016:20:11:24 -0500] "GET /.well-known/acme-challenge/HGr8U1IeTW4kY_Z6UIyaakzOkyQgPr_7ArlLgtZE8SX HTTP/1.1" 200 87 "-" "Mozilla/5.0 (compatible; Let's Encrypt validation server; +https://www.letsencrypt.org)"
Note that to use the webroot plugin, your server must be configured to serve
files from hidden directories.
files from hidden directories. If ``/.well-known`` is treated specially by
your webserver configuration, you might need to modify the configuration
to ensure that files inside ``/.well-known/ache-challenge`` are served by
the webserver.
Standalone
----------
To obtain a cert using a "standalone" webserver, you can use the
standalone plugin by including ``certonly`` and ``--standalone``
on the command line. This plugin needs to bind to port 80 or 443 in
order to perform domain validation, so you may need to stop your
existing webserver. To control which port the plugin uses, include
one of the options shown below on the command line.
* ``--standalone-supported-challenges http-01`` to use port 80
* ``--standalone-supported-challenges tls-sni-01`` to use port 443
The standalone plugin does not rely on any other server software running
on the machine where you obtain the certificate. It must still be possible
for that machine to accept inbound connections from the Internet on the
specified port using each requested domain name.
Manual
------
@ -161,7 +159,8 @@ other than your target webserver or perform the steps for domain
validation yourself, you can use the manual plugin. While hidden from
the UI, you can use the plugin to obtain a cert by specifying
``certonly`` and ``--manual`` on the command line. This requires you
to copy and paste commands into another terminal session.
to copy and paste commands into another terminal session, which may
be on a different computer.
Nginx
-----
@ -172,7 +171,7 @@ is still experimental, however, and is not installed with
letsencrypt-auto_. If installed, you can select this plugin on the
command line by including ``--nginx``.
Third party plugins
Third-party plugins
-------------------
These plugins are listed at
@ -182,24 +181,65 @@ interested, you can also :ref:`write your own plugin <dev-plugin>`.
Renewal
=======
.. note:: Let's Encrypt CA issues short lived certificates (90
.. note:: Let's Encrypt CA issues short-lived certificates (90
days). Make sure you renew the certificates at least once in 3
months.
In order to renew certificates simply call the ``letsencrypt`` (or
letsencrypt-auto_) again, and use the same values when prompted. You
can automate it slightly by passing necessary flags on the CLI (see
`--help all`), or even further using the :ref:`config-file`. The
``--renew-by-default`` flag may be helpful for automating renewal. If
you're sure that UI doesn't prompt for any details you can add the
command to ``crontab`` (make it less than every 90 days to avoid
problems, say every month).
The ``letsencrypt`` client now supports a ``renew`` action to check
all installed certificates for impending expiry and attempt to renew
them. The simplest form is simply
``letsencrypt renew``
This will attempt to renew any previously-obtained certificates that
expire in less than 30 days. The same plugin and options that were used
at the time the certificate was originally issued will be used for the
renewal attempt, unless you specify other plugins or options.
If you're sure that this command executes successfully without human
intervention, you can add the command to ``crontab`` (since certificates
are only renewed when they're determined to be near expiry, the command
can run on a regular basis, like every week or every day); note that
the current version provides detailed output describing either renewal
success or failure.
The ``--force-renew`` flag may be helpful for automating renewal;
it causes the expiration time of the certificate(s) to be ignored when
considering renewal, and attempts to renew each and every installed
certificate regardless of its age. (This form is not appropriate to run
daily because each certificate will be renewed every day, which will
quickly run into the certificate authority rate limit.)
Note that options provided to ``letsencrypt renew`` will apply to
*every* certificate for which renewal is attempted; for example,
``letsencrypt renew --rsa-key-size 4096`` would try to replace every
near-expiry certificate with an equivalent certificate using a 4096-bit
RSA public key. If a certificate is successfully renewed using
specified options, those options will be saved and used for future
renewals of that certificate.
An alternative form that provides for more fine-grained control over the
renewal process (while renewing specified certificates one at a time),
is ``letsencrypt certonly`` with the complete set of subject domains of
a specific certificate specified via `-d` flags, like
``letsencrypt certonly -d example.com -d www.example.com``
(All of the domains covered by the certificate must be specified in
this case in order to renew and replace the old certificate rather
than obtaining a new one; don't forget any `www.` domains! Specifying
a subset of the domains creates a new, separate certificate containing
only those domains, rather than replacing the original certificate.)
The ``certonly`` form attempts to renew one individual certificate.
Please note that the CA will send notification emails to the address
you provide if you do not renew certificates that are about to expire.
Let's Encrypt is working hard on automating the renewal process. Until
the tool is ready, we are sorry for the inconvenience!
Let's Encrypt is working hard on improving the renewal process, and we
apologize for any inconveniences you encounter in integrating these
commands into your individual environment.
.. _where-certs:
@ -389,6 +429,52 @@ If you don't want to use the Apache plugin, you can omit the
Packages for Debian Jessie are coming in the next few weeks.
**Gentoo**
The official Let's Encrypt client is available in Gentoo Portage. If you
want to use the Apache plugin, it has to be installed separately:
.. code-block:: shell
emerge -av app-crypt/letsencrypt
emerge -av app-crypt/letsencrypt-apache
Currently, only the Apache plugin is included in Portage. However, if you
want the nginx plugin, you can use Layman to add the mrueg overlay which
does include the nginx plugin package:
.. code-block:: shell
emerge -av app-portage/layman
layman -S
layman -a mrueg
emerge -av app-crypt/letsencrypt-nginx
When using the Apache plugin, you will run into a "cannot find a cert or key
directive" error if you're sporting the default Gentoo ``httpd.conf``.
You can fix this by commenting out two lines in ``/etc/apache2/httpd.conf``
as follows:
Change
.. code-block:: shell
<IfDefine SSL>
LoadModule ssl_module modules/mod_ssl.so
</IfDefine>
to
.. code-block:: shell
#<IfDefine SSL>
LoadModule ssl_module modules/mod_ssl.so
#</IfDefine>
For the time being, this is the only way for the Apache plugin to recognise
the appropriate directives when installing the certificate.
Note: this change is not required for the other plugins.
**Other Operating Systems**
OS packaging is an ongoing effort. If you'd like to package

View file

@ -2,5 +2,6 @@ include LICENSE.txt
include README.rst
recursive-include docs *
recursive-include letsencrypt_apache/tests/testdata *
include letsencrypt_apache/centos-options-ssl-apache.conf
include letsencrypt_apache/options-ssl-apache.conf
recursive-include letsencrypt_apache/augeas_lens *.aug

View file

@ -106,7 +106,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
add("handle-sites", default=constants.os_constant("handle_sites"),
help="Let installer handle enabling sites for you." +
"(Only Ubuntu/Debian currently)")
le_util.add_deprecated_argument(add, "init-script", 1)
le_util.add_deprecated_argument(add, argument_name="ctl", nargs=1)
le_util.add_deprecated_argument(
add, argument_name="init-script", nargs=1)
def __init__(self, *args, **kwargs):
"""Initialize an Apache Configurator.

View file

@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.4.0.dev0'
version = '0.5.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -1,202 +0,0 @@
#!/bin/sh -e
#
# A script to run the latest release version of the Let's Encrypt in a
# virtual environment
#
# Installs and updates the letencrypt virtualenv, and runs letsencrypt
# using that virtual environment. This allows the client to function decently
# without requiring specific versions of its dependencies from the operating
# system.
# Note: you can set XDG_DATA_HOME or VENV_PATH before running this script,
# if you want to change where the virtual environment will be installed
XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
VENV_BIN=${VENV_PATH}/bin
# The path to the letsencrypt-auto script. Everything that uses these might
# at some point be inlined...
LEA_PATH=`dirname "$0"`
BOOTSTRAP=${LEA_PATH}/bootstrap
# This script takes the same arguments as the main letsencrypt program, but it
# additionally responds to --verbose (more output) and --debug (allow support
# for experimental platforms)
for arg in "$@" ; do
# This first clause is redundant with the third, but hedging on portability
if [ "$arg" = "-v" ] || [ "$arg" = "--verbose" ] || echo "$arg" | grep -E -- "-v+$" ; then
VERBOSE=1
elif [ "$arg" = "--debug" ] ; then
DEBUG=1
fi
done
# letsencrypt-auto needs root access to bootstrap OS dependencies, and
# letsencrypt itself needs root access for almost all modes of operation
# The "normal" case is that sudo is used for the steps that need root, but
# this script *can* be run as root (not recommended), or fall back to using
# `su`
if test "`id -u`" -ne "0" ; then
if command -v sudo 1>/dev/null 2>&1; then
SUDO=sudo
else
echo \"sudo\" is not available, will use \"su\" for installation steps...
# Because the parameters in `su -c` has to be a string,
# we need properly escape it
su_sudo() {
args=""
# This `while` loop iterates over all parameters given to this function.
# For each parameter, all `'` will be replace by `'"'"'`, and the escaped string
# will be wrapped in a pair of `'`, then appended to `$args` string
# For example, `echo "It's only 1\$\!"` will be escaped to:
# 'echo' 'It'"'"'s only 1$!'
# │ │└┼┘│
# │ │ │ └── `'s only 1$!'` the literal string
# │ │ └── `\"'\"` is a single quote (as a string)
# │ └── `'It'`, to be concatenated with the strings following it
# └── `echo` wrapped in a pair of `'`, it's totally fine for the shell command itself
while [ $# -ne 0 ]; do
args="$args'$(printf "%s" "$1" | sed -e "s/'/'\"'\"'/g")' "
shift
done
su root -c "$args"
}
SUDO=su_sudo
fi
else
SUDO=
fi
ExperimentalBootstrap() {
# Arguments: Platform name, boostrap script name, SUDO command (iff needed)
if [ "$DEBUG" = 1 ] ; then
if [ "$2" != "" ] ; then
echo "Bootstrapping dependencies for $1..."
if [ "$3" != "" ] ; then
"$3" "$BOOTSTRAP/$2"
else
"$BOOTSTRAP/$2"
fi
fi
else
echo "WARNING: $1 support is very experimental at present..."
echo "if you would like to work on improving it, please ensure you have backups"
echo "and then run this script again with the --debug flag!"
exit 1
fi
}
DeterminePythonVersion() {
if command -v python2.7 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2.7}
elif command -v python27 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python27}
elif command -v python2 > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python2}
elif command -v python > /dev/null ; then
export LE_PYTHON=${LE_PYTHON:-python}
else
echo "Cannot find any Pythons... please install one!"
exit 1
fi
PYVER=`$LE_PYTHON --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
if [ $PYVER -lt 26 ] ; then
echo "You have an ancient version of Python entombed in your operating system..."
echo "This isn't going to work; you'll need at least version 2.6."
exit 1
fi
}
# virtualenv call is not idempotent: it overwrites pip upgraded in
# later steps, causing "ImportError: cannot import name unpack_url"
if [ ! -d $VENV_PATH ]
then
if [ ! -f $BOOTSTRAP/debian.sh ] ; then
echo "Cannot find the letsencrypt bootstrap scripts in $BOOTSTRAP"
exit 1
fi
if [ -f /etc/debian_version ] ; then
echo "Bootstrapping dependencies for Debian-based OSes..."
$SUDO $BOOTSTRAP/_deb_common.sh
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO $BOOTSTRAP/_rpm_common.sh
elif `grep -q openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE-based OSes..."
$SUDO $BOOTSTRAP/_suse_common.sh
elif [ -f /etc/arch-release ] ; then
if [ "$DEBUG" = 1 ] ; then
echo "Bootstrapping dependencies for Archlinux..."
$SUDO $BOOTSTRAP/archlinux.sh
else
echo "Please use pacman to install letsencrypt packages:"
echo "# pacman -S letsencrypt letsencrypt-apache"
echo
echo "If you would like to use the virtualenv way, please run the script again with the"
echo "--debug flag."
exit 1
fi
elif [ -f /etc/manjaro-release ] ; then
ExperimentalBootstrap "Manjaro Linux" manjaro.sh "$SUDO"
elif [ -f /etc/gentoo-release ] ; then
ExperimentalBootstrap "Gentoo" _gentoo_common.sh "$SUDO"
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
elif uname | grep -iq Darwin ; then
ExperimentalBootstrap "Mac OS X" mac.sh # homebrew doesn't normally run as root
elif grep -iq "Amazon Linux" /etc/issue ; then
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh "$SUDO"
else
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
echo
echo "You will need to bootstrap, configure virtualenv, and run a pip install manually"
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
echo "for more info"
fi
DeterminePythonVersion
echo "Creating virtual environment..."
if [ "$VERBOSE" = 1 ] ; then
virtualenv --no-site-packages --python $LE_PYTHON $VENV_PATH
else
virtualenv --no-site-packages --python $LE_PYTHON $VENV_PATH > /dev/null
fi
else
DeterminePythonVersion
fi
printf "Updating letsencrypt and virtual environment dependencies..."
if [ "$VERBOSE" = 1 ] ; then
echo
$VENV_BIN/pip install -U setuptools
$VENV_BIN/pip install -U pip
$VENV_BIN/pip install -U letsencrypt letsencrypt-apache
# nginx is buggy / disabled for now, but upgrade it if the user has
# installed it manually
if $VENV_BIN/pip freeze | grep -q letsencrypt-nginx ; then
$VENV_BIN/pip install -U letsencrypt letsencrypt-nginx
fi
else
$VENV_BIN/pip install -U setuptools > /dev/null
printf .
$VENV_BIN/pip install -U pip > /dev/null
printf .
# nginx is buggy / disabled for now...
$VENV_BIN/pip install -U letsencrypt > /dev/null
printf .
$VENV_BIN/pip install -U letsencrypt-apache > /dev/null
if $VENV_BIN/pip freeze | grep -q letsencrypt-nginx ; then
printf .
$VENV_BIN/pip install -U letsencrypt-nginx > /dev/null
fi
echo
fi
# Explain what's about to happen, for the benefit of those getting sudo
# password prompts...
echo "Requesting root privileges to run with virtualenv:" $SUDO $VENV_BIN/letsencrypt "$@"
$SUDO $VENV_BIN/letsencrypt "$@"

1
letsencrypt-auto Symbolic link
View file

@ -0,0 +1 @@
letsencrypt-auto-source/letsencrypt-auto

View file

@ -19,7 +19,7 @@ XDG_DATA_HOME=${XDG_DATA_HOME:-~/.local/share}
VENV_NAME="letsencrypt"
VENV_PATH=${VENV_PATH:-"$XDG_DATA_HOME/$VENV_NAME"}
VENV_BIN=${VENV_PATH}/bin
LE_AUTO_VERSION="0.4.0.dev0"
LE_AUTO_VERSION="0.5.0.dev0"
# This script takes the same arguments as the main letsencrypt program, but it
# additionally responds to --verbose (more output) and --debug (allow support
@ -290,7 +290,7 @@ BootstrapArchCommon() {
#
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
# only "virtualenv2" binary, not "virtualenv" necessary in
# ./bootstrap/dev/_common_venv.sh
# ./tools/_venv_common.sh
deps="
python2
@ -372,7 +372,7 @@ Bootstrap() {
elif [ -f /etc/redhat-release ]; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
BootstrapRpmCommon
elif [ -f /etc/os-release] && `grep -q openSUSE /etc/os-release` ; then
elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE-based OSes..."
BootstrapSuseCommon
elif [ -f /etc/arch-release ]; then
@ -638,17 +638,17 @@ zope.event==4.1.0
# sha256: sJyMHUezUxxADgGVaX8UFKYyId5u9HhZik8UYPfZo5I
zope.interface==4.1.3
# sha256: QMIkIvGF3mcJhGLAKRX7n5EVIPjOrfLtklN6ePjbJes
# sha256: fNFWiij6VxfG5o7u3oNbtrYKQ4q9vhzOLATfxNlozvQ
acme==0.3.0
# sha256: ilvjjTWOS86xchl0WBZ0YOAw_0rmqdnjNsxb1hq2RD8
# sha256: T37KMj0TnsuvHIzCCmoww2fpfpOBTj7cd4NAqucXcpw
acme==0.4.0
# sha256: qdnzpoRf_44QXKoktNoAKs2RBAxUta2Sr6GS0t_tAKo
# sha256: ELWJaHNvBZIqVPJYkla8yXLtXIuamqAf6f_VAFv16Uk
letsencrypt==0.3.0
# sha256: 33BQiANlNLGqGpirTfdCEElTF9YbpaKiYpTbK4zeGD8
# sha256: lwsV1OdEzzlMeb08C_PRxaCXZ2vOk_1AI2755rZHmPM
letsencrypt==0.4.0
# sha256: EypLpEw3-Tr8unw4aSFsHXgRiU8ZYLrJKOJohP2tC9M
# sha256: HYvP13GzA-DDJYwlfOoaraJO0zuYO48TCSAyTUAGCqA
letsencrypt-apache==0.3.0
# sha256: D3YDaVFjLsMSEfjI5B5D5tn5FeWUtNHYXCObw3ih2tg
# sha256: VTgvsePYGRmI4IOSAnxoYFHd8KciD73bxIuIHtbVFd8
letsencrypt-apache==0.4.0
# sha256: uDndLZwRfHAUMMFJlWkYpCOphjtIsJyQ4wpgE-fS9E8
# sha256: j4MIDaoknQNsvM-4rlzG_wB7iNbZN1ITca-r57Gbrbw
@ -1632,6 +1632,7 @@ UNLIKELY_EOF
echo "$PEEP_OUT"
exit 1
fi
echo "Installation succeeded."
fi
echo "Requesting root privileges to run letsencrypt..."
echo " " $SUDO "$VENV_BIN/letsencrypt" "$@"

View file

@ -128,7 +128,7 @@ Bootstrap() {
elif [ -f /etc/redhat-release ]; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
BootstrapRpmCommon
elif [ -f /etc/os-release] && `grep -q openSUSE /etc/os-release` ; then
elif [ -f /etc/os-release ] && `grep -q openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE-based OSes..."
BootstrapSuseCommon
elif [ -f /etc/arch-release ]; then
@ -209,6 +209,7 @@ UNLIKELY_EOF
echo "$PEEP_OUT"
exit 1
fi
echo "Installation succeeded."
fi
echo "Requesting root privileges to run letsencrypt..."
echo " " $SUDO "$VENV_BIN/letsencrypt" "$@"

View file

@ -4,7 +4,7 @@ BootstrapArchCommon() {
#
# "python-virtualenv" is Python3, but "python2-virtualenv" provides
# only "virtualenv2" binary, not "virtualenv" necessary in
# ./bootstrap/dev/_common_venv.sh
# ./tools/_venv_common.sh
deps="
python2

View file

@ -11,13 +11,13 @@ BootstrapGentooCommon() {
case "$PACKAGE_MANAGER" in
(paludis)
"$SUDO" cave resolve --keep-targets if-possible $PACKAGES -x
"$SUDO" cave resolve --preserve-world --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
"$SUDO" pmerge --noreplace $PACKAGES
"$SUDO" pmerge --noreplace --oneshot $PACKAGES
;;
(portage|*)
"$SUDO" emerge --noreplace $PACKAGES
"$SUDO" emerge --noreplace --oneshot $PACKAGES
;;
esac
}

View file

@ -201,17 +201,17 @@ zope.event==4.1.0
# sha256: sJyMHUezUxxADgGVaX8UFKYyId5u9HhZik8UYPfZo5I
zope.interface==4.1.3
# sha256: QMIkIvGF3mcJhGLAKRX7n5EVIPjOrfLtklN6ePjbJes
# sha256: fNFWiij6VxfG5o7u3oNbtrYKQ4q9vhzOLATfxNlozvQ
acme==0.3.0
# sha256: ilvjjTWOS86xchl0WBZ0YOAw_0rmqdnjNsxb1hq2RD8
# sha256: T37KMj0TnsuvHIzCCmoww2fpfpOBTj7cd4NAqucXcpw
acme==0.4.0
# sha256: qdnzpoRf_44QXKoktNoAKs2RBAxUta2Sr6GS0t_tAKo
# sha256: ELWJaHNvBZIqVPJYkla8yXLtXIuamqAf6f_VAFv16Uk
letsencrypt==0.3.0
# sha256: 33BQiANlNLGqGpirTfdCEElTF9YbpaKiYpTbK4zeGD8
# sha256: lwsV1OdEzzlMeb08C_PRxaCXZ2vOk_1AI2755rZHmPM
letsencrypt==0.4.0
# sha256: EypLpEw3-Tr8unw4aSFsHXgRiU8ZYLrJKOJohP2tC9M
# sha256: HYvP13GzA-DDJYwlfOoaraJO0zuYO48TCSAyTUAGCqA
letsencrypt-apache==0.3.0
# sha256: D3YDaVFjLsMSEfjI5B5D5tn5FeWUtNHYXCObw3ih2tg
# sha256: VTgvsePYGRmI4IOSAnxoYFHd8KciD73bxIuIHtbVFd8
letsencrypt-apache==0.4.0
# sha256: uDndLZwRfHAUMMFJlWkYpCOphjtIsJyQ4wpgE-fS9E8
# sha256: j4MIDaoknQNsvM-4rlzG_wB7iNbZN1ITca-r57Gbrbw

View file

@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.4.0.dev0'
version = '0.5.0.dev0'
install_requires = [
'letsencrypt=={0}'.format(version),

View file

@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.4.0.dev0'
version = '0.5.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [

View file

@ -1,4 +1,4 @@
"""Let's Encrypt client."""
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
__version__ = '0.4.0.dev0'
__version__ = '0.5.0.dev0'

View file

@ -57,7 +57,7 @@ class AuthHandler(object):
def get_authorizations(self, domains, best_effort=False):
"""Retrieve all authorizations for challenges.
:param set domains: Domains for authorization
:param list domains: Domains for authorization
:param bool best_effort: Whether or not all authorizations are
required (this is useful in renewal)
@ -480,6 +480,9 @@ def is_preferred(offered_challb, satisfied,
return True
_ACME_PREFIX = "urn:acme:error:"
_ERROR_HELP_COMMON = (
"To fix these errors, please make sure that your domain name was entered "
"correctly and the DNS A record(s) for that domain contain(s) the "
@ -490,7 +493,9 @@ _ERROR_HELP = {
"connection":
_ERROR_HELP_COMMON + " Additionally, please check that your computer "
"has a publicly routable IP address and that no firewalls are preventing "
"the server from communicating with the client.",
"the server from communicating with the client. If you're using the "
"webroot plugin, you should also verify that you are serving files "
"from the webroot path you provided.",
"dnssec":
_ERROR_HELP_COMMON + " Additionally, if you have DNSSEC enabled for "
"your domain, please ensure that the signature is valid.",
@ -540,11 +545,13 @@ def _generate_failed_chall_msg(failed_achalls):
"""
typ = failed_achalls[0].error.typ
if typ.startswith(_ACME_PREFIX):
typ = typ[len(_ACME_PREFIX):]
msg = ["The following errors were reported by the server:"]
for achall in failed_achalls:
msg.append("\n\nDomain: %s\nType: %s\nDetail: %s" % (
achall.domain, achall.error.typ, achall.error.detail))
achall.domain, typ, achall.error.detail))
if typ in _ERROR_HELP:
msg.append("\n\n")

View file

@ -6,7 +6,9 @@ from __future__ import print_function
# (TODO: split this file into main.py and cli.py)
import argparse
import atexit
import copy
import functools
import glob
import json
import logging
import logging.handlers
@ -44,6 +46,18 @@ from letsencrypt.plugins import disco as plugins_disco
logger = logging.getLogger(__name__)
# Global, to save us from a lot of argument passing within the scope of this module
_parser = None
# These are the items which get pulled out of a renewal configuration
# file's renewalparams and actually used in the client configuration
# during the renewal process. We have to record their types here because
# the renewal configuration process loses this information.
STR_CONFIG_ITEMS = ["config_dir", "logs_dir", "work_dir", "user_agent",
"server", "account", "authenticator", "installer",
"standalone_supported_challenges"]
INT_CONFIG_ITEMS = ["rsa_key_size", "tls_sni_01_port", "http01_port"]
# For help strings, figure out how the user ran us.
# When invoked from letsencrypt-auto, sys.argv[0] is something like:
# "/home/user/.local/share/letsencrypt/bin/letsencrypt"
@ -69,6 +83,7 @@ the cert. Major SUBCOMMANDS are:
(default) run Obtain & install a cert in your current webserver
certonly Obtain cert, but do not install it (aka "auth")
install Install a previously obtained cert in a server
renew Renew previously obtained certs that are near expiry
revoke Revoke a previously obtained certificate
rollback Rollback server configuration changes made during install
config_changes Show changes made to server config during installation
@ -208,15 +223,12 @@ def _find_duplicative_certs(config, domains):
# Verify the directory is there
le_util.make_or_verify_dir(configs_dir, mode=0o755, uid=os.geteuid())
for renewal_file in os.listdir(configs_dir):
if not renewal_file.endswith(".conf"):
continue
for renewal_file in _renewal_conf_files(cli_config):
try:
full_path = os.path.join(configs_dir, renewal_file)
candidate_lineage = storage.RenewableCert(full_path, cli_config)
candidate_lineage = storage.RenewableCert(renewal_file, cli_config)
except (errors.CertStorageError, IOError):
logger.warning("Renewal configuration file %s is broken. "
"Skipping.", full_path)
logger.warning("Renewal conf file %s is broken. Skipping.", renewal_file)
logger.debug("Traceback was:\n%s", traceback.format_exc())
continue
# TODO: Handle these differently depending on whether they are
# expired or still valid?
@ -235,7 +247,10 @@ def _find_duplicative_certs(config, domains):
def _treat_as_renewal(config, domains):
"""Determine whether there are duplicated names and how to handle them.
"""Determine whether there are duplicated names and how to handle
them (renew, reinstall, newcert, or raising an error to stop
the client run if the user chooses to cancel the operation when
prompted).
:returns: Two-element tuple containing desired new-certificate behavior as
a string token ("reinstall", "renew", or "newcert"), plus either
@ -262,8 +277,24 @@ def _treat_as_renewal(config, domains):
elif subset_names_cert is not None:
return _handle_subset_cert_request(config, domains, subset_names_cert)
def _should_renew(config, lineage):
"Return true if any of the circumstances for automatic renewal apply."
if config.renew_by_default:
logger.info("Auto-renewal forced with --force-renewal...")
return True
if lineage.should_autorenew(interactive=True):
logger.info("Cert is due for renewal, auto-renewing...")
return True
if config.dry_run:
logger.info("Cert not due for renewal, but simulating renewal for dry run")
return True
logger.info("Cert not yet due for renewal")
return False
def _handle_identical_cert_request(config, cert):
"""Figure out what to do if a cert has the same names as a perviously obtained one
"""Figure out what to do if a cert has the same names as a previously obtained one
:param storage.RenewableCert cert:
@ -271,17 +302,12 @@ def _handle_identical_cert_request(config, cert):
: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...")
if _should_renew(config, cert):
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."
@ -311,6 +337,7 @@ def _handle_identical_cert_request(config, 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
@ -379,23 +406,28 @@ def _report_new_cert(cert_path, fullchain_path):
reporter_util.add_message(msg, reporter_util.MEDIUM_PRIORITY)
def _suggest_donation_if_appropriate(config):
def _suggest_donation_if_appropriate(config, action):
"""Potentially suggest a donation to support Let's Encrypt."""
if not config.staging: # --dry-run implies --staging
reporter_util = zope.component.getUtility(interfaces.IReporter)
msg = ("If you like Let's Encrypt, please consider supporting our work by:\n\n"
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
"Donating to EFF: https://eff.org/donate-le\n\n")
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
def _report_successful_dry_run():
if config.staging or config.verb == "renew":
# --dry-run implies --staging
return
if action not in ["renew", "newcert"]:
return
reporter_util = zope.component.getUtility(interfaces.IReporter)
reporter_util.add_message("The dry run was successful.",
reporter_util.HIGH_PRIORITY, on_crash=False)
msg = ("If you like Let's Encrypt, please consider supporting our work by:\n\n"
"Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate\n"
"Donating to EFF: https://eff.org/donate-le\n\n")
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
def _auth_from_domains(le_client, config, domains):
def _report_successful_dry_run(config):
reporter_util = zope.component.getUtility(interfaces.IReporter)
if config.verb != "renew":
reporter_util.add_message("The dry run was successful.",
reporter_util.HIGH_PRIORITY, on_crash=False)
def _auth_from_domains(le_client, config, domains, lineage=None):
"""Authenticate and enroll certificate."""
# 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
@ -404,10 +436,14 @@ def _auth_from_domains(le_client, config, domains):
# (which results in treating the request as a renewal), or newcert
# (which results in treating the request as a new certificate request).
action, lineage = _treat_as_renewal(config, domains)
if config.dry_run and action == "reinstall":
logger.info(
"Cert not due for renewal, but simulating renewal for dry run")
# If lineage is specified, use that one instead of looking around for
# a matching one.
if lineage is None:
# This will find a relevant matching lineage that exists
action, lineage = _treat_as_renewal(config, domains)
else:
# Renewal, where we already know the specific lineage we're
# interested in
action = "renew"
if action == "reinstall":
@ -440,7 +476,7 @@ def _auth_from_domains(le_client, config, domains):
if lineage is False:
raise errors.Error("Certificate could not be obtained")
if not config.dry_run:
if not config.dry_run and not config.verb == "renew":
_report_new_cert(lineage.cert, lineage.fullchain)
return lineage, action
@ -457,7 +493,7 @@ def _avoid_invalidating_lineage(config, lineage, original_server):
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()
now_valid = "fake" not in repr(latest_cert.get_issuer()).lower()
if _is_staging(config.server):
if not _is_staging(original_server) or now_valid:
@ -516,6 +552,7 @@ def set_configurator(previously, now):
raise errors.PluginSelectionError(msg.format(repr(previously), repr(now)))
return now
def cli_plugin_requests(config):
"""
Figure out which plugins the user requested with CLI and config options
@ -544,6 +581,7 @@ def cli_plugin_requests(config):
noninstaller_plugins = ["webroot", "manual", "standalone"]
def choose_configurator_plugins(config, plugins, verb):
"""
Figure out which configurator we're going to use, modifies
@ -566,7 +604,7 @@ def choose_configurator_plugins(config, plugins, verb):
'{1} {2} certonly --{0}{1}{1}'
'(Alternatively, add a --installer flag. See https://eff.org/letsencrypt-plugins'
'{1} and "--help plugins" for more information.)'.format(
req_auth, os.linesep, cli_command))
req_auth, os.linesep, cli_command))
raise errors.MissingCommandlineFlag(msg)
else:
@ -635,30 +673,28 @@ def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
else:
display_ops.success_renewal(domains, action)
_suggest_donation_if_appropriate(config)
_suggest_donation_if_appropriate(config, action)
def obtain_cert(config, plugins):
def obtain_cert(config, plugins, lineage=None):
"""Implements "certonly": authenticate & obtain cert, but do not install it."""
if config.domains and config.csr is not None:
# TODO: --csr could have a priority, when --domains is
# supplied, check if CSR matches given domains?
return "--domains and --csr are mutually exclusive"
# pylint: disable=too-many-locals
try:
# installers are used in auth mode to determine domain names
installer, authenticator = choose_configurator_plugins(config, plugins, "certonly")
except errors.PluginSelectionError as e:
return e.message
logger.info("Could not choose appropriate plugin: %s", e)
raise
# TODO: Handle errors from _init_le_client?
le_client = _init_le_client(config, authenticator, installer)
action = "newcert"
# This is a special case; cert and chain are simply saved
if config.csr is not None:
certr, chain = le_client.obtain_certificate_from_csr(le_util.CSR(
file=config.csr[0], data=config.csr[1], form="der"))
assert lineage is None, "Did not expect a CSR with a RenewableCert"
csr, typ = config.actual_csr
certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr, typ)
if config.dry_run:
logger.info(
"Dry run: skipping saving certificate to %s", config.cert_path)
@ -668,11 +704,23 @@ def obtain_cert(config, plugins):
_report_new_cert(cert_path, cert_fullchain)
else:
domains = _find_domains(config, installer)
_auth_from_domains(le_client, config, domains)
_, action = _auth_from_domains(le_client, config, domains, lineage)
if config.dry_run:
_report_successful_dry_run()
_suggest_donation_if_appropriate(config)
_report_successful_dry_run(config)
elif config.verb == "renew":
if installer is None:
# Tell the user that the server was not restarted.
print("new certificate deployed without reload, fullchain is",
lineage.fullchain)
else:
# In case of a renewal, reload server to pick up new certificate.
# In principle we could have a configuration option to inhibit this
# from happening.
installer.restart()
print("new certificate deployed with reload of",
config.installer, "server; fullchain is", lineage.fullchain)
_suggest_donation_if_appropriate(config, action)
def install(config, plugins):
@ -695,6 +743,295 @@ def install(config, plugins):
le_client.enhance_config(domains, config)
def _set_by_cli(var):
"""
Return True if a particular config variable has been set by the user
(CLI or config file) including if the user explicitly set it to the
default. Returns False if the variable was assigned a default value.
"""
detector = _set_by_cli.detector
if detector is None:
# Setup on first run: `detector` is a weird version of config in which
# the default value of every attribute is wrangled to be boolean-false
plugins = plugins_disco.PluginsRegistry.find_all()
# reconstructed_args == sys.argv[1:], or whatever was passed to main()
reconstructed_args = _parser.args + [_parser.verb]
detector = _set_by_cli.detector = prepare_and_parse_args(
plugins, reconstructed_args, detect_defaults=True)
# propagate plugin requests: eg --standalone modifies config.authenticator
auth, inst = cli_plugin_requests(detector)
detector.authenticator = auth if auth else ""
detector.installer = inst if inst else ""
logger.debug("Default Detector is %r", detector)
try:
# Is detector.var something that isn't false?
change_detected = getattr(detector, var)
except AttributeError:
logger.warning("Missing default analysis for %r", var)
return False
if change_detected:
return True
# Special case: we actually want account to be set to "" if the server
# the account was on has changed
elif var == "account" and (detector.server or detector.dry_run or detector.staging):
return True
# Special case: vars like --no-redirect that get set True -> False
# default to None; False means they were set
elif var in detector.store_false_vars and change_detected is not None:
return True
else:
return False
# static housekeeping var
_set_by_cli.detector = None
def _restore_required_config_elements(config, renewalparams):
"""Sets non-plugin specific values in config from renewalparams
:param configuration.NamespaceConfig config: configuration for the
current lineage
:param configobj.Section renewalparams: parameters from the renewal
configuration file that defines this lineage
"""
# string-valued items to add if they're present
for config_item in STR_CONFIG_ITEMS:
if config_item in renewalparams and not _set_by_cli(config_item):
value = renewalparams[config_item]
# Unfortunately, we've lost type information from ConfigObj,
# so we don't know if the original was NoneType or str!
if value == "None":
value = None
setattr(config.namespace, config_item, value)
# int-valued items to add if they're present
for config_item in INT_CONFIG_ITEMS:
if config_item in renewalparams and not _set_by_cli(config_item):
try:
value = int(renewalparams[config_item])
setattr(config.namespace, config_item, value)
except ValueError:
raise errors.Error(
"Expected a numeric value for {0}".format(config_item))
def _restore_plugin_configs(config, renewalparams):
"""Sets plugin specific values in config from renewalparams
:param configuration.NamespaceConfig config: configuration for the
current lineage
:param configobj.Section renewalparams: Parameters from the renewal
configuration file that defines this lineage
"""
# Now use parser to get plugin-prefixed items with correct types
# XXX: the current approach of extracting only prefixed items
# related to the actually-used installer and authenticator
# works as long as plugins don't need to read plugin-specific
# variables set by someone else (e.g., assuming Apache
# configurator doesn't need to read webroot_ variables).
# Note: if a parameter that used to be defined in the parser is no
# longer defined, stored copies of that parameter will be
# deserialized as strings by this logic even if they were
# originally meant to be some other type.
if renewalparams["authenticator"] == "webroot":
_restore_webroot_config(config, renewalparams)
plugin_prefixes = []
else:
plugin_prefixes = [renewalparams["authenticator"]]
if renewalparams.get("installer", None) is not None:
plugin_prefixes.append(renewalparams["installer"])
for plugin_prefix in set(plugin_prefixes):
for config_item, config_value in renewalparams.iteritems():
if config_item.startswith(plugin_prefix + "_") and not _set_by_cli(config_item):
# Values None, True, and False need to be treated specially,
# As they don't get parsed correctly based on type
if config_value in ("None", "True", "False"):
# bool("False") == True
# pylint: disable=eval-used
setattr(config.namespace, config_item, eval(config_value))
continue
for action in _parser.parser._actions: # pylint: disable=protected-access
if action.type is not None and action.dest == config_item:
setattr(config.namespace, config_item,
action.type(config_value))
break
else:
setattr(config.namespace, config_item, str(config_value))
def _restore_webroot_config(config, renewalparams):
"""
webroot_map is, uniquely, a dict, and the general-purpose configuration
restoring logic is not able to correctly parse it from the serialized
form.
"""
if "webroot_map" in renewalparams:
# if the user does anything that would create a new webroot map on the
# CLI, don't use the old one
if not (_set_by_cli("webroot_map") or _set_by_cli("webroot_path")):
setattr(config.namespace, "webroot_map", renewalparams["webroot_map"])
elif "webroot_path" in renewalparams:
logger.info("Ancient renewal conf file without webroot-map, restoring webroot-path")
setattr(config.namespace, "webroot_path", renewalparams["webroot_path"])
def _reconstitute(config, full_path):
"""Try to instantiate a RenewableCert, updating config with relevant items.
This is specifically for use in renewal and enforces several checks
and policies to ensure that we can try to proceed with the renwal
request. The config argument is modified by including relevant options
read from the renewal configuration file.
:param configuration.NamespaceConfig config: configuration for the
current lineage
:param str full_path: Absolute path to the configuration file that
defines this lineage
:returns: the RenewableCert object or None if a fatal error occurred
:rtype: `storage.RenewableCert` or NoneType
"""
try:
renewal_candidate = storage.RenewableCert(
full_path, configuration.RenewerConfiguration(config))
except (errors.CertStorageError, IOError):
logger.warning("Renewal configuration file %s is broken. Skipping.", full_path)
logger.debug("Traceback was:\n%s", traceback.format_exc())
return None
if "renewalparams" not in renewal_candidate.configuration:
logger.warning("Renewal configuration file %s lacks "
"renewalparams. Skipping.", full_path)
return None
renewalparams = renewal_candidate.configuration["renewalparams"]
if "authenticator" not in renewalparams:
logger.warning("Renewal configuration file %s does not specify "
"an authenticator. Skipping.", full_path)
return None
# Now restore specific values along with their data types, if
# those elements are present.
try:
_restore_required_config_elements(config, renewalparams)
_restore_plugin_configs(config, renewalparams)
except (ValueError, errors.Error) as error:
logger.warning(
"An error occured while parsing %s. The error was %s. "
"Skipping the file.", full_path, error.message)
logger.debug("Traceback was:\n%s", traceback.format_exc())
return None
try:
for d in renewal_candidate.names():
_process_domain(config, d)
except errors.ConfigurationError as error:
logger.warning("Renewal configuration file %s references a cert "
"that contains an invalid domain name. The problem "
"was: %s. Skipping.", full_path, error)
return None
return renewal_candidate
def _renewal_conf_files(config):
"""Return /path/to/*.conf in the renewal conf directory"""
return glob.glob(os.path.join(config.renewal_configs_dir, "*.conf"))
def _renew_describe_results(config, renew_successes, renew_failures,
renew_skipped, parse_failures):
status = lambda x, msg: " " + "\n ".join(i + " (" + msg +")" for i in x)
if config.dry_run:
print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry")
print("** (The test certificates below have not been saved.)")
print()
if renew_skipped:
print("The following certs are not due for renewal yet:")
print(status(renew_skipped, "skipped"))
if not renew_successes and not renew_failures:
print("No renewals were attempted.")
elif renew_successes and not renew_failures:
print("Congratulations, all renewals succeeded. The following certs "
"have been renewed:")
print(status(renew_successes, "success"))
elif renew_failures and not renew_successes:
print("All renewal attempts failed. The following certs could not be "
"renewed:")
print(status(renew_failures, "failure"))
elif renew_failures and renew_successes:
print("The following certs were successfully renewed:")
print(status(renew_successes, "success"))
print("\nThe following certs could not be renewed:")
print(status(renew_failures, "failure"))
if parse_failures:
print("\nAdditionally, the following renewal configuration files "
"were invalid: ")
print(status(parse_failures, "parsefail"))
if config.dry_run:
print("** DRY RUN: simulating 'letsencrypt renew' close to cert expiry")
print("** (The test certificates above have not been saved.)")
def renew(config, unused_plugins):
"""Renew previously-obtained certificates."""
if config.domains != []:
raise errors.Error("Currently, the renew verb is only capable of "
"renewing all installed certificates that are due "
"to be renewed; individual domains cannot be "
"specified with this action. If you would like to "
"renew specific certificates, use the certonly "
"command. The renew verb may provide other options "
"for selecting certificates to renew in the future.")
if config.csr is not None:
raise errors.Error("Currently, the renew verb cannot be used when "
"specifying a CSR file. Please try the certonly "
"command instead.")
renewer_config = configuration.RenewerConfiguration(config)
renew_successes = []
renew_failures = []
renew_skipped = []
parse_failures = []
for renewal_file in _renewal_conf_files(renewer_config):
print("Processing " + renewal_file)
lineage_config = copy.deepcopy(config)
# Note that this modifies config (to add back the configuration
# elements from within the renewal configuration file).
try:
renewal_candidate = _reconstitute(lineage_config, renewal_file)
except Exception as e: # pylint: disable=broad-except
logger.warning("Renewal configuration file %s produced an "
"unexpected error: %s. Skipping.", renewal_file, e)
logger.debug("Traceback was:\n%s", traceback.format_exc())
parse_failures.append(renewal_file)
continue
try:
if renewal_candidate is None:
parse_failures.append(renewal_file)
else:
# XXX: ensure that each call here replaces the previous one
zope.component.provideUtility(lineage_config)
if _should_renew(lineage_config, renewal_candidate):
plugins = plugins_disco.PluginsRegistry.find_all()
obtain_cert(lineage_config, plugins, renewal_candidate)
renew_successes.append(renewal_candidate.fullchain)
else:
renew_skipped.append(renewal_candidate.fullchain)
except Exception as e: # pylint: disable=broad-except
# obtain_cert (presumably) encountered an unanticipated problem.
logger.warning("Attempting to renew cert from %s produced an "
"unexpected error: %s. Skipping.", renewal_file, e)
logger.debug("Traceback was:\n%s", traceback.format_exc())
renew_failures.append(renewal_candidate.fullchain)
# Describe all the results
_renew_describe_results(config, renew_successes, renew_failures,
renew_skipped, parse_failures)
def revoke(config, unused_plugins): # TODO: coop with renewal config
"""Revoke a previously obtained certificate."""
# For user-agent construction
@ -813,14 +1150,14 @@ class HelpfulArgumentParser(object):
# Maps verbs/subcommands to the functions that implement them
VERBS = {"auth": obtain_cert, "certonly": obtain_cert,
"config_changes": config_changes, "everything": run,
"install": install, "plugins": plugins_cmd,
"install": install, "plugins": plugins_cmd, "renew": renew,
"revoke": revoke, "rollback": rollback, "run": run}
# List of topics for which additional help can be provided
HELP_TOPICS = ["all", "security",
"paths", "automation", "testing"] + VERBS.keys()
def __init__(self, args, plugins):
def __init__(self, args, plugins, detect_defaults=False):
plugin_names = [name for name, _p in plugins.iteritems()]
self.help_topics = self.HELP_TOPICS + plugin_names + [None]
usage, short_usage = usage_strings(plugins)
@ -834,6 +1171,14 @@ class HelpfulArgumentParser(object):
self.parser._add_config_file_help = False # pylint: disable=protected-access
self.silent_parser = SilentParser(self.parser)
# This setting attempts to force all default values to things that are
# pythonically false; it is used to detect when values have been
# explicitly set by the user, including when they are set to their
# normal default value
self.detect_defaults = detect_defaults
if detect_defaults:
self.store_false_vars = {} # vars that use "store_false"
self.args = args
self.determine_verb()
help1 = self.prescan_for_flag("-h", self.help_topics)
@ -845,7 +1190,7 @@ class HelpfulArgumentParser(object):
print(usage)
sys.exit(0)
self.visible_topics = self.determine_help_topics(self.help_arg)
self.groups = {} # elements are added by .add_group()
self.groups = {} # elements are added by .add_group()
def parse_args(self):
"""Parses command line arguments and returns the result.
@ -867,23 +1212,76 @@ class HelpfulArgumentParser(object):
parsed_args.domains.append(domain)
if parsed_args.staging or parsed_args.dry_run:
if (parsed_args.server not in
(flag_default("server"), constants.STAGING_URI)):
if parsed_args.server not in (flag_default("server"), constants.STAGING_URI):
conflicts = ["--staging"] if parsed_args.staging else []
conflicts += ["--dry-run"] if parsed_args.dry_run else []
raise errors.Error("--server value conflicts with {0}".format(
" and ".join(conflicts)))
if not self.detect_defaults:
raise errors.Error("--server value conflicts with {0}".format(
" and ".join(conflicts)))
parsed_args.server = constants.STAGING_URI
if parsed_args.dry_run:
if self.verb != "certonly":
if self.verb not in ["certonly", "renew"]:
raise errors.Error("--dry-run currently only works with the "
"'certonly' subcommand")
"'certonly' or 'renew' subcommands (%r)" % self.verb)
parsed_args.break_my_certs = parsed_args.staging = True
if glob.glob(os.path.join(parsed_args.config_dir, constants.ACCOUNTS_DIR, "*")):
# The user has a prod account, but might not have a staging
# one; we don't want to start trying to perform interactive registration
parsed_args.agree_tos = True
parsed_args.register_unsafely_without_email = True
if parsed_args.csr:
self.handle_csr(parsed_args)
if self.detect_defaults: # plumbing
parsed_args.store_false_vars = self.store_false_vars
return parsed_args
def handle_csr(self, parsed_args):
"""
Process a --csr flag. This needs to happen early enough that the
webroot plugin can know about the calls to _process_domain
"""
try:
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="der")
typ = OpenSSL.crypto.FILETYPE_ASN1
domains = crypto_util.get_sans_from_csr(csr.data, OpenSSL.crypto.FILETYPE_ASN1)
except OpenSSL.crypto.Error:
try:
e1 = traceback.format_exc()
typ = OpenSSL.crypto.FILETYPE_PEM
csr = le_util.CSR(file=parsed_args.csr[0], data=parsed_args.csr[1], form="pem")
domains = crypto_util.get_sans_from_csr(csr.data, typ)
except OpenSSL.crypto.Error:
logger.debug("DER CSR parse error %s", e1)
logger.debug("PEM CSR parse error %s", traceback.format_exc())
raise errors.Error("Failed to parse CSR file: {0}".format(parsed_args.csr[0]))
for d in domains:
_process_domain(parsed_args, d)
for d in domains:
sanitised = le_util.enforce_domain_sanity(d)
if d.lower() != sanitised:
raise errors.ConfigurationError(
"CSR domain {0} needs to be sanitised to {1}.".format(d, sanitised))
if not domains:
# TODO: add CN to domains instead:
raise errors.Error(
"Unfortunately, your CSR %s needs to have a SubjectAltName for every domain"
% parsed_args.csr[0])
parsed_args.actual_csr = (csr, typ)
csr_domains, config_domains = set(domains), set(parsed_args.domains)
if csr_domains != config_domains:
raise errors.ConfigurationError(
"Inconsistent domain requests:\nFrom the CSR: {0}\nFrom command line/config: {1}"
.format(", ".join(csr_domains), ", ".join(config_domains)))
def determine_verb(self):
"""Determines the verb/subcommand provided by the user.
@ -932,10 +1330,16 @@ class HelpfulArgumentParser(object):
def add(self, topic, *args, **kwargs):
"""Add a new command line argument.
@topic is required, to indicate which part of the help will document
it, but can be None for `always documented'.
:param str: help topic this should be listed under, can be None for
"always documented"
:param list *args: the names of this argument flag
:param dict **kwargs: various argparse settings for this argument
"""
if self.detect_defaults:
kwargs = self.modify_arg_for_default_detection(self, *args, **kwargs)
if self.visible_topics[topic]:
if topic in self.groups:
group = self.groups[topic]
@ -946,6 +1350,39 @@ class HelpfulArgumentParser(object):
kwargs["help"] = argparse.SUPPRESS
self.parser.add_argument(*args, **kwargs)
def modify_arg_for_default_detection(self, *args, **kwargs):
"""
Adding an arg, but ensure that it has a default that evaluates to false,
so that _set_by_cli can tell if it was set. Only called if detect_defaults==True.
:param list *args: the names of this argument flag
:param dict **kwargs: various argparse settings for this argument
:returns: a modified versions of kwargs
"""
# argument either doesn't have a default, or the default doesn't
# isn't Pythonically false
if kwargs.get("default", True):
arg_type = kwargs.get("type", None)
if arg_type == int or kwargs.get("action", "") == "count":
kwargs["default"] = 0
elif arg_type == read_file or "-c" in args:
kwargs["default"] = ""
kwargs["type"] = str
else:
kwargs["default"] = ""
# This doesn't matter at present (none of the store_false args
# are renewal-relevant), but implement it for future sanity:
# detect the setting of args whose presence causes True -> False
if kwargs.get("action", "") == "store_false":
kwargs["default"] = None
for var in args:
self.store_false_vars[var] = True
return kwargs
def add_deprecated_argument(self, argument_name, num_args):
"""Adds a deprecated argument with the name argument_name.
@ -1013,7 +1450,7 @@ class HelpfulArgumentParser(object):
return dict([(t, t == chosen_topic) for t in self.help_topics])
def prepare_and_parse_args(plugins, args):
def prepare_and_parse_args(plugins, args, detect_defaults=False):
"""Returns parsed command line arguments.
:param .PluginsRegistry plugins: available plugins
@ -1023,7 +1460,7 @@ def prepare_and_parse_args(plugins, args):
:rtype: argparse.Namespace
"""
helpful = HelpfulArgumentParser(args, plugins)
helpful = HelpfulArgumentParser(args, plugins, detect_defaults)
# --help is automatically provided by argparse
helpful.add(
@ -1040,6 +1477,11 @@ def prepare_and_parse_args(plugins, args):
help="Run without ever asking for user input. This may require "
"additional command line flags; the client will try to explain "
"which ones are required if it finds one missing")
helpful.add(
None, "--dry-run", action="store_true", dest="dry_run",
help="Perform a test run of the client, obtaining test (invalid) certs"
" but not saving them to disk. This can currently only be used"
" with the 'certonly' subcommand.")
helpful.add(
None, "--register-unsafely-without-email", action="store_true",
help="Specifying this flag enables registering an account with no "
@ -1078,10 +1520,12 @@ def prepare_and_parse_args(plugins, args):
version="%(prog)s {0}".format(letsencrypt.__version__),
help="show program's version number and exit")
helpful.add(
"automation", "--renew-by-default", action="store_true",
help="Select renewal by default when domains are a superset of a "
"previously attained cert (often --keep-until-expiring is "
"more appropriate). Implies --expand.")
"automation", "--force-renewal", "--renew-by-default",
action="store_true", dest="renew_by_default", help="If a certificate "
"already exists for the requested domains, renew it now, "
"regardless of whether it is near expiry. (Often "
"--keep-until-expiring is more appropriate). Also implies "
"--expand.")
helpful.add(
"automation", "--agree-tos", dest="tos", action="store_true",
help="Agree to the Let's Encrypt Subscriber Agreement")
@ -1160,6 +1604,16 @@ def prepare_and_parse_args(plugins, args):
help="Require that all configuration files are owned by the current "
"user; only needed if your config is somewhere unsafe like /tmp/")
helpful.add_group(
"renew", description="The 'renew' subcommand will attempt to renew all"
" certificates (or more precisely, certificate lineages) you have"
" previously obtained if they are close to expiry, and print a"
" summary of the results. By default, 'renew' will reuse the options"
" used to create obtain or most recently successfully renew each"
" certificate lineage. You can try it with `--dry-run` first. For"
" more fine-grained control, you can renew individual lineages with"
" the `certonly` subcommand.")
helpful.add_deprecated_argument("--agree-dev-preview", 0)
_create_subparsers(helpful)
@ -1168,6 +1622,9 @@ def prepare_and_parse_args(plugins, args):
# parser (--help should display plugin-specific options last)
_plugins_parsing(helpful, plugins)
if not detect_defaults:
global _parser # pylint: disable=global-statement
_parser = helpful
return helpful.parse_args()
@ -1254,10 +1711,6 @@ def _paths_parser(helpful):
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)
add("testing", "--dry-run", action="store_true", dest="dry_run",
help="Perform a test run of the client, obtaining test (invalid) certs"
" but not saving them to disk. This can currently only be used"
" with the 'certonly' subcommand.")
def _plugins_parsing(helpful, plugins):
@ -1296,21 +1749,24 @@ def _plugins_parsing(helpful, plugins):
# they are parsed in conjunction with --domains, they live here for
# legibility. helpful.add_plugin_ags must be called first to add the
# "webroot" topic
helpful.add("webroot", "-w", "--webroot-path", action=WebrootPathProcessor,
helpful.add("webroot", "-w", "--webroot-path", default=[], action=WebrootPathProcessor,
help="public_html / webroot path. This can be specified multiple times to "
"handle different domains; each domain will have the webroot path that"
" preceded it. For instance: `-w /var/www/example -d example.com -d "
"www.example.com -w /var/www/thing -d thing.net -d m.thing.net`")
# --webroot-map still has some awkward properties, so it is undocumented
helpful.add("webroot", "--webroot-map", default={}, action=WebrootMapProcessor,
help="JSON dictionary mapping domains to webroot paths; this implies -d "
"for each entry. You may need to escape this from your shell. "
"""Eg: --webroot-map '{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """
"This option is merged with, but takes precedence over, -w / -d entries."
" At present, if you put webroot-map in a config file, it needs to be "
' on a single line, like: webroot-map = {"example.com":"/var/www"}.')
help="JSON dictionary mapping domains to webroot paths; this "
"implies -d for each entry. You may need to escape this "
"from your shell. E.g.: --webroot-map "
"""'{"eg1.is,m.eg1.is":"/www/eg1/", "eg2.is":"/www/eg2"}' """
"This option is merged with, but takes precedence over, "
"-w / -d entries. At present, if you put webroot-map in "
"a config file, it needs to be on a single line, like: "
'webroot-map = {"example.com":"/var/www"}.')
class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
def __init__(self, *args, **kwargs):
self.domain_before_webroot = False
argparse.Action.__init__(self, *args, **kwargs)
@ -1320,8 +1776,7 @@ class WebrootPathProcessor(argparse.Action): # pylint: disable=missing-docstring
Keep a record of --webroot-path / -w flags during processing, so that
we know which apply to which -d flags
"""
if args.webroot_path is None: # first -w flag encountered
args.webroot_path = []
if not args.webroot_path: # first -w flag encountered
# if any --domain flags preceded the first --webroot-path flag,
# apply that webroot path to those; subsequent entries in
# args.webroot_map are filled in by cli.DomainFlagProcessor
@ -1359,14 +1814,14 @@ def _process_domain(args_or_config, domain_arg, webroot_path=None):
args_or_config.webroot_map.setdefault(domain, webroot_path[-1])
class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring
class WebrootMapProcessor(argparse.Action): # pylint: disable=missing-docstring
def __call__(self, parser, args, webroot_map_arg, option_string=None):
webroot_map = json.loads(webroot_map_arg)
for domains, webroot_path in webroot_map.iteritems():
_process_domain(args, domains, [webroot_path])
class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring
class DomainFlagProcessor(argparse.Action): # pylint: disable=missing-docstring
def __call__(self, parser, args, domain_arg, option_string=None):
"""Just wrap _process_domain in argparseese."""
_process_domain(args, domain_arg)
@ -1389,7 +1844,7 @@ def setup_log_file_handler(config, logfile, fmt):
def _cli_log_handler(config, level, fmt):
if config.text_mode:
if config.text_mode or config.noninteractive_mode or config.verb == "renew":
handler = colored_logging.StreamHandler()
handler.setFormatter(logging.Formatter(fmt))
else:
@ -1457,8 +1912,8 @@ def _handle_exception(exc_type, exc_value, trace, config):
# acme.messages.Error: urn:acme:error:malformed :: The request message was
# malformed :: Error creating new registration :: Validation of contact
# mailto:none@longrandomstring.biz failed: Server failure at resolver
if ("urn:acme" in err and ":: " in err
and config.verbose_count <= flag_default("verbose_count")):
if (("urn:acme" in err and ":: " in err and
config.verbose_count <= flag_default("verbose_count"))):
# prune ACME error code, we have a human description
_code, _sep, err = err.partition(":: ")
msg = "An unexpected error occurred:\n" + err + "Please see the "
@ -1475,9 +1930,9 @@ def _handle_exception(exc_type, exc_value, trace, config):
def main(cli_args=sys.argv[1:]):
"""Command line argument parsing and main script execution."""
sys.excepthook = functools.partial(_handle_exception, config=None)
plugins = plugins_disco.PluginsRegistry.find_all()
# note: arg parser internally handles --help (and exits afterwards)
plugins = plugins_disco.PluginsRegistry.find_all()
args = prepare_and_parse_args(plugins, cli_args)
config = configuration.NamespaceConfig(args)
zope.component.provideUtility(config)
@ -1506,6 +1961,9 @@ def main(cli_args=sys.argv[1:]):
displayer = display_util.NoninteractiveDisplay(sys.stdout)
elif config.text_mode:
displayer = display_util.FileDisplay(sys.stdout)
elif config.verb == "renew":
config.noninteractive_mode = True
displayer = display_util.NoninteractiveDisplay(sys.stdout)
else:
displayer = display_util.NcursesDisplay()
zope.component.provideUtility(displayer)
@ -1515,17 +1973,6 @@ def main(cli_args=sys.argv[1:]):
zope.component.provideUtility(report)
atexit.register(report.atexit_print_messages)
if not os.geteuid() == 0:
logger.warning(
"Root (sudo) is required to run most of letsencrypt functionality.")
# check must be done after arg parsing as --help should work
# w/o root; on the other hand, e.g. "letsencrypt run
# --authenticator dns" or "letsencrypt plugins" does not
# require root as well
#return (
# "{0}Root is required to run letsencrypt. Please use sudo.{0}"
# .format(os.linesep))
return config.func(config, plugins)
if __name__ == "__main__":

View file

@ -195,7 +195,8 @@ class Client(object):
else:
self.auth_handler = None
def _obtain_certificate(self, domains, csr):
def obtain_certificate_from_csr(self, domains, csr,
typ=OpenSSL.crypto.FILETYPE_ASN1):
"""Obtain certificate.
Internal function with precondition that `domains` are
@ -223,26 +224,11 @@ class Client(object):
authzr = self.auth_handler.get_authorizations(domains)
certr = self.acme.request_issuance(
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, csr.data)),
jose.ComparableX509(
OpenSSL.crypto.load_certificate_request(typ, csr.data)),
authzr)
return certr, self.acme.fetch_chain(certr)
def obtain_certificate_from_csr(self, csr):
"""Obtain certficiate from CSR.
:param .le_util.CSR csr: DER-encoded Certificate Signing
Request.
:returns: `.CertificateResource` and certificate chain (as
returned by `.fetch_chain`).
:rtype: tuple
"""
return self._obtain_certificate(
# TODO: add CN to domains?
crypto_util.get_sans_from_csr(
csr.data, OpenSSL.crypto.FILETYPE_ASN1), csr)
def obtain_certificate(self, domains):
"""Obtains a certificate from the ACME server.
@ -263,7 +249,7 @@ class Client(object):
self.config.rsa_key_size, self.config.key_dir)
csr = crypto_util.init_save_csr(key, domains, self.config.csr_dir)
return self._obtain_certificate(domains, csr) + (key, csr)
return self.obtain_certificate_from_csr(domains, csr) + (key, csr)
def obtain_and_enroll_certificate(self, domains):
"""Obtain and enroll certificate.

View file

@ -1,4 +1,5 @@
"""Let's Encrypt user-supplied configuration."""
import copy
import os
import urlparse
@ -78,6 +79,12 @@ class NamespaceConfig(object):
return os.path.join(
self.namespace.work_dir, constants.TEMP_CHECKPOINT_DIR)
def __deepcopy__(self, _memo):
# Work around https://bugs.python.org/issue1515 for py26 tests :( :(
# https://travis-ci.org/letsencrypt/letsencrypt/jobs/106900743#L3276
new_ns = copy.deepcopy(self.namespace)
return type(self)(new_ns)
class RenewerConfiguration(object):
"""Configuration wrapper for renewer."""

View file

@ -37,7 +37,9 @@ STAGING_URI = "https://acme-staging.api.letsencrypt.org/directory"
RENEWER_DEFAULTS = dict(
renewer_enabled="yes",
renew_before_expiry="30 days",
deploy_before_expiry="20 days",
# This value should ensure that there is never a deployment delay by
# default.
deploy_before_expiry="99 years",
)
"""Defaults for renewer script."""

View file

@ -446,7 +446,7 @@ class NoninteractiveDisplay(object):
line=os.linesep, frame=side_frame, msg=message))
def menu(self, message, choices, ok_label=None, cancel_label=None,
default=None, cli_flag=None):
help_label=None, default=None, cli_flag=None):
# pylint: disable=unused-argument,too-many-arguments
"""Avoid displaying a menu.

View file

@ -308,7 +308,7 @@ def enforce_domain_sanity(domain):
# Unicode
try:
domain = domain.encode('ascii')
domain = domain.encode('ascii').lower()
except UnicodeDecodeError:
raise errors.ConfigurationError(
"Internationalized domain names are not presently supported: {0}"

View file

@ -41,6 +41,36 @@ class Plugin(object):
self.config = config
self.name = name
@jose_util.abstractclassmethod
def add_parser_arguments(cls, add):
"""Add plugin arguments to the CLI argument parser.
:param callable add: Function that proxies calls to
`argparse.ArgumentParser.add_argument` prepending options
with unique plugin name prefix.
NOTE: if you add argpase arguments such that users setting them can
create a config entry that python's bool() would consider false (ie,
the use might set the variable to "", [], 0, etc), please ensure that
cli._set_by_cli() works for your variable.
"""
@classmethod
def inject_parser_options(cls, parser, name):
"""Inject parser options.
See `~.IPlugin.inject_parser_options` for docs.
"""
# dummy function, doesn't check if dest.startswith(self.dest_namespace)
def add(arg_name_no_prefix, *args, **kwargs):
# pylint: disable=missing-docstring
return parser.add_argument(
"--{0}{1}".format(option_namespace(name), arg_name_no_prefix),
*args, **kwargs)
return cls.add_parser_arguments(add)
@property
def option_namespace(self):
"""ArgumentParser options namespace (prefix of all options)."""
@ -64,32 +94,6 @@ class Plugin(object):
def conf(self, var):
"""Find a configuration value for variable ``var``."""
return getattr(self.config, self.dest(var))
@classmethod
def inject_parser_options(cls, parser, name):
"""Inject parser options.
See `~.IPlugin.inject_parser_options` for docs.
"""
# dummy function, doesn't check if dest.startswith(self.dest_namespace)
def add(arg_name_no_prefix, *args, **kwargs):
# pylint: disable=missing-docstring
return parser.add_argument(
"--{0}{1}".format(option_namespace(name), arg_name_no_prefix),
*args, **kwargs)
return cls.add_parser_arguments(add)
@jose_util.abstractclassmethod
def add_parser_arguments(cls, add):
"""Add plugin arguments to the CLI argument parser.
:param callable add: Function that proxies calls to
`argparse.ArgumentParser.add_argument` prepending options
with unique plugin name prefix.
"""
# other

View file

@ -91,7 +91,8 @@ s.serve_forever()" """
help="Automatically allows public IP logging.")
def prepare(self): # pylint: disable=missing-docstring,no-self-use
pass # pragma: no cover
if self.config.noninteractive_mode and not self.conf("test-mode"):
raise errors.PluginError("Running manual mode non-interactively is not supported")
def more_info(self): # pylint: disable=missing-docstring,no-self-use
return ("This plugin requires user's manual intervention in setting "

View file

@ -23,16 +23,21 @@ class AuthenticatorTest(unittest.TestCase):
def setUp(self):
from letsencrypt.plugins.manual import Authenticator
self.config = mock.MagicMock(
http01_port=8080, manual_test_mode=False, manual_public_ip_logging_ok=False)
http01_port=8080, manual_test_mode=False,
manual_public_ip_logging_ok=False, noninteractive_mode=True)
self.auth = Authenticator(config=self.config, name="manual")
self.achalls = [achallenges.KeyAuthorizationAnnotatedChallenge(
challb=acme_util.HTTP01_P, domain="foo.com", account_key=KEY)]
config_test_mode = mock.MagicMock(
http01_port=8080, manual_test_mode=True)
http01_port=8080, manual_test_mode=True, noninteractive_mode=True)
self.auth_test_mode = Authenticator(
config=config_test_mode, name="manual")
def test_prepare(self):
self.assertRaises(errors.PluginError, self.auth.prepare)
self.auth_test_mode.prepare() # error not raised
def test_more_info(self):
self.assertTrue(isinstance(self.auth.more_info(), str))

View file

@ -90,6 +90,9 @@ class ServerManager(object):
logger.debug("Stopping server at %s:%d...",
*instance.server.socket.getsockname()[:2])
instance.server.shutdown()
# Not calling server_close causes problems when renewing multiple
# certs with `letsencrypt renew` using TLSSNI01 and PyOpenSSL 0.13
instance.server.server_close()
instance.thread.join()
del self._instances[port]
@ -200,7 +203,8 @@ class Authenticator(common.Plugin):
return self.supported_challenges
def perform(self, achalls): # pylint: disable=missing-docstring
if any(util.already_listening(port) for port in self._necessary_ports):
renewer = self.config.verb == "renew"
if any(util.already_listening(port, renewer) for port in self._necessary_ports):
raise errors.MisconfigurationError(
"At least one of the (possibly) required ports is "
"already taken.")

View file

@ -125,7 +125,7 @@ class AuthenticatorTest(unittest.TestCase):
self.config.standalone_supported_challenges = chall
self.assertRaises(
errors.MisconfigurationError, self.auth.perform, [])
mock_util.already_listening.assert_called_once_with(port)
mock_util.already_listening.assert_called_once_with(port, False)
mock_util.already_listening.reset_mock()
@mock.patch("letsencrypt.plugins.standalone.zope.component.getUtility")

View file

@ -11,7 +11,7 @@ from letsencrypt import interfaces
logger = logging.getLogger(__name__)
def already_listening(port):
def already_listening(port, renewer=False):
"""Check if a process is already listening on the port.
If so, also tell the user via a display notification.
@ -49,11 +49,20 @@ def already_listening(port):
pid = listeners[0]
name = psutil.Process(pid).name()
display = zope.component.getUtility(interfaces.IDisplay)
extra = ""
if renewer:
extra = (
" For automated renewal, you may want to use a script that stops"
" and starts your webserver. You can find an example at"
" https://letsencrypt.org/howitworks/#writing-your-own-renewal-script"
". Alternatively you can use the webroot plugin to renew without"
" needing to stop and start your webserver.")
display.notification(
"The program {0} (process ID {1}) is already listening "
"on TCP port {2}. This will prevent us from binding to "
"that port. Please stop the {0} program temporarily "
"and then try again.".format(name, pid, port))
"and then try again.{3}".format(name, pid, port, extra),
height=13)
return True
except (psutil.NoSuchProcess, psutil.AccessDenied):
# Perhaps the result of a race where the process could have

View file

@ -49,8 +49,10 @@ to serve all files under specified web root ({0})."""
path_map = self.conf("map")
if not path_map:
raise errors.PluginError("--{0} must be set".format(
self.option_name("path")))
raise errors.PluginError(
"Missing parts of webroot configuration; please set either "
"--webroot-path and --domains, or --webroot-map. Run with "
" --help webroot for examples.")
for name, path in path_map.items():
if not os.path.isdir(path):
raise errors.PluginError(path + " does not exist or is not a directory")

View file

@ -769,6 +769,9 @@ class RenewableCert(object): # pylint: disable=too-many-instance-attributes
self.cli_config = cli_config
target_version = self.next_free_version()
archive = self.cli_config.archive_dir
# XXX if anyone ever moves a renewal configuration file, this will
# break... perhaps prefix should be the dirname of the previous
# cert.pem?
prefix = os.path.join(archive, self.lineagename)
target = dict(
[(kind,

View file

@ -437,9 +437,12 @@ class ReportFailedChallsTest(unittest.TestCase):
"chall": acme_util.HTTP01,
"uri": "uri",
"status": messages.STATUS_INVALID,
"error": messages.Error(typ="tls", detail="detail"),
"error": messages.Error(typ="urn:acme:error:tls", detail="detail"),
}
# Prevent future regressions if the error type changes
self.assertTrue(kwargs["error"].description is not None)
self.http01 = achallenges.KeyAuthorizationAnnotatedChallenge(
# pylint: disable=star-args
challb=messages.ChallengeBody(**kwargs),

View file

@ -227,13 +227,17 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertTrue("MisconfigurationError" in ret)
args = ["certonly", "--webroot"]
ret, _, _, _ = self._call(args)
self.assertTrue("--webroot-path must be set" in ret)
try:
self._call(args)
assert False, "Exception should have been raised"
except errors.PluginSelectionError as e:
self.assertTrue("please set either --webroot-path" in e.message)
self._cli_missing_flag(["--standalone"], "With the standalone plugin, you probably")
with mock.patch("letsencrypt.cli._init_le_client") as mock_init:
with mock.patch("letsencrypt.cli._auth_from_domains"):
with mock.patch("letsencrypt.cli._auth_from_domains") as mock_afd:
mock_afd.return_value = (mock.MagicMock(), mock.MagicMock())
self._call(["certonly", "--manual", "-d", "foo.bar"])
unused_config, auth, unused_installer = mock_init.call_args[0]
self.assertTrue(isinstance(auth, manual.Authenticator))
@ -323,11 +327,11 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertEqual(config.fullchain_path, os.path.abspath(fullchain))
def test_certonly_bad_args(self):
ret, _, _, _ = self._call(['-d', 'foo.bar', 'certonly', '--csr', CSR])
self.assertEqual(ret, '--domains and --csr are mutually exclusive')
ret, _, _, _ = self._call(['-a', 'bad_auth', 'certonly'])
self.assertEqual(ret, 'The requested bad_auth plugin does not appear to be installed')
try:
self._call(['-a', 'bad_auth', 'certonly'])
assert False, "Exception should have been raised"
except errors.PluginSelectionError as e:
self.assertTrue('The requested bad_auth plugin does not appear' in e.message)
def test_check_config_sanity_domain(self):
# Punycode
@ -530,35 +534,52 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertRaises(errors.Error,
self._certonly_new_request_common, mock_client)
def _test_certonly_renewal_common(self, renewal_verb, extra_args=None):
def _test_renewal_common(self, due_for_renewal, extra_args, log_out=None,
args=None, renew=True):
# pylint: disable=too-many-locals
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_lineage.should_autorenew.return_value = due_for_renewal
mock_certr = mock.MagicMock()
mock_key = mock.MagicMock(pem='pem_key')
with mock.patch('letsencrypt.cli._treat_as_renewal') as mock_renewal:
mock_renewal.return_value = (renewal_verb, mock_lineage)
mock_client = mock.MagicMock()
mock_client.obtain_certificate.return_value = (mock_certr, 'chain',
mock_key, 'csr')
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
mock_init.return_value = mock_client
get_utility_path = 'letsencrypt.cli.zope.component.getUtility'
with mock.patch(get_utility_path) as mock_get_utility:
with mock.patch('letsencrypt.cli.OpenSSL'):
with mock.patch('letsencrypt.cli.crypto_util'):
args = ['-d', 'foo.bar', '-a',
'standalone', 'certonly']
if extra_args:
args += extra_args
self._call(args)
mock_client = mock.MagicMock()
mock_client.obtain_certificate.return_value = (mock_certr, 'chain',
mock_key, 'csr')
try:
with mock.patch('letsencrypt.cli._find_duplicative_certs') as mock_fdc:
mock_fdc.return_value = (mock_lineage, None)
with mock.patch('letsencrypt.cli._init_le_client') as mock_init:
mock_init.return_value = mock_client
get_utility_path = 'letsencrypt.cli.zope.component.getUtility'
with mock.patch(get_utility_path) as mock_get_utility:
with mock.patch('letsencrypt.cli.OpenSSL') as mock_ssl:
mock_latest = mock.MagicMock()
mock_latest.get_issuer.return_value = "Fake fake"
mock_ssl.crypto.load_certificate.return_value = mock_latest
with mock.patch('letsencrypt.cli.crypto_util'):
if not args:
args = ['-d', 'isnot.org', '-a', 'standalone', 'certonly']
if extra_args:
args += extra_args
self._call(args)
mock_client.obtain_certificate.assert_called_once_with(['foo.bar'])
if log_out:
with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
self.assertTrue(log_out in lf.read())
if renew:
mock_client.obtain_certificate.assert_called_once_with(['isnot.org'])
else:
self.assertEqual(mock_client.obtain_certificate.call_count, 0)
except:
self._dump_log()
raise
return mock_lineage, mock_get_utility
def test_certonly_renewal(self):
lineage, get_utility = self._test_certonly_renewal_common('renew')
lineage, get_utility = self._test_renewal_common(True, [])
self.assertEqual(lineage.save_successor.call_count, 1)
lineage.update_all_links_to.assert_called_once_with(
lineage.latest_common_version())
@ -566,12 +587,122 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self.assertTrue('fullchain.pem' in cert_msg)
self.assertTrue('donate' in get_utility().add_message.call_args[0][0])
def test_certonly_dry_run_reinstall_is_renewal(self):
_, get_utility = self._test_certonly_renewal_common('reinstall',
['--dry-run'])
def test_certonly_renewal_triggers(self):
# --dry-run should force renewal
_, get_utility = self._test_renewal_common(False, ['--dry-run', '--keep'],
log_out="simulating renewal")
self.assertEqual(get_utility().add_message.call_count, 1)
self.assertTrue('dry run' in get_utility().add_message.call_args[0][0])
_, _ = self._test_renewal_common(False, ['--renew-by-default', '-tvv', '--debug'],
log_out="Auto-renewal forced")
self.assertEqual(get_utility().add_message.call_count, 1)
_, _ = self._test_renewal_common(False, ['-tvv', '--debug', '--keep'],
log_out="not yet due", renew=False)
def _dump_log(self):
with open(os.path.join(self.logs_dir, "letsencrypt.log")) as lf:
print "Logs:"
print lf.read()
def test_renew_verb(self):
with open(test_util.vector_path('sample-renewal.conf')) as src:
# put the correct path for cert.pem, chain.pem etc in the renewal conf
renewal_conf = src.read().replace("MAGICDIR", test_util.vector_path())
rd = os.path.join(self.config_dir, "renewal")
if not os.path.exists(rd):
os.makedirs(rd)
rc = os.path.join(rd, "sample-renewal.conf")
with open(rc, "w") as dest:
dest.write(renewal_conf)
args = ["renew", "--dry-run", "-tvv"]
self._test_renewal_common(True, [], args=args, renew=True)
def test_renew_verb_empty_config(self):
renewer_configs_dir = os.path.join(self.config_dir, 'renewal')
os.makedirs(renewer_configs_dir)
with open(os.path.join(renewer_configs_dir, 'empty.conf'), 'w'):
pass # leave the file empty
self.test_renew_verb()
def _make_dummy_renewal_config(self):
renewer_configs_dir = os.path.join(self.config_dir, 'renewal')
os.makedirs(renewer_configs_dir)
with open(os.path.join(renewer_configs_dir, 'test.conf'), 'w') as f:
f.write("My contents don't matter")
def _test_renew_common(self, renewalparams=None,
names=None, assert_oc_called=None):
self._make_dummy_renewal_config()
with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc:
mock_lineage = mock.MagicMock()
mock_lineage.fullchain = "somepath/fullchain.pem"
if renewalparams is not None:
mock_lineage.configuration = {'renewalparams': renewalparams}
if names is not None:
mock_lineage.names.return_value = names
mock_rc.return_value = mock_lineage
with mock.patch('letsencrypt.cli.obtain_cert') as mock_obtain_cert:
self._test_renewal_common(True, None,
args=['renew'], renew=False)
if assert_oc_called is not None:
if assert_oc_called:
self.assertTrue(mock_obtain_cert.called)
else:
self.assertFalse(mock_obtain_cert.called)
def test_renew_no_renewalparams(self):
self._test_renew_common(assert_oc_called=False)
def test_renew_no_authenticator(self):
self._test_renew_common(renewalparams={}, assert_oc_called=False)
def test_renew_with_bad_int(self):
renewalparams = {'authenticator': 'webroot',
'rsa_key_size': 'over 9000'}
self._test_renew_common(renewalparams=renewalparams,
assert_oc_called=False)
def test_renew_with_bad_domain(self):
renewalparams = {'authenticator': 'webroot'}
names = ['*.example.com']
self._test_renew_common(renewalparams=renewalparams,
names=names, assert_oc_called=False)
def test_renew_plugin_config_restoration(self):
renewalparams = {'authenticator': 'webroot',
'webroot_path': 'None',
'webroot_imaginary_flag': '42'}
self._test_renew_common(renewalparams=renewalparams,
assert_oc_called=True)
def test_renew_reconstitute_error(self):
# pylint: disable=protected-access
with mock.patch('letsencrypt.cli._reconstitute') as mock_reconstitute:
mock_reconstitute.side_effect = Exception
self._test_renew_common(assert_oc_called=False)
def test_renew_obtain_cert_error(self):
self._make_dummy_renewal_config()
with mock.patch('letsencrypt.storage.RenewableCert') as mock_rc:
mock_lineage = mock.MagicMock()
mock_lineage.fullchain = "somewhere/fullchain.pem"
mock_rc.return_value = mock_lineage
mock_lineage.configuration = {
'renewalparams': {'authenticator': 'webroot'}}
with mock.patch('letsencrypt.cli.obtain_cert') as mock_obtain_cert:
mock_obtain_cert.side_effect = Exception
self._test_renewal_common(True, None,
args=['renew'], renew=False)
def test_renew_with_bad_cli_args(self):
self.assertRaises(errors.Error, self._test_renewal_common, True, None,
args='renew -d example.com'.split(), renew=False)
self.assertRaises(errors.Error, self._test_renewal_common, True, None,
args='renew --csr {0}'.format(CSR).split(),
renew=False)
@mock.patch('letsencrypt.cli.zope.component.getUtility')
@mock.patch('letsencrypt.cli._treat_as_renewal')
@mock.patch('letsencrypt.cli._init_le_client')
@ -581,8 +712,8 @@ class CLITest(unittest.TestCase): # pylint: disable=too-many-public-methods
self._call(['-d', 'foo.bar', '-a', 'standalone', 'certonly'])
self.assertFalse(mock_client.obtain_certificate.called)
self.assertFalse(mock_client.obtain_and_enroll_certificate.called)
self.assertTrue(
'donate' in mock_get_utility().add_message.call_args[0][0])
self.assertEqual(mock_get_utility().add_message.call_count, 0)
#self.assertTrue('donate' not in mock_get_utility().add_message.call_args[0][0])
def _test_certonly_csr_common(self, extra_args=None):
certr = 'certr'

View file

@ -82,6 +82,7 @@ class ClientTest(unittest.TestCase):
no_verify_ssl=False, config_dir="/etc/letsencrypt")
# pylint: disable=star-args
self.account = mock.MagicMock(**{"key.pem": KEY})
self.eg_domains = ["example.com", "www.example.com"]
from letsencrypt.client import Client
with mock.patch("letsencrypt.client.acme_client.Client") as acme:
@ -101,21 +102,40 @@ class ClientTest(unittest.TestCase):
self.acme.fetch_chain.return_value = mock.sentinel.chain
def _check_obtain_certificate(self):
self.client.auth_handler.get_authorizations.assert_called_once_with(
["example.com", "www.example.com"])
self.client.auth_handler.get_authorizations.assert_called_once_with(self.eg_domains)
self.acme.request_issuance.assert_called_once_with(
jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, CSR_SAN)),
self.client.auth_handler.get_authorizations())
self.acme.fetch_chain.assert_called_once_with(mock.sentinel.certr)
def test_obtain_certificate_from_csr(self):
# FIXME move parts of this to test_cli.py...
@mock.patch("letsencrypt.cli._process_domain")
def test_obtain_certificate_from_csr(self, mock_process_domain):
self._mock_obtain_certificate()
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(le_util.CSR(
form="der", file=None, data=CSR_SAN)))
self._check_obtain_certificate()
from letsencrypt import cli
test_csr = le_util.CSR(form="der", file=None, data=CSR_SAN)
mock_parsed_args = mock.MagicMock()
with mock.patch("letsencrypt.client.le_util.CSR") as mock_CSR:
mock_CSR.return_value = test_csr
mock_parsed_args.domains = self.eg_domains[:]
mock_parser = mock.MagicMock(cli.HelpfulArgumentParser)
cli.HelpfulArgumentParser.handle_csr(mock_parser, mock_parsed_args)
# make sure cli processing occurred
cli_processed = (call[0][1] for call in mock_process_domain.call_args_list)
self.assertEqual(set(cli_processed), set(("example.com", "www.example.com")))
# Now provoke an inconsistent domains error...
mock_parsed_args.domains.append("hippopotamus.io")
self.assertRaises(errors.ConfigurationError,
cli.HelpfulArgumentParser.handle_csr, mock_parser, mock_parsed_args)
self.assertEqual(
(mock.sentinel.certr, mock.sentinel.chain),
self.client.obtain_certificate_from_csr(self.eg_domains, test_csr))
# and that the cert was obtained correctly
self._check_obtain_certificate()
@mock.patch("letsencrypt.client.crypto_util")
def test_obtain_certificate(self, mock_crypto_util):

View file

@ -0,0 +1,28 @@
-----BEGIN CERTIFICATE-----
MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF
ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5
MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+
slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN
NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h
A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx
UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP
r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC
AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8
L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE
bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy
eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0
c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB
8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw
Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl
cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy
dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl
IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0
b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy
KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve
jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2
Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU
+ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf
rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ==
-----END CERTIFICATE-----

View file

@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV
BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw
NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i
8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8
tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj
7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8
BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD
HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj
UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7
eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB
vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl
zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo
vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L
oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW
rFo4Uv1EnkKJm3vJFe50eJGhEKlx
-----END CERTIFICATE-----

View file

@ -0,0 +1,47 @@
-----BEGIN CERTIFICATE-----
MIIE1DCCA7ygAwIBAgITAPoz/CBluNQV/Eh9F+CS6dSxEDANBgkqhkiG9w0BAQsF
ADAfMR0wGwYDVQQDDBRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAyMDIyMzQ5
MDBaFw0xNjA1MDIyMzQ5MDBaMBQxEjAQBgNVBAMTCWlzbm90Lm9yZzCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBALyudqLKcIdWZ5VaK1fuhlEDbZtvs2E+
slm4dmSS1nFve7MdlZ69K0gdtnhkiPQ0wGQTligeDZ8fY8iL87GZO0tp5f7S+QJN
NYCiYw6j4qp5JBy/zG22kJz1Quu7/vXMYLzLvK6x6YixiWAWyqqvlUVBLS1r4W3h
A5Z+F1EIsXeyz7TJe3lAzIWAAxpfH9OviIz2rEDotuCdU771USLLNSw4qJojNlTx
UpZG6lGFs8KGb8tqROXknaMKE4PvN3SITixSUTFbktt1Wz60moWbNdLMKvgkzuUP
r4viO2P4SO5slNAY0ZeEssPpVAelN3EvrAcEZtoKmG5fnQDVo8uVag0CAwEAAaOC
AhIwggIOMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
BQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUqhI4u6aaPrcYQnmypxV8Tap8
L54wHwYDVR0jBBgwFoAU+3hPEvlgFYMsnxd/NBmzLjbqQYkweAYIKwYBBQUHAQEE
bDBqMDMGCCsGAQUFBzABhidodHRwOi8vb2NzcC5zdGFnaW5nLXgxLmxldHNlbmNy
eXB0Lm9yZy8wMwYIKwYBBQUHMAKGJ2h0dHA6Ly9jZXJ0LnN0YWdpbmcteDEubGV0
c2VuY3J5cHQub3JnLzAUBgNVHREEDTALgglpc25vdC5vcmcwgf4GA1UdIASB9jCB
8zAIBgZngQwBAgEwgeYGCysGAQQBgt8TAQEBMIHWMCYGCCsGAQUFBwIBFhpodHRw
Oi8vY3BzLmxldHNlbmNyeXB0Lm9yZzCBqwYIKwYBBQUHAgIwgZ4MgZtUaGlzIENl
cnRpZmljYXRlIG1heSBvbmx5IGJlIHJlbGllZCB1cG9uIGJ5IFJlbHlpbmcgUGFy
dGllcyBhbmQgb25seSBpbiBhY2NvcmRhbmNlIHdpdGggdGhlIENlcnRpZmljYXRl
IFBvbGljeSBmb3VuZCBhdCBodHRwczovL2xldHNlbmNyeXB0Lm9yZy9yZXBvc2l0
b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAbAhX6FfQwELayneY4l5RvYSdw/Jj5CRy
KzrM7ISld7x9YPpxX6Pmht/YyMhLWrtxvFUR2+RNhSIYB8IjQEjmKjvR7UNeiUve
jzPEAuTg/9m3i0FJpPHc2aKGzlLFQCMm5/RrvnXI6ljIcyhocLvMiN46iexcExI2
Ese3w8GoH6wARYKxU/QBexfoXQLgtAbYzNRE6EgKWtB+txV+7+d2MgbhCEit5VwU
+ydT8inp9URsA7iKM03hDdGOBysddkrm1/yEhVy/Oo6bT9WMAUHVvz61hHekWcSf
rAQ6BayubvWOUx06eTowXr1gln/rl+WXOxcsJeag127NuhmHOCXZxQ==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIDETCCAfmgAwIBAgIJAJzxkS6o1QkIMA0GCSqGSIb3DQEBCwUAMB8xHTAbBgNV
BAMMFGhhcHB5IGhhY2tlciBmYWtlIENBMB4XDTE1MDQwNzIzNTAzOFoXDTI1MDQw
NDIzNTAzOFowHzEdMBsGA1UEAwwUaGFwcHkgaGFja2VyIGZha2UgQ0EwggEiMA0G
CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDCCkd5mgXFErJ3F2M0E9dw+Ta/md5i
8TDId01HberAApqmydG7UZYF3zLTSzNjlNSOmtybvrSGUnZ9r9tSQcL8VM6WUOM8
tnIpiIjEA2QkBycMwvRmZ/B2ltPdYs/R9BqNwO1g18GDZrHSzUYtNKNeFI6Glamj
7GK2Vr0SmiEamlNIR5ktAFsEErzf/d4jCF7sosMsJpMCm1p58QkP4LHLShVLXDa8
BMfVoI+ipYcA08iNUFkgW8VWDclIDxcysa0psDDtMjX3+4aPkE/cefmP+1xOfUuD
HOGV8XFynsP4EpTfVOZr0/g9gYQ7ZArqXX7GTQkFqduwPm/w5qxSPTarAgMBAAGj
UDBOMB0GA1UdDgQWBBT7eE8S+WAVgyyfF380GbMuNupBiTAfBgNVHSMEGDAWgBT7
eE8S+WAVgyyfF380GbMuNupBiTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUA
A4IBAQAd9Da+Zv+TjMv7NTAmliqnWHY6d3UxEZN3hFEJ58IQVHbBZVZdW7zhRktB
vR05Kweac0HJeK91TKmzvXl21IXLvh0gcNLU/uweD3no/snfdB4OoFompljThmgl
zBqiqWoKBJQrLCA8w5UB+ReomRYd/EYXF/6TAfzm6hr//Xt5mPiUHPdvYt75lMAo
vRxLSbF8TSQ6b7BYxISWjPgFASNNqJNHEItWsmQMtAjjwzb9cs01XH9pChVAWn9L
oeMKa+SlHSYrWG93+EcrIH/dGU76uNOiaDzBSKvaehG53h25MHuO1anNICJvZovW
rFo4Uv1EnkKJm3vJFe50eJGhEKlx
-----END CERTIFICATE-----

View file

@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8rnaiynCHVmeV
WitX7oZRA22bb7NhPrJZuHZkktZxb3uzHZWevStIHbZ4ZIj0NMBkE5YoHg2fH2PI
i/OxmTtLaeX+0vkCTTWAomMOo+KqeSQcv8xttpCc9ULru/71zGC8y7yusemIsYlg
Fsqqr5VFQS0ta+Ft4QOWfhdRCLF3ss+0yXt5QMyFgAMaXx/Tr4iM9qxA6LbgnVO+
9VEiyzUsOKiaIzZU8VKWRupRhbPChm/LakTl5J2jChOD7zd0iE4sUlExW5LbdVs+
tJqFmzXSzCr4JM7lD6+L4jtj+EjubJTQGNGXhLLD6VQHpTdxL6wHBGbaCphuX50A
1aPLlWoNAgMBAAECggEAfKKWFWS6PnwSAnNErFoQeZVVItb/XB5JO8EA2+CvLNFi
mefR/MCixYlzDkYCvaXW7ISPrMJlZxYaGNBx0oAQzfkPB2wfNqj/zY/29SXGxast
8puzk0mEb1oHsaZGfeFaiXvfkFpPlI8J2uJTT7qaVNv/1sArciSv9QonpsyiRhlB
yqT49juNVoR1tJHyXzkkRfHKTG8OlJd4kuFOl3fM9dTFPQ/ft0kTNAQ/B4SFvSwF
RJsbLbsbFGsUdV9ekE6UX6oWD/Ah707rvgtCyS0Bc+0O3t2EKwmm3RXPRUMHCVxE
bKdTxRB4etbjMVXMuVhB8Y4GbfrtMCy+qxZQ6znCAQKBgQDr7bcYAZVZp/nBMVB+
lBO9w73J6lnEWm6bZ9728KlGAKETaRhxZQSi6TN6MWwNwnk6rinyz4uVwVr9ZRCs
WkB1TbvW0JNcWdr3YClwsKXAt8X22bjGe0LagDJHG6r1TPS+MdovOS2M6IMaxlbT
rzFhSJ8ojLX3tqnOsmc7YAFLjQKBgQDMu8E9hoJt82lQzOGrjHmGzGEu2GLx9WKO
e4nkj335kX6fIhMMqSXBFbTJZwXoYvk5J8ZnaARbYG0m5nxDCwRjX5HWa8q0B2Po
ta53w01sKKznzlPjUhsdhEthun7MCFfLZpgvcZ9xVzOXo3/Zfn2+RrsPSjrVDqBy
hj+k5mW4gQKBgHFWKf3LTO7cBdvsD8ou4mjn7nVgMi1kb/wR4wdnxzmMtdR4STi4
GYkVVBhgQ5M8mDY7UoWFdH3FfCt8cI0Lcimn5ROl8RSNSeZKeL3c7lNtNRmHr/8R
WaVTrlOAlBjxFiWEF1dWNW6ah9jF7RIV+DfOxj6ZkhTk2CAmjfb1AMpFAoGABf96
KdNG/vGipDtcYSo8ZTaXoke0nmISARqdb5TEnAsnKoJVDInoEUARi9T411YO9x2z
MlRZzFOG3xzhhxVLi53BKAcAaUXOJ4MrGVcfbYvDhQcGbiJ5qOO3UaWlEVUtPUhE
LR+nDCsB1+9yT2zlQi3QTSJflt5W1QQZ2TrmwAECgYEAvQ7+sTcHs1K9yKj7koEu
A19FbMA0IwvrVRcV/VqmlsoW6e6wW2YND+GtaDbKdD0aBPivqLJwpNFrsRA+W0iB
vzmML6sKhhL+j7tjSgq+iQdBkKz0j9PyReuhe9CRnljMmyun+4qKEk0KUvxBrjPY
Skn+ML18qyUoEPnmbpfHxCs=
-----END PRIVATE KEY-----

View file

@ -0,0 +1 @@
../../archive/sample-renewal/cert1.pem

View file

@ -0,0 +1 @@
../../archive/sample-renewal/chain1.pem

View file

@ -0,0 +1 @@
../../archive/sample-renewal/fullchain1.pem

View file

@ -0,0 +1 @@
../../archive/sample-renewal/privkey1.pem

View file

@ -0,0 +1,76 @@
cert = MAGICDIR/live/sample-renewal/cert.pem
privkey = MAGICDIR/live/sample-renewal/privkey.pem
chain = MAGICDIR/live/sample-renewal/chain.pem
fullchain = MAGICDIR/live/sample-renewal/fullchain.pem
renew_before_expiry = 1 year
# Options and defaults used in the renewal process
[renewalparams]
no_self_upgrade = False
apache_enmod = a2enmod
no_verify_ssl = False
ifaces = None
apache_dismod = a2dismod
register_unsafely_without_email = False
apache_handle_modules = True
uir = None
installer = none
nginx_ctl = nginx
config_dir = MAGICDIR
text_mode = False
func = <function obtain_cert at 0x7f093a163c08>
staging = True
prepare = False
work_dir = /var/lib/letsencrypt
tos = False
init = False
http01_port = 80
duplicate = False
noninteractive_mode = True
key_path = None
nginx = False
nginx_server_root = /etc/nginx
fullchain_path = /home/ubuntu/letsencrypt/chain.pem
email = None
csr = None
agree_dev_preview = None
redirect = None
verb = certonly
verbose_count = -3
config_file = None
renew_by_default = False
hsts = False
apache_handle_sites = True
authenticator = standalone
domains = isnot.org,
rsa_key_size = 2048
apache_challenge_location = /etc/apache2
checkpoints = 1
manual_test_mode = False
apache = False
cert_path = /home/ubuntu/letsencrypt/cert.pem
webroot_path = None
reinstall = False
expand = False
strict_permissions = False
apache_server_root = /etc/apache2
account = None
dry_run = False
manual_public_ip_logging_ok = False
chain_path = /home/ubuntu/letsencrypt/chain.pem
break_my_certs = False
standalone = True
manual = False
server = https://acme-staging.api.letsencrypt.org/directory
standalone_supported_challenges = "tls-sni-01,http-01"
webroot = False
os_packages_only = False
apache_init_script = None
user_agent = None
apache_le_vhost_ext = -le-ssl.conf
debug = False
tls_sni_01_port = 443
logs_dir = /var/log/letsencrypt
apache_vhost_root = /etc/apache2/sites-available
configurator = None
[[webroot_map]]

View file

@ -4,7 +4,7 @@ from setuptools import setup
from setuptools import find_packages
version = '0.4.0.dev0'
version = '0.5.0.dev0'
install_requires = [
'setuptools', # pkg_resources

View file

@ -20,16 +20,22 @@ else
readlink="readlink"
fi
common() {
letsencrypt_test \
common_no_force_renew() {
letsencrypt_test_no_force_renew \
--authenticator standalone \
--installer null \
"$@"
}
common() {
common_no_force_renew \
--renew-by-default \
"$@"
}
common --domains le1.wtf --standalone-supported-challenges tls-sni-01 auth
common --domains le2.wtf --standalone-supported-challenges http-01 run
common -a manual -d le.wtf auth
common -a manual -d le.wtf auth --rsa-key-size 4096
export CSR_PATH="${root}/csr.der" KEY_PATH="${root}/key.pem" \
OPENSSL_CNF=examples/openssl.cnf
@ -44,6 +50,40 @@ common --domains le3.wtf install \
--cert-path "${root}/csr/cert.pem" \
--key-path "${root}/csr/key.pem"
CheckCertCount() {
CERTCOUNT=`ls "${root}/conf/archive/le.wtf/cert"* | wc -l`
if [ "$CERTCOUNT" -ne "$1" ] ; then
echo Wrong cert count, not "$1" `ls "${root}/conf/archive/le.wtf/"*`
exit 1
fi
}
CheckCertCount 1
# This won't renew (because it's not time yet)
common_no_force_renew renew
CheckCertCount 1
# --renew-by-default is used, so renewal should occur
common renew
CheckCertCount 2
# This will renew because the expiry is less than 10 years from now
sed -i "4arenew_before_expiry = 10 years" "$root/conf/renewal/le.wtf.conf"
common_no_force_renew renew --rsa-key-size 2048
CheckCertCount 3
# The 4096 bit setting should persist to the first renewal, but be overriden in the second
size1=`wc -c ${root}/conf/archive/le.wtf/privkey1.pem | cut -d" " -f1`
size2=`wc -c ${root}/conf/archive/le.wtf/privkey2.pem | cut -d" " -f1`
size3=`wc -c ${root}/conf/archive/le.wtf/privkey3.pem | cut -d" " -f1`
# 4096 bit PEM keys are about ~3270 bytes, 2048 ones are about 1700 bytes
if [ "$size1" -lt 3000 ] || [ "$size2" -lt 3000 ] || [ "$size3" -gt 1800 ] ; then
echo key sizes violate assumptions:
ls -l "${root}/conf/archive/le.wtf/privkey"*
exit 1
fi
# revoke by account key
common revoke --cert-path "$root/conf/live/le.wtf/cert.pem"
# revoke renewed

View file

@ -12,6 +12,12 @@ store_flags="$store_flags --logs-dir $root/logs"
export root store_flags
letsencrypt_test () {
letsencrypt_test_no_force_renew \
--renew-by-default \
"$@"
}
letsencrypt_test_no_force_renew () {
letsencrypt \
--server "${SERVER:-http://localhost:4000/directory}" \
--no-verify-ssl \
@ -19,11 +25,10 @@ letsencrypt_test () {
--http-01-port 5002 \
--manual-test-mode \
$store_flags \
--text \
--non-interactive \
--no-redirect \
--agree-tos \
--register-unsafely-without-email \
--renew-by-default \
--debug \
-vvvvvvv \
"$@"

View file

@ -241,21 +241,21 @@ def local_git_clone(repo_url):
"clones master of repo_url"
with lcd(LOGDIR):
local('if [ -d letsencrypt ]; then rm -rf letsencrypt; fi')
local('git clone %s'% repo_url)
local('git clone %s letsencrypt'% repo_url)
local('tar czf le.tar.gz letsencrypt')
def local_git_branch(repo_url, branch_name):
"clones branch <branch_name> of repo_url"
with lcd(LOGDIR):
local('if [ -d letsencrypt ]; then rm -rf letsencrypt; fi')
local('git clone %s --branch %s --single-branch'%(repo_url, branch_name))
local('git clone %s letsencrypt --branch %s --single-branch'%(repo_url, branch_name))
local('tar czf le.tar.gz letsencrypt')
def local_git_PR(repo_url, PRnumstr, merge_master=True):
"clones specified pull request from repo_url and optionally merges into master"
with lcd(LOGDIR):
local('if [ -d letsencrypt ]; then rm -rf letsencrypt; fi')
local('git clone %s'% repo_url)
local('git clone %s letsencrypt'% repo_url)
local('cd letsencrypt && git fetch origin pull/%s/head:lePRtest'%PRnumstr)
local('cd letsencrypt && git co lePRtest')
if merge_master:

View file

@ -35,22 +35,16 @@ then
#sudo cp /etc/httpd/sites-available/$PUBLIC_HOSTNAME.conf /etc/httpd/sites-enabled/
fi
# run letsencrypt-apache2 via letsencrypt-auto
# Run letsencrypt-apache2.
cd letsencrypt
export SUDO=sudo
if [ -f /etc/debian_version ] ; then
echo "Bootstrapping dependencies for Debian-based OSes..."
$SUDO bootstrap/_deb_common.sh
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO bootstrap/_rpm_common.sh
else
echo "Dont have bootstrapping for this OS!"
exit 1
echo "Bootstrapping dependencies..."
letsencrypt-auto-source/letsencrypt-auto --os-packages-only
if [ $? -ne 0 ] ; then
exit 1
fi
bootstrap/dev/venv.sh
tools/venv.sh
sudo venv/bin/letsencrypt -v --debug --text --agree-dev-preview --agree-tos \
--renew-by-default --redirect --register-unsafely-without-email \
--domain $PUBLIC_HOSTNAME --server $BOULDER_URL

View file

@ -8,7 +8,8 @@
#private_ip=$(curl -s http://169.254.169.254/2014-11-05/meta-data/local-ipv4)
cd letsencrypt
./letsencrypt-auto certonly -v --standalone --debug \
./letsencrypt-auto --os-packages-only
./letsencrypt-auto certonly --no-self-upgrade -v --standalone --debug \
--text --agree-dev-preview --agree-tos \
--renew-by-default --redirect \
--register-unsafely-without-email \

View file

@ -0,0 +1,55 @@
#!/bin/bash -x
# $OS_TYPE $PUBLIC_IP $PRIVATE_IP $PUBLIC_HOSTNAME $BOULDER_URL
# are dynamically set at execution
# run letsencrypt-apache2 via letsencrypt-auto
cd letsencrypt
export SUDO=sudo
if [ -f /etc/debian_version ] ; then
echo "Bootstrapping dependencies for Debian-based OSes..."
$SUDO bootstrap/_deb_common.sh
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO bootstrap/_rpm_common.sh
else
echo "Dont have bootstrapping for this OS!"
exit 1
fi
bootstrap/dev/venv.sh
sudo venv/bin/letsencrypt certonly --debug --standalone -t --agree-dev-preview --agree-tos \
--renew-by-default --redirect --register-unsafely-without-email \
--domain $PUBLIC_HOSTNAME --server $BOULDER_URL -v
if [ $? -ne 0 ] ; then
FAIL=1
fi
if [ "$OS_TYPE" = "ubuntu" ] ; then
venv/bin/tox -e apacheconftest
else
echo Not running hackish apache tests on $OS_TYPE
fi
if [ $? -ne 0 ] ; then
FAIL=1
fi
sudo venv/bin/letsencrypt renew --renew-by-default
if [ $? -ne 0 ] ; then
FAIL=1
fi
ls /etc/letsencrypt/archive/$PUBLIC_HOSTNAME | grep -q 2.pem
if [ $? -ne 0 ] ; then
FAIL=1
fi
# return error if any of the subtests failed
if [ "$FAIL" = 1 ] ; then
exit 1
fi

View file

@ -6,71 +6,15 @@ VENV_NAME="venv"
LEA_PATH=./letsencrypt/
VENV_PATH=${LEA_PATH/$VENV_NAME}
VENV_BIN=${VENV_PATH}/bin
BOOTSTRAP=${LEA_PATH}/bootstrap
SUDO=sudo
ExperimentalBootstrap() {
# Arguments: Platform name, boostrap script name, SUDO command (iff needed)
if [ "$2" != "" ] ; then
echo "Bootstrapping dependencies for $1..."
if [ "$3" != "" ] ; then
"$3" "$BOOTSTRAP/$2"
else
"$BOOTSTRAP/$2"
fi
fi
}
# virtualenv call is not idempotent: it overwrites pip upgraded in
# later steps, causing "ImportError: cannot import name unpack_url"
if [ ! -f $BOOTSTRAP/debian.sh ] ; then
echo "Cannot find the letsencrypt bootstrap scripts in $BOOTSTRAP"
exit 1
fi
if [ -f /etc/debian_version ] ; then
echo "Bootstrapping dependencies for Debian-based OSes..."
$SUDO $BOOTSTRAP/_deb_common.sh
elif [ -f /etc/redhat-release ] ; then
echo "Bootstrapping dependencies for RedHat-based OSes..."
$SUDO $BOOTSTRAP/_rpm_common.sh
elif `grep -q openSUSE /etc/os-release` ; then
echo "Bootstrapping dependencies for openSUSE-based OSes..."
$SUDO $BOOTSTRAP/_suse_common.sh
elif [ -f /etc/arch-release ] ; then
if [ "$DEBUG" = 1 ] ; then
echo "Bootstrapping dependencies for Archlinux..."
$SUDO $BOOTSTRAP/archlinux.sh
else
echo "Please use pacman to install letsencrypt packages:"
echo "# pacman -S letsencrypt letsencrypt-apache"
echo
echo "If you would like to use the virtualenv way, please run the script again with the"
echo "--debug flag."
exit 1
fi
elif [ -f /etc/manjaro-release ] ; then
ExperimentalBootstrap "Manjaro Linux" manjaro.sh "$SUDO"
elif [ -f /etc/gentoo-release ] ; then
ExperimentalBootstrap "Gentoo" _gentoo_common.sh "$SUDO"
elif uname | grep -iq FreeBSD ; then
ExperimentalBootstrap "FreeBSD" freebsd.sh "$SUDO"
elif uname | grep -iq Darwin ; then
ExperimentalBootstrap "Mac OS X" mac.sh # homebrew doesn't normally run as root
elif grep -iq "Amazon Linux" /etc/issue ; then
ExperimentalBootstrap "Amazon Linux" _rpm_common.sh "$SUDO"
else
echo "Sorry, I don't know how to bootstrap Let's Encrypt on your operating system!"
echo
echo "You will need to bootstrap, configure virtualenv, and run a pip install manually"
echo "Please see https://letsencrypt.readthedocs.org/en/latest/contributing.html#prerequisites"
echo "for more info"
fi
echo "Bootstrapped!"
"$LEA_PATH/letsencrypt-auto" --os-packages-only
cd letsencrypt
./bootstrap/dev/venv.sh
./tools/venv.sh
PYVER=`python --version 2>&1 | cut -d" " -f 2 | cut -d. -f1,2 | sed 's/\.//'`
if [ $PYVER -eq 26 ] ; then

View file

@ -9,9 +9,9 @@ fi
function sayhash { # $1 <-- HASH ; $2 <---SIGFILEBALL
while read -p "Press Enter to read the hash aloud or type 'done': " INP && [ "$INP" = "" ] ; do
cat $1 | (echo "(Parameter.set 'Duration_Stretch 1.5)"; \
cat $1 | (echo "(Parameter.set 'Duration_Stretch 1.8)"; \
echo -n '(SayText "'; \
sha1sum | cut -c1-40 | fold -1 | sed 's/^a$/alpha/; s/^b$/bravo/; s/^c$/charlie/; s/^d$/delta/; s/^e$/echo/; s/^f$/foxtrot/'; \
sha256sum | cut -c1-64 | fold -1 | sed 's/^a$/alpha/; s/^b$/bravo/; s/^c$/charlie/; s/^d$/delta/; s/^e$/echo/; s/^f$/foxtrot/'; \
echo '")' ) | festival
done
@ -23,8 +23,8 @@ function offlinesign { # $1 <-- INPFILE ; $2 <---SIGFILE
echo HASH FOR SIGNING:
SIGFILEBALL="$2.lzma.base64"
#echo "(place the resulting raw binary signature in $SIGFILEBALL)"
sha1sum $1
echo metahash for confirmation only $(sha1sum $1 |cut -d' ' -f1 | tr -d '\n' | sha1sum | cut -c1-6) ...
sha256sum $1
echo metahash for confirmation only $(sha256sum $1 |cut -d' ' -f1 | tr -d '\n' | sha256sum | cut -c1-6) ...
echo
sayhash $1 $SIGFILEBALL
}

View file

@ -171,10 +171,10 @@ while ! openssl dgst -sha256 -verify $RELEASE_OPENSSL_PUBKEY -signature \
read -p "Please correctly sign letsencrypt-auto with offline-signrequest.sh"
done
git add letsencrypt-auto-source
git diff --cached
git commit --gpg-sign="$RELEASE_GPG_KEY" -m "Release $version"
git tag --local-user "$RELEASE_GPG_KEY" \
--sign --message "Release $version" "$tag"
git tag --local-user "$RELEASE_GPG_KEY" --sign --message "Release $version" "$tag"
cd ..
echo Now in $PWD

View file

@ -3,7 +3,7 @@
export VENV_ARGS="--python python2"
./bootstrap/dev/_venv_common.sh \
./tools/_venv_common.sh \
-e acme[testing] \
-e .[dev,docs,testing] \
-e letsencrypt-apache \

View file

@ -4,5 +4,5 @@
export VENV_NAME="${VENV_NAME:-venv3}"
export VENV_ARGS="--python python3"
./bootstrap/dev/_venv_common.sh \
./tools/_venv_common.sh \
-e acme[testing] \