Merge branch 'master' into mageia

This commit is contained in:
Brad Warren 2016-06-07 16:23:08 -07:00
commit 079e6ee139
573 changed files with 21003 additions and 7780 deletions

8
.gitignore vendored
View file

@ -6,7 +6,10 @@ dist*/
/venv*/
/kgs/
/.tox/
/releases/
letsencrypt.log
certbot.log
letsencrypt-auto-source/letsencrypt-auto.sig.lzma.base64
# coverage
.coverage
@ -22,3 +25,8 @@ letsencrypt.log
# auth --cert-path --chain-path
/*.pem
# letstest
tests/letstest/letest-*/
tests/letstest/*.pem
tests/letstest/venv/

View file

@ -38,7 +38,7 @@ load-plugins=linter_plugin
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes
disable=fixme,locally-disabled,abstract-class-not-used,abstract-class-little-used,bad-continuation,too-few-public-methods,no-self-use,invalid-name,too-many-instance-attributes,cyclic-import
# abstract-class-not-used cannot be disabled locally (at least in
# pylint 1.4.1), same for abstract-class-little-used

View file

@ -1,28 +1,66 @@
language: python
services:
- rabbitmq
- mariadb
cache:
directories:
- $HOME/.cache/pip
# This makes sure we get a host with docker-compose present.
dist: trusty
# http://docs.travis-ci.com/user/ci-environment/#CI-environment-OS
# gimme has to be kept in sync with Boulder's Go version setting in .travis.yml
before_install:
- 'dpkg -s libaugeas0'
- '[ "xxx$BOULDER_INTEGRATION" = "xxx" ] || eval "$(gimme 1.5.1)"'
# using separate envs with different TOXENVs creates 4x1 Travis build
# matrix, which allows us to clearly distinguish which component under
# test has failed
env:
global:
- GOPATH=/tmp/go
- PATH=$GOPATH/bin:$PATH
matrix:
- TOXENV=py26 BOULDER_INTEGRATION=1
- TOXENV=py27 BOULDER_INTEGRATION=1
- TOXENV=lint
- TOXENV=cover
- BOULDERPATH=$PWD/boulder/
matrix:
include:
- python: "2.6"
env: TOXENV=py26 BOULDER_INTEGRATION=1
sudo: true
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
- python: "2.6"
env: TOXENV=py26-oldest BOULDER_INTEGRATION=1
sudo: true
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
- python: "2.7"
env: TOXENV=apacheconftest
sudo: required
- python: "2.7"
env: TOXENV=py27 BOULDER_INTEGRATION=1
sudo: true
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
- python: "2.7"
env: TOXENV=py27-oldest BOULDER_INTEGRATION=1
sudo: true
after_failure:
- sudo cat /var/log/mysql/error.log
- ps aux | grep mysql
- python: "2.7"
env: TOXENV=lint
- sudo: required
env: TOXENV=le_auto
services: docker
before_install:
addons:
- python: "2.7"
env: TOXENV=cover
- python: "3.3"
env: TOXENV=py33
- python: "3.4"
env: TOXENV=py34
- python: "3.5"
env: TOXENV=py35
# Only build pushes to the master branch, PRs, and branches beginning with
# `test-`. This reduces the number of simultaneous Travis runs, which speeds
@ -36,15 +74,21 @@ branches:
sudo: false
addons:
# make sure simplehttp simple verification works (custom /etc/hosts)
# Custom /etc/hosts required for simple verification of http-01
# and tls-sni-01, and for certbot_test_nginx
hosts:
- le.wtf
mariadb: "10.0"
- le1.wtf
- le2.wtf
- le3.wtf
- nginx.wtf
- boulder
- boulder-mysql
- boulder-rabbitmq
apt:
sources:
- augeas
packages: # keep in sync with bootstrap/ubuntu.sh and Boulder
- python
packages: # Keep in sync with letsencrypt-auto-source/pieces/bootstrappers/deb_common.sh and Boulder.
- python-dev
- python-virtualenv
- gcc
@ -53,11 +97,14 @@ addons:
- libssl-dev
- libffi-dev
- ca-certificates
# For letsencrypt-nginx integration testing
# For certbot-nginx integration testing
- nginx-light
- openssl
# For Boulder integration testing
- rsyslog
# for apacheconftest
- apache2
- libapache2-mod-wsgi
- libapache2-mod-macro
- sudo
install: "travis_retry pip install tox coveralls"
script: 'travis_retry tox && ([ "xxx$BOULDER_INTEGRATION" = "xxx" ] || ./tests/travis-integration.sh)'

View file

@ -3,25 +3,9 @@ ChangeLog
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>`_.
`commit log <https://github.com/certbot/certbot/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/certbot/certbot/issues?utf8=%E2%9C%93&q=milestone%3A0.3.0

View file

@ -15,4 +15,4 @@ to the Sphinx generated docs is provided below.
-->
https://letsencrypt.readthedocs.org/en/latest/contributing.html
https://certbot.eff.org/docs/contributing.html

View file

@ -10,20 +10,21 @@ MAINTAINER William Budington <bill@eff.org>
EXPOSE 443
# TODO: make sure --config-dir and --work-dir cannot be changed
# through the CLI (letsencrypt-docker wrapper that uses standalone
# through the CLI (certbot-docker wrapper that uses standalone
# authenticator and text mode only?)
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/letsencrypt
WORKDIR /opt/certbot
# no need to mkdir anything:
# https://docs.docker.com/reference/builder/#copy
# If <dest> doesn't exist, it is created along with all missing
# directories in its path.
ENV DEBIAN_FRONTEND=noninteractive
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
RUN /opt/letsencrypt/src/ubuntu.sh && \
COPY letsencrypt-auto-source/letsencrypt-auto /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto
RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
@ -33,35 +34,37 @@ RUN /opt/letsencrypt/src/ubuntu.sh && \
# Dockerfile we make sure we cache as much as possible
COPY setup.py README.rst CHANGES.rst MANIFEST.in /opt/letsencrypt/src/
COPY setup.py README.rst CHANGES.rst MANIFEST.in letsencrypt-auto-source/pieces/pipstrap.py /opt/certbot/src/
# all above files are necessary for setup.py, however, package source
# code directory has to be copied separately to a subdirectory...
# all above files are necessary for setup.py and venv setup, however,
# package source code directory has to be copied separately to a
# subdirectory...
# https://docs.docker.com/reference/builder/#copy: "If <src> is a
# directory, the entire contents of the directory are copied,
# including filesystem metadata. Note: The directory itself is not
# copied, just its contents." Order again matters, three files are far
# more likely to be cached than the whole project directory
COPY letsencrypt /opt/letsencrypt/src/letsencrypt/
COPY acme /opt/letsencrypt/src/acme/
COPY letsencrypt-apache /opt/letsencrypt/src/letsencrypt-apache/
COPY letsencrypt-nginx /opt/letsencrypt/src/letsencrypt-nginx/
COPY certbot /opt/certbot/src/certbot/
COPY acme /opt/certbot/src/acme/
COPY certbot-apache /opt/certbot/src/certbot-apache/
COPY certbot-nginx /opt/certbot/src/certbot-nginx/
# py26reqs.txt not installed!
RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
/opt/letsencrypt/venv/bin/pip install \
-e /opt/letsencrypt/src/acme \
-e /opt/letsencrypt/src \
-e /opt/letsencrypt/src/letsencrypt-apache \
-e /opt/letsencrypt/src/letsencrypt-nginx
RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv
# PATH is set now so pipstrap upgrades the correct (v)env
ENV PATH /opt/certbot/venv/bin:$PATH
RUN /opt/certbot/venv/bin/python /opt/certbot/src/pipstrap.py && \
/opt/certbot/venv/bin/pip install \
-e /opt/certbot/src/acme \
-e /opt/certbot/src \
-e /opt/certbot/src/certbot-apache \
-e /opt/certbot/src/certbot-nginx
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image);
# "rm -rf /opt/certbot/src" (it's stays in the underlaying image);
# this might also help in debugging: you can "docker run --entrypoint
# bash" and investigate, apply patches, etc.
ENV PATH /opt/letsencrypt/venv/bin:$PATH
ENTRYPOINT [ "letsencrypt" ]
ENTRYPOINT [ "certbot" ]

View file

@ -9,11 +9,11 @@ MAINTAINER Yan <yan@eff.org>
EXPOSE 443
# TODO: make sure --config-dir and --work-dir cannot be changed
# through the CLI (letsencrypt-docker wrapper that uses standalone
# through the CLI (certbot-docker wrapper that uses standalone
# authenticator and text mode only?)
VOLUME /etc/letsencrypt /var/lib/letsencrypt
WORKDIR /opt/letsencrypt
WORKDIR /opt/certbot
# no need to mkdir anything:
# https://docs.docker.com/reference/builder/#copy
@ -22,8 +22,8 @@ WORKDIR /opt/letsencrypt
# TODO: Install non-default Python versions for tox.
# TODO: Install Apache/Nginx for plugin development.
COPY bootstrap/ubuntu.sh /opt/letsencrypt/src/ubuntu.sh
RUN /opt/letsencrypt/src/ubuntu.sh && \
COPY letsencrypt-auto-source/letsencrypt-auto /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto
RUN /opt/certbot/src/letsencrypt-auto-source/letsencrypt-auto --os-packages-only && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* \
/tmp/* \
@ -32,8 +32,7 @@ RUN /opt/letsencrypt/src/ubuntu.sh && \
# the above is not likely to change, so by putting it further up the
# Dockerfile we make sure we cache as much as possible
# py26reqs.txt not installed!
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/letsencrypt/src/
COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh tox.ini pep8.travis.sh .pep8 .pylintrc /opt/certbot/src/
# all above files are necessary for setup.py, however, package source
# code directory has to be copied separately to a subdirectory...
@ -43,27 +42,27 @@ COPY setup.py README.rst CHANGES.rst MANIFEST.in linter_plugin.py tox.cover.sh t
# copied, just its contents." Order again matters, three files are far
# more likely to be cached than the whole project directory
COPY letsencrypt /opt/letsencrypt/src/letsencrypt/
COPY acme /opt/letsencrypt/src/acme/
COPY letsencrypt-apache /opt/letsencrypt/src/letsencrypt-apache/
COPY letsencrypt-nginx /opt/letsencrypt/src/letsencrypt-nginx/
COPY letshelp-letsencrypt /opt/letsencrypt/src/letshelp-letsencrypt/
COPY letsencrypt-compatibility-test /opt/letsencrypt/src/letsencrypt-compatibility-test/
COPY tests /opt/letsencrypt/src/tests/
COPY certbot /opt/certbot/src/certbot/
COPY acme /opt/certbot/src/acme/
COPY certbot-apache /opt/certbot/src/certbot-apache/
COPY certbot-nginx /opt/certbot/src/certbot-nginx/
COPY letshelp-certbot /opt/certbot/src/letshelp-certbot/
COPY certbot-compatibility-test /opt/certbot/src/certbot-compatibility-test/
COPY tests /opt/certbot/src/tests/
RUN virtualenv --no-site-packages -p python2 /opt/letsencrypt/venv && \
/opt/letsencrypt/venv/bin/pip install \
-e /opt/letsencrypt/src/acme \
-e /opt/letsencrypt/src \
-e /opt/letsencrypt/src/letsencrypt-apache \
-e /opt/letsencrypt/src/letsencrypt-nginx \
-e /opt/letsencrypt/src/letshelp-letsencrypt \
-e /opt/letsencrypt/src/letsencrypt-compatibility-test \
-e /opt/letsencrypt/src[dev,docs,testing]
RUN virtualenv --no-site-packages -p python2 /opt/certbot/venv && \
/opt/certbot/venv/bin/pip install \
-e /opt/certbot/src/acme \
-e /opt/certbot/src \
-e /opt/certbot/src/certbot-apache \
-e /opt/certbot/src/certbot-nginx \
-e /opt/certbot/src/letshelp-certbot \
-e /opt/certbot/src/certbot-compatibility-test \
-e /opt/certbot/src[dev,docs]
# install in editable mode (-e) to save space: it's not possible to
# "rm -rf /opt/letsencrypt/src" (it's stays in the underlaying image);
# "rm -rf /opt/certbot/src" (it's stays in the underlaying image);
# this might also help in debugging: you can "docker run --entrypoint
# bash" and investigate, apply patches, etc.
ENV PATH /opt/letsencrypt/venv/bin:$PATH
ENV PATH /opt/certbot/venv/bin:$PATH

View file

@ -1,4 +1,4 @@
Let's Encrypt Python Client
Certbot ACME Client
Copyright (c) Electronic Frontier Foundation and others
Licensed Apache Version 2.0

View file

@ -1,4 +1,3 @@
include py26reqs.txt
include README.rst
include CHANGES.rst
include CONTRIBUTING.md
@ -6,4 +5,4 @@ include LICENSE.txt
include linter_plugin.py
recursive-include docs *
recursive-include examples *
recursive-include letsencrypt/tests/testdata *
recursive-include certbot/tests/testdata *

View file

@ -3,59 +3,80 @@
Disclaimer
==========
The Let's Encrypt Client is **BETA SOFTWARE**. It contains plenty of bugs and
rough edges, and should be tested thoroughly in staging environments before use
on production systems.
Certbot (previously, the Let's Encrypt client) is **BETA SOFTWARE**. It
contains plenty of bugs and rough edges, and should be tested thoroughly in
staging environments before use on production systems.
For more information regarding the status of the project, please see
https://letsencrypt.org. Be sure to checkout the
`Frequently Asked Questions (FAQ) <https://community.letsencrypt.org/t/frequently-asked-questions-faq/26#topic-title>`_.
About the Let's Encrypt Client
About Certbot
==============================
The Let's Encrypt Client is a fully-featured, extensible client for the Let's
Certbot is a fully-featured, extensible client for the Let's
Encrypt CA (or any other CA that speaks the `ACME
<https://github.com/ietf-wg-acme/acme/blob/master/draft-ietf-acme-acme.md>`_
protocol) that can automate the tasks of obtaining certificates and
configuring webservers to use them.
configuring webservers to use them. This client runs on Unix-based operating
systems.
Until May 2016, Certbot was named simply ``letsencrypt`` or ``letsencrypt-auto``,
depending on install method. Instructions on the Internet, and some pieces of the
software, may still refer to this older name.
Contributing
------------
If you'd like to contribute to this project please read `Developer Guide
<https://certbot.eff.org/docs/contributing.html>`_.
.. _installation:
Installation
------------
If ``letsencrypt`` is packaged for your OS, you can install it from there, and
run it by typing ``letsencrypt``. Because not all operating systems have
packages yet, we provide a temporary solution via the ``letsencrypt-auto``
wrapper script, which obtains some dependencies from your OS and puts others
in a python virtual environment::
If ``certbot`` (or ``letsencrypt``) is packaged for your Unix OS (visit
certbot.eff.org_ to find out), you can install it
from there, and run it by typing ``certbot`` (or ``letsencrypt``). Because
not all operating systems have packages yet, we provide a temporary solution
via the ``certbot-auto`` wrapper script, which obtains some dependencies from
your OS and puts others in a python virtual environment::
user@webserver:~$ git clone https://github.com/letsencrypt/letsencrypt
user@webserver:~$ cd letsencrypt
user@webserver:~/letsencrypt$ ./letsencrypt-auto --help
user@webserver:~$ wget https://dl.eff.org/certbot-auto
user@webserver:~$ chmod a+x ./certbot-auto
user@webserver:~$ ./certbot-auto --help
Or for full command line help, type::
.. hint:: The certbot-auto download is protected by HTTPS, which is pretty good, but if you'd like to
double check the integrity of the ``certbot-auto`` script, you can use these steps for verification before running it::
./letsencrypt-auto --help all
user@server:~$ wget -N https://dl.eff.org/certbot-auto.asc
user@server:~$ gpg2 --recv-key A2CFB51FA275A7286234E7B24D17C995CD9775F2
user@server:~$ gpg2 --trusted-key 4D17C995CD9775F2 --verify certbot-auto.asc certbot-auto
``letsencrypt-auto`` updates to the latest client release automatically. And
since ``letsencrypt-auto`` is a wrapper to ``letsencrypt``, it accepts exactly
And for full command line help, you can type::
./certbot-auto --help all
``certbot-auto`` updates to the latest client release automatically. And
since ``certbot-auto`` is a wrapper to ``certbot``, it accepts exactly
the same command line flags and arguments. More details about this script and
other installation methods can be found `in the User Guide
<https://letsencrypt.readthedocs.org/en/latest/using.html#installation>`_.
<https://certbot.eff.org/docs/using.html#installation>`_.
How to run the client
---------------------
In many cases, you can just run ``letsencrypt-auto`` or ``letsencrypt``, and the
In many cases, you can just run ``certbot-auto`` or ``certbot``, and the
client will guide you through the process of obtaining and installing certs
interactively.
You can also tell it exactly what you want it to do from the command line.
For instance, if you want to obtain a cert for ``thing.com``,
``www.thing.com``, and ``otherthing.net``, using the Apache plugin to both
For instance, if you want to obtain a cert for ``example.com``,
``www.example.com``, and ``other.example.net``, using the Apache plugin to both
obtain and install the certs, you could do this::
./letsencrypt-auto --apache -d thing.com -d www.thing.com -d otherthing.net
./certbot-auto --apache -d example.com -d www.example.com -d other.example.net
(The first time you run the command, it will make an account, and ask for an
email and agreement to the Let's Encrypt Subscriber Agreement; you can
@ -64,7 +85,7 @@ automate those with ``--email`` and ``--agree-tos``)
If you want to use a webserver that doesn't have full plugin support yet, you
can still use "standalone" or "webroot" plugins to obtain a certificate::
./letsencrypt-auto certonly --standalone --email admin@thing.com -d thing.com -d www.thing.com -d otherthing.net
./certbot-auto certonly --standalone --email admin@example.com -d example.com -d www.example.com -d other.example.net
Understanding the client in more depth
@ -72,24 +93,29 @@ Understanding the client in more depth
To understand what the client is doing in detail, it's important to
understand the way it uses plugins. Please see the `explanation of
plugins <https://letsencrypt.readthedocs.org/en/latest/using.html#plugins>`_ in
plugins <https://certbot.eff.org/docs/using.html#plugins>`_ in
the User Guide.
Links
=====
Documentation: https://letsencrypt.readthedocs.org
Documentation: https://certbot.eff.org/docs
Software project: https://github.com/letsencrypt/letsencrypt
Software project: https://github.com/certbot/certbot
Notes for developers: https://letsencrypt.readthedocs.org/en/latest/contributing.html
Notes for developers: https://certbot.eff.org/docs/contributing.html
Main Website: https://letsencrypt.org/
IRC Channel: #letsencrypt on `Freenode`_
IRC Channel: #letsencrypt on `Freenode`_ or #certbot on `OFTC`_
Community: https://community.letsencrypt.org
ACME spec: http://ietf-wg-acme.github.io/acme/
ACME working area in github: https://github.com/ietf-wg-acme/acme
Mailing list: `client-dev`_ (to subscribe without a Google account, send an
email to client-dev+subscribe@letsencrypt.org)
@ -97,12 +123,12 @@ email to client-dev+subscribe@letsencrypt.org)
.. |build-status| image:: https://travis-ci.org/letsencrypt/letsencrypt.svg?branch=master
:target: https://travis-ci.org/letsencrypt/letsencrypt
.. |build-status| image:: https://travis-ci.org/certbot/certbot.svg?branch=master
:target: https://travis-ci.org/certbot/certbot
:alt: Travis CI status
.. |coverage| image:: https://coveralls.io/repos/letsencrypt/letsencrypt/badge.svg?branch=master
:target: https://coveralls.io/r/letsencrypt/letsencrypt
.. |coverage| image:: https://coveralls.io/repos/certbot/certbot/badge.svg?branch=master
:target: https://coveralls.io/r/certbot/certbot
:alt: Coverage status
.. |docs| image:: https://readthedocs.org/projects/letsencrypt/badge/
@ -122,16 +148,15 @@ System Requirements
===================
The Let's Encrypt Client presently only runs on Unix-ish OSes that include
Python 2.6 or 2.7; Python 3.x support will be added after the Public Beta
launch. The client requires root access in order to write to
``/etc/letsencrypt``, ``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to
bind to ports 80 and 443 (if you use the ``standalone`` plugin) and to read and
modify webserver configurations (if you use the ``apache`` or ``nginx``
plugins). If none of these apply to you, it is theoretically possible to run
without root privileges, but for most users who want to avoid running an ACME
client as root, either `letsencrypt-nosudo
<https://github.com/diafygi/letsencrypt-nosudo>`_ or `simp_le
<https://github.com/kuba/simp_le>`_ are more appropriate choices.
Python 2.6 or 2.7; Python 3.x support will hopefully be added in the future. The
client requires root access in order to write to ``/etc/letsencrypt``,
``/var/log/letsencrypt``, ``/var/lib/letsencrypt``; to bind to ports 80 and 443
(if you use the ``standalone`` plugin) and to read and modify webserver
configurations (if you use the ``apache`` or ``nginx`` plugins). If none of
these apply to you, it is theoretically possible to run without root privileges,
but for most users who want to avoid running an ACME client as root, either
`letsencrypt-nosudo <https://github.com/diafygi/letsencrypt-nosudo>`_ or
`simp_le <https://github.com/kuba/simp_le>`_ are more appropriate choices.
The Apache plugin currently requires a Debian-based OS with augeas version
1.0; this includes Ubuntu 12.04+ and Debian 7+.
@ -146,10 +171,10 @@ Current Features
- standalone (runs its own simple webserver to prove you control a domain)
- webroot (adds files to webroot directories in order to prove control of
domains and obtain certs)
- nginx/0.8.48+ (highly experimental, not included in letsencrypt-auto)
- nginx/0.8.48+ (highly experimental, not included in certbot-auto)
* The private key is generated locally on your system.
* Can talk to the Let's Encrypt CA or optionally to other ACME
* Can talk to the Let's Encrypt CA or optionally to other ACME
compliant services.
* Can get domain-validated (DV) certificates.
* Can revoke certificates.
@ -163,5 +188,7 @@ Current Features
* Free and Open Source Software, made with Python.
.. _Freenode: https://freenode.net
.. _Freenode: https://webchat.freenode.net?channels=%23letsencrypt
.. _OFTC: https://webchat.oftc.net?channels=%23certbot
.. _client-dev: https://groups.google.com/a/letsencrypt.org/forum/#!forum/client-dev
.. _certbot.eff.org: https://certbot.eff.org/

17
Vagrantfile vendored
View file

@ -5,10 +5,19 @@
VAGRANTFILE_API_VERSION = "2"
# Setup instructions from docs/contributing.rst
# Script installs dependencies for tox and boulder integration
$ubuntu_setup_script = <<SETUP_SCRIPT
cd /vagrant
./bootstrap/install-deps.sh
./bootstrap/dev/venv.sh
./letsencrypt-auto-source/letsencrypt-auto --os-packages-only
./tools/venv.sh
wget https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz -P /tmp/
sudo tar -C /usr/local -xzf /tmp/go1.5.3.linux-amd64.tar.gz
if ! grep -Fxq "export GOROOT=/usr/local/go" /home/vagrant/.profile ; then echo "export GOROOT=/usr/local/go" >> /home/vagrant/.profile; fi
if ! grep -Fxq "export PATH=\\$GOROOT/bin:\\$PATH" /home/vagrant/.profile ; then echo "export PATH=\\$GOROOT/bin:\\$PATH" >> /home/vagrant/.profile; fi
if ! grep -Fxq "export GOPATH=\\$HOME/go" /home/vagrant/.profile ; then echo "export GOPATH=\\$HOME/go" >> /home/vagrant/.profile; fi
if ! grep -Fxq "cd /vagrant/; ./tests/boulder-start.sh &" /etc/rc.local ; then sed -i -e '$i \cd /vagrant/; ./tests/boulder-start.sh &\n' /etc/rc.local; fi
export DEBIAN_FRONTEND=noninteractive
sudo -E apt-get -q -y install git make libltdl-dev mariadb-server rabbitmq-server nginx-light
SETUP_SCRIPT
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
@ -21,6 +30,10 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# Cannot allocate memory" when running
# letsencrypt.client.tests.display.util_test.NcursesDisplayTest
v.memory = 1024
# Handle cases when the host is behind a private network by making the
# NAT engine use the host's resolver mechanisms to handle DNS requests.
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
end
end

4
acme/.pep8 Normal file
View file

@ -0,0 +1,4 @@
[pep8]
# E265 block comment should start with '# '
# E501 line too long (X > 79 characters)
ignore = E265,E501

383
acme/.pylintrc Normal file
View file

@ -0,0 +1,383 @@
[MASTER]
# Specify a configuration file.
#rcfile=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Profiled execution.
profile=no
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Pickle collected data for later comparisons.
persistent=yes
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=linter_plugin
# DEPRECATED
include-ids=no
# DEPRECATED
symbols=no
# Use multiple processes to speed up Pylint.
jobs=1
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time. See also the "--disable" option for examples.
#enable=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=fixme,locally-disabled,abstract-class-not-used
# bstract-class-not-used cannot be disabled locally (at least in
# pylint 1.4.1/2)
[REPORTS]
# Set the output format. Available formats are text, parseable, colorized, msvs
# (visual studio) and html. You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Put messages in a separate file for each module / package specified on the
# command line instead of printing them on stdout. Reports (if any) will be
# written in a file name "pylint_global.[txt|html]".
files-output=no
# Tells whether to display a full report or only the messages
reports=yes
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Add a comment according to your evaluation note. This is used by the global
# evaluation report (RP0004).
comment=no
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
[FORMAT]
# Maximum number of characters on a single line.
max-line-length=80
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
# List of optional constructs for which whitespace checking is disabled
no-space-check=trailing-comma,dict-separator
# Maximum number of lines in a module
max-module-lines=1000
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string=' '
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging,logger
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis
ignored-modules=
# List of classes names for which member attributes should not be checked
# (useful for classes with attributes dynamically set).
ignored-classes=SQLObject
# When zope mode is activated, add a predefined set of Zope acquired attributes
# to generated-members.
zope=no
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E0201 when accessed. Python regular
# expressions are accepted.
generated-members=REQUEST,acl_users,aq_parent
[SIMILARITIES]
# Minimum lines number of a similarity.
min-similarity-lines=4
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
[VARIABLES]
# Tells whether we should check for unused import in __init__ files.
init-import=no
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_$|dummy|unused
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
[BASIC]
# Required attributes for module, separated by a comma
required-attributes=
# List of builtins function names that should not be used, separated by a comma
bad-functions=map,filter,input
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_,logger
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Regular expression matching correct function names
function-rgx=[a-z_][a-z0-9_]{2,40}$
# Naming hint for function names
function-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct variable names
variable-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for variable names
variable-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct attribute names
attr-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for attribute names
attr-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct argument names
argument-rgx=[a-z_][a-z0-9_]{2,30}$
# Naming hint for argument names
argument-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct method names
method-rgx=[a-z_][a-z0-9_]{2,49}$
# Naming hint for method names
method-name-hint=[a-z_][a-z0-9_]{2,30}$
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=__.*__|test_[A-Za-z0-9_]*|_.*|.*Test
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
[CLASSES]
# List of interface methods to ignore, separated by a comma. This is used for
# instance to not check methods defines in Zope's Interface base class.
ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
[DESIGN]
# Maximum number of arguments for function / method
max-args=6
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of statements in function / method body
max-statements=50
# Maximum number of parents for a class (see R0901).
max-parents=12
# Maximum number of attributes for a class (see R0902).
max-attributes=7
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
[IMPORTS]
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=regsub,TERMIOS,Bastion,rexec
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception

View file

@ -1,12 +1,12 @@
"""ACME protocol implementation.
This module is an implementation of the `ACME protocol`_. Latest
supported version: `v02`_.
supported version: `draft-ietf-acme-01`_.
.. _`ACME protocol`: https://github.com/letsencrypt/acme-spec
.. _`v02`:
https://github.com/letsencrypt/acme-spec/commit/d328fea2d507deb9822793c512830d827a4150c4
.. _`ACME protocol`: https://ietf-wg-acme.github.io/acme
.. _`draft-ietf-acme-01`:
https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01
"""

View file

@ -13,7 +13,6 @@ from acme import errors
from acme import crypto_util
from acme import fields
from acme import jose
from acme import other
logger = logging.getLogger(__name__)
@ -36,14 +35,6 @@ class Challenge(jose.TypedJSONObjectWithFields):
return UnrecognizedChallenge.from_json(jobj)
class ContinuityChallenge(Challenge): # pylint: disable=abstract-method
"""Client validation challenges."""
class DVChallenge(Challenge): # pylint: disable=abstract-method
"""Domain validation challenges."""
class ChallengeResponse(jose.TypedJSONObjectWithFields):
# _fields_to_partial_json | pylint: disable=abstract-method
"""ACME challenge response."""
@ -78,8 +69,8 @@ class UnrecognizedChallenge(Challenge):
return cls(jobj)
class _TokenDVChallenge(DVChallenge):
"""DV Challenge with token.
class _TokenChallenge(Challenge):
"""Challenge with token.
:ivar bytes token:
@ -149,7 +140,7 @@ class KeyAuthorizationChallengeResponse(ChallengeResponse):
return True
class KeyAuthorizationChallenge(_TokenDVChallenge):
class KeyAuthorizationChallenge(_TokenChallenge):
# pylint: disable=abstract-class-little-used,too-many-ancestors
"""Challenge based on Key Authorization.
@ -236,10 +227,8 @@ class HTTP01Response(KeyAuthorizationChallengeResponse):
:param challenges.SimpleHTTP chall: Corresponding challenge.
:param unicode domain: Domain name being verified.
:param account_public_key: Public key for the key pair
being authorized. If ``None`` key verification is not
performed!
:param JWK account_public_key:
:param JWK account_public_key: Public key for the key pair
being authorized.
:param int port: Port used in the validation.
:returns: ``True`` iff validation is successful, ``False``
@ -336,7 +325,7 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse):
"""
@property
def z(self):
def z(self): # pylint: disable=invalid-name
"""``z`` value used for verification.
:rtype bytes:
@ -391,7 +380,14 @@ class TLSSNI01Response(KeyAuthorizationChallengeResponse):
return crypto_util.probe_sni(**kwargs)
def verify_cert(self, cert):
"""Verify tls-sni-01 challenge certificate."""
"""Verify tls-sni-01 challenge certificate.
:param OpensSSL.crypto.X509 cert: Challenge certificate.
:returns: Whether the certificate was successfully verified.
:rtype: bool
"""
# pylint: disable=protected-access
sans = crypto_util._pyopenssl_cert_or_req_san(cert)
logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans)
@ -455,108 +451,8 @@ class TLSSNI01(KeyAuthorizationChallenge):
return self.response(account_key).gen_cert(key=kwargs.get('cert_key'))
@Challenge.register
class RecoveryContact(ContinuityChallenge):
"""ACME "recoveryContact" challenge.
:ivar unicode activation_url:
:ivar unicode success_url:
:ivar unicode contact:
"""
typ = "recoveryContact"
activation_url = jose.Field("activationURL", omitempty=True)
success_url = jose.Field("successURL", omitempty=True)
contact = jose.Field("contact", omitempty=True)
@ChallengeResponse.register
class RecoveryContactResponse(ChallengeResponse):
"""ACME "recoveryContact" challenge response.
:ivar unicode token:
"""
typ = "recoveryContact"
token = jose.Field("token", omitempty=True)
@Challenge.register
class ProofOfPossession(ContinuityChallenge):
"""ACME "proofOfPossession" challenge.
:ivar .JWAAlgorithm alg:
:ivar bytes nonce: Random data, **not** base64-encoded.
:ivar hints: Various clues for the client (:class:`Hints`).
"""
typ = "proofOfPossession"
NONCE_SIZE = 16
class Hints(jose.JSONObjectWithFields):
"""Hints for "proofOfPossession" challenge.
:ivar JWK jwk: JSON Web Key
:ivar tuple cert_fingerprints: `tuple` of `unicode`
:ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509`
certificates.
:ivar tuple subject_key_identifiers: `tuple` of `unicode`
:ivar tuple issuers: `tuple` of `unicode`
:ivar tuple authorized_for: `tuple` of `unicode`
"""
jwk = jose.Field("jwk", decoder=jose.JWK.from_json)
cert_fingerprints = jose.Field(
"certFingerprints", omitempty=True, default=())
certs = jose.Field("certs", omitempty=True, default=())
subject_key_identifiers = jose.Field(
"subjectKeyIdentifiers", omitempty=True, default=())
serial_numbers = jose.Field("serialNumbers", omitempty=True, default=())
issuers = jose.Field("issuers", omitempty=True, default=())
authorized_for = jose.Field("authorizedFor", omitempty=True, default=())
@certs.encoder
def certs(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(jose.encode_cert(cert) for cert in value)
@certs.decoder
def certs(value): # pylint: disable=missing-docstring,no-self-argument
return tuple(jose.decode_cert(cert) for cert in value)
alg = jose.Field("alg", decoder=jose.JWASignature.from_json)
nonce = jose.Field(
"nonce", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=NONCE_SIZE))
hints = jose.Field("hints", decoder=Hints.from_json)
@ChallengeResponse.register
class ProofOfPossessionResponse(ChallengeResponse):
"""ACME "proofOfPossession" challenge response.
:ivar bytes nonce: Random data, **not** base64-encoded.
:ivar acme.other.Signature signature: Sugnature of this message.
"""
typ = "proofOfPossession"
NONCE_SIZE = ProofOfPossession.NONCE_SIZE
nonce = jose.Field(
"nonce", encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=NONCE_SIZE))
signature = jose.Field("signature", decoder=other.Signature.from_json)
def verify(self):
"""Verify the challenge."""
# self.signature is not Field | pylint: disable=no-member
return self.signature.verify(self.nonce)
@Challenge.register # pylint: disable=too-many-ancestors
class DNS(_TokenDVChallenge):
class DNS(_TokenChallenge):
"""ACME "dns" challenge."""
typ = "dns"
@ -604,7 +500,7 @@ class DNS(_TokenDVChallenge):
"""
return DNSResponse(validation=self.gen_validation(
self, account_key, **kwargs))
account_key, **kwargs))
def validation_domain_name(self, name):
"""Domain name for TXT validation record.

View file

@ -9,11 +9,10 @@ from six.moves.urllib import parse as urllib_parse # pylint: disable=import-err
from acme import errors
from acme import jose
from acme import other
from acme import test_util
CERT = test_util.load_cert('cert.pem')
CERT = test_util.load_comparable_cert('cert.pem')
KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem'))
@ -73,7 +72,8 @@ class KeyAuthorizationChallengeResponseTest(unittest.TestCase):
def test_verify_wrong_form(self):
from acme.challenges import KeyAuthorizationChallengeResponse
response = KeyAuthorizationChallengeResponse(
key_authorization='.foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY')
key_authorization='.foo.oKGqedy-b-acd5eoybm2f-'
'NVFxvyOoET5CNy3xnv8WY')
self.assertFalse(response.verify(self.chall, KEY.public_key()))
@ -273,10 +273,12 @@ class TLSSNI01ResponseTest(unittest.TestCase):
@mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True)
def test_simple_verify(self, mock_verify_cert):
mock_verify_cert.return_value = mock.sentinel.verification
self.assertEqual(mock.sentinel.verification, self.response.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(self.response, mock.sentinel.cert)
self.assertEqual(
mock.sentinel.verification, self.response.simple_verify(
self.chall, self.domain, KEY.public_key(),
cert=mock.sentinel.cert))
mock_verify_cert.assert_called_once_with(
self.response, mock.sentinel.cert)
@mock.patch('acme.challenges.TLSSNI01Response.probe_cert')
def test_simple_verify_false_on_probe_error(self, mock_probe_cert):
@ -321,233 +323,6 @@ class TLSSNI01Test(unittest.TestCase):
mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key)
class RecoveryContactTest(unittest.TestCase):
def setUp(self):
from acme.challenges import RecoveryContact
self.msg = RecoveryContact(
activation_url='https://example.ca/sendrecovery/a5bd99383fb0',
success_url='https://example.ca/confirmrecovery/bb1b9928932',
contact='c********n@example.com')
self.jmsg = {
'type': 'recoveryContact',
'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0',
'successURL': 'https://example.ca/confirmrecovery/bb1b9928932',
'contact': 'c********n@example.com',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import RecoveryContact
self.assertEqual(self.msg, RecoveryContact.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import RecoveryContact
hash(RecoveryContact.from_json(self.jmsg))
def test_json_without_optionals(self):
del self.jmsg['activationURL']
del self.jmsg['successURL']
del self.jmsg['contact']
from acme.challenges import RecoveryContact
msg = RecoveryContact.from_json(self.jmsg)
self.assertTrue(msg.activation_url is None)
self.assertTrue(msg.success_url is None)
self.assertTrue(msg.contact is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class RecoveryContactResponseTest(unittest.TestCase):
def setUp(self):
from acme.challenges import RecoveryContactResponse
self.msg = RecoveryContactResponse(token='23029d88d9e123e')
self.jmsg = {
'resource': 'challenge',
'type': 'recoveryContact',
'token': '23029d88d9e123e',
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import RecoveryContactResponse
self.assertEqual(
self.msg, RecoveryContactResponse.from_json(self.jmsg))
def test_from_json_hashable(self):
from acme.challenges import RecoveryContactResponse
hash(RecoveryContactResponse.from_json(self.jmsg))
def test_json_without_optionals(self):
del self.jmsg['token']
from acme.challenges import RecoveryContactResponse
msg = RecoveryContactResponse.from_json(self.jmsg)
self.assertTrue(msg.token is None)
self.assertEqual(self.jmsg, msg.to_partial_json())
class ProofOfPossessionHintsTest(unittest.TestCase):
def setUp(self):
jwk = KEY.public_key()
issuers = (
'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA',
'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure',
)
cert_fingerprints = (
'93416768eb85e33adc4277f4c9acd63e7418fcfe',
'16d95b7b63f1972b980b14c20291f3c0d1855d95',
'48b46570d9fc6358108af43ad1649484def0debf',
)
subject_key_identifiers = ('d0083162dcc4c8a23ecb8aecbd86120e56fd24e5')
authorized_for = ('www.example.com', 'example.net')
serial_numbers = (34234239832, 23993939911, 17)
from acme.challenges import ProofOfPossession
self.msg = ProofOfPossession.Hints(
jwk=jwk, issuers=issuers, cert_fingerprints=cert_fingerprints,
certs=(CERT,), subject_key_identifiers=subject_key_identifiers,
authorized_for=authorized_for, serial_numbers=serial_numbers)
self.jmsg_to = {
'jwk': jwk,
'certFingerprints': cert_fingerprints,
'certs': (jose.encode_b64jose(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, CERT)),),
'subjectKeyIdentifiers': subject_key_identifiers,
'serialNumbers': serial_numbers,
'issuers': issuers,
'authorizedFor': authorized_for,
}
self.jmsg_from = self.jmsg_to.copy()
self.jmsg_from.update({'jwk': jwk.to_json()})
def test_to_partial_json(self):
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import ProofOfPossession
self.assertEqual(
self.msg, ProofOfPossession.Hints.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import ProofOfPossession
hash(ProofOfPossession.Hints.from_json(self.jmsg_from))
def test_json_without_optionals(self):
for optional in ['certFingerprints', 'certs', 'subjectKeyIdentifiers',
'serialNumbers', 'issuers', 'authorizedFor']:
del self.jmsg_from[optional]
del self.jmsg_to[optional]
from acme.challenges import ProofOfPossession
msg = ProofOfPossession.Hints.from_json(self.jmsg_from)
self.assertEqual(msg.cert_fingerprints, ())
self.assertEqual(msg.certs, ())
self.assertEqual(msg.subject_key_identifiers, ())
self.assertEqual(msg.serial_numbers, ())
self.assertEqual(msg.issuers, ())
self.assertEqual(msg.authorized_for, ())
self.assertEqual(self.jmsg_to, msg.to_partial_json())
class ProofOfPossessionTest(unittest.TestCase):
def setUp(self):
from acme.challenges import ProofOfPossession
hints = ProofOfPossession.Hints(
jwk=KEY.public_key(), cert_fingerprints=(),
certs=(), serial_numbers=(), subject_key_identifiers=(),
issuers=(), authorized_for=())
self.msg = ProofOfPossession(
alg=jose.RS256, hints=hints,
nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ')
self.jmsg_to = {
'type': 'proofOfPossession',
'alg': jose.RS256,
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'hints': hints,
}
self.jmsg_from = {
'type': 'proofOfPossession',
'alg': jose.RS256.to_json(),
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'hints': hints.to_json(),
}
def test_to_partial_json(self):
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import ProofOfPossession
self.assertEqual(
self.msg, ProofOfPossession.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import ProofOfPossession
hash(ProofOfPossession.from_json(self.jmsg_from))
class ProofOfPossessionResponseTest(unittest.TestCase):
def setUp(self):
# acme-spec uses a confusing example in which both signature
# nonce and challenge nonce are the same, don't make the same
# mistake here...
signature = other.Signature(
alg=jose.RS256, jwk=KEY.public_key(),
sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83'
b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap'
b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde'
b'\x99\x08\xf0\x0e{',
nonce=b'\x99\xc7Q\xb3f2\xbc\xdci\xfe\xd6\x98k\xc67\xdf',
)
from acme.challenges import ProofOfPossessionResponse
self.msg = ProofOfPossessionResponse(
nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ',
signature=signature)
self.jmsg_to = {
'resource': 'challenge',
'type': 'proofOfPossession',
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'signature': signature,
}
self.jmsg_from = {
'resource': 'challenge',
'type': 'proofOfPossession',
'nonce': 'eET5udtV7aoX8Xl8gYiZIA',
'signature': signature.to_json(),
}
def test_verify(self):
self.assertTrue(self.msg.verify())
def test_to_partial_json(self):
self.assertEqual(self.jmsg_to, self.msg.to_partial_json())
def test_from_json(self):
from acme.challenges import ProofOfPossessionResponse
self.assertEqual(
self.msg, ProofOfPossessionResponse.from_json(self.jmsg_from))
def test_from_json_hashable(self):
from acme.challenges import ProofOfPossessionResponse
hash(ProofOfPossessionResponse.from_json(self.jmsg_from))
class DNSTest(unittest.TestCase):
def setUp(self):
@ -590,7 +365,8 @@ class DNSTest(unittest.TestCase):
def test_check_validation_wrong_fields(self):
bad_validation = jose.JWS.sign(
payload=self.msg.update(token=b'x' * 20).json_dumps().encode('utf-8'),
payload=self.msg.update(
token=b'x' * 20).json_dumps().encode('utf-8'),
alg=jose.RS256, key=KEY)
self.assertFalse(self.msg.check_validation(
bad_validation, KEY.public_key()))

View file

@ -1,5 +1,7 @@
"""ACME client API."""
import collections
import datetime
from email.utils import parsedate_tz
import heapq
import logging
import time
@ -10,7 +12,6 @@ from six.moves import http_client # pylint: disable=import-error
import OpenSSL
import requests
import sys
import werkzeug
from acme import errors
from acme import jose
@ -20,7 +21,9 @@ from acme import messages
logger = logging.getLogger(__name__)
# Python does not validate certificates by default before version 2.7.9
# Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure
# many important security related options. On these platforms we use PyOpenSSL
# for SSL, which does allow these options to be configured.
# https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning
if sys.version_info < (2, 7, 9): # pragma: no cover
requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3()
@ -64,15 +67,13 @@ class Client(object): # pylint: disable=too-many-instance-attributes
@classmethod
def _regr_from_response(cls, response, uri=None, new_authzr_uri=None,
terms_of_service=None):
terms_of_service = (
response.links['terms-of-service']['url']
if 'terms-of-service' in response.links else terms_of_service)
if 'terms-of-service' in response.links:
terms_of_service = response.links['terms-of-service']['url']
if 'next' in response.links:
new_authzr_uri = response.links['next']['url']
if new_authzr_uri is None:
try:
new_authzr_uri = response.links['next']['url']
except KeyError:
raise errors.ClientError('"next" link missing')
raise errors.ClientError('"next" link missing')
return messages.RegistrationResource(
body=messages.Registration.from_json(response.json()),
@ -179,40 +180,41 @@ class Client(object): # pylint: disable=too-many-instance-attributes
raise errors.UnexpectedUpdate(authzr)
return authzr
def request_challenges(self, identifier, new_authzr_uri):
def request_challenges(self, identifier, new_authzr_uri=None):
"""Request challenges.
:param identifier: Identifier to be challenged.
:type identifier: `.messages.Identifier`
:param str new_authzr_uri: new-authorization URI
:param .messages.Identifier identifier: Identifier to be challenged.
:param str new_authzr_uri: ``new-authorization`` URI. If omitted,
will default to value found in ``directory``.
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
"""
new_authz = messages.NewAuthorization(identifier=identifier)
response = self.net.post(new_authzr_uri, new_authz)
response = self.net.post(self.directory.new_authz
if new_authzr_uri is None else new_authzr_uri,
new_authz)
# TODO: handle errors
assert response.status_code == http_client.CREATED
return self._authzr_from_response(response, identifier)
def request_domain_challenges(self, domain, new_authz_uri):
def request_domain_challenges(self, domain, new_authzr_uri=None):
"""Request challenges for domain names.
This is simply a convenience function that wraps around
`request_challenges`, but works with domain names instead of
generic identifiers.
generic identifiers. See ``request_challenges`` for more
documentation.
:param str domain: Domain name to be challenged.
:param str new_authzr_uri: new-authorization URI
:returns: Authorization Resource.
:rtype: `.AuthorizationResource`
"""
return self.request_challenges(messages.Identifier(
typ=messages.IDENTIFIER_FQDN, value=domain), new_authz_uri)
typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri)
def answer_challenge(self, challb, response):
"""Answer challenge.
@ -246,6 +248,9 @@ class Client(object): # pylint: disable=too-many-instance-attributes
def retry_after(cls, response, default):
"""Compute next `poll` time based on response ``Retry-After`` header.
Handles integers and various datestring formats per
https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37
:param requests.Response response: Response from `poll`.
:param int default: Default value (in seconds), used when
``Retry-After`` header is not present or invalid.
@ -258,12 +263,16 @@ class Client(object): # pylint: disable=too-many-instance-attributes
try:
seconds = int(retry_after)
except ValueError:
# pylint: disable=no-member
decoded = werkzeug.parse_date(retry_after) # RFC1123
if decoded is None:
seconds = default
else:
return decoded
# The RFC 2822 parser handles all of RFC 2616's cases in modern
# environments (primarily HTTP 1.1+ but also py27+)
when = parsedate_tz(retry_after)
if when is not None:
try:
tz_secs = datetime.timedelta(when[-1] if when[-1] else 0)
return datetime.datetime(*when[:7]) - tz_secs
except (ValueError, OverflowError):
pass
seconds = default
return datetime.datetime.now() + datetime.timedelta(seconds=seconds)
@ -334,11 +343,12 @@ class Client(object): # pylint: disable=too-many-instance-attributes
:param authzrs: `list` of `.AuthorizationResource`
:param int mintime: Minimum time before next attempt, used if
``Retry-After`` is not present in the response.
:param int max_attempts: Maximum number of attempts before
`PollError` with non-empty ``waiting`` is raised.
:param int max_attempts: Maximum number of attempts (per
authorization) before `PollError` with non-empty ``waiting``
is raised.
:returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is
the issued certificate (`.messages.CertificateResource.),
the issued certificate (`.messages.CertificateResource`),
and ``updated_authzrs`` is a `tuple` consisting of updated
Authorization Resources (`.AuthorizationResource`) as
present in the responses from server, and in the same order
@ -349,15 +359,19 @@ class Client(object): # pylint: disable=too-many-instance-attributes
was marked by the CA as invalid
"""
# priority queue with datetime (based on Retry-After) as key,
# pylint: disable=too-many-locals
assert max_attempts > 0
attempts = collections.defaultdict(int)
exhausted = set()
# priority queue with datetime.datetime (based on Retry-After) as key,
# and original Authorization Resource as value
waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs]
# mapping between original Authorization Resource and the most
# recently updated one
updated = dict((authzr, authzr) for authzr in authzrs)
while waiting and max_attempts:
max_attempts -= 1
while waiting:
# find the smallest Retry-After, and sleep if necessary
when, authzr = heapq.heappop(waiting)
now = datetime.datetime.now()
@ -371,16 +385,20 @@ class Client(object): # pylint: disable=too-many-instance-attributes
updated_authzr, response = self.poll(updated[authzr])
updated[authzr] = updated_authzr
attempts[authzr] += 1
# pylint: disable=no-member
if updated_authzr.body.status not in (
messages.STATUS_VALID, messages.STATUS_INVALID):
# push back to the priority queue, with updated retry_after
heapq.heappush(waiting, (self.retry_after(
response, default=mintime), authzr))
if attempts[authzr] < max_attempts:
# push back to the priority queue, with updated retry_after
heapq.heappush(waiting, (self.retry_after(
response, default=mintime), authzr))
else:
exhausted.add(authzr)
if not max_attempts or any(authzr.body.status == messages.STATUS_INVALID
for authzr in six.itervalues(updated)):
raise errors.PollError(waiting, updated)
if exhausted or any(authzr.body.status == messages.STATUS_INVALID
for authzr in six.itervalues(updated)):
raise errors.PollError(exhausted, updated)
updated_authzrs = tuple(updated[authzr] for authzr in authzrs)
return self.request_issuance(csr, updated_authzrs), updated_authzrs
@ -481,7 +499,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
'Successful revocation must return HTTP OK status')
class ClientNetwork(object):
class ClientNetwork(object): # pylint: disable=too-many-instance-attributes
"""Client network."""
JSON_CONTENT_TYPE = 'application/json'
JSON_ERROR_CONTENT_TYPE = 'application/problem+json'
@ -494,6 +512,10 @@ class ClientNetwork(object):
self.verify_ssl = verify_ssl
self._nonces = set()
self.user_agent = user_agent
self.session = requests.Session()
def __del__(self):
self.session.close()
def _wrap_in_jws(self, obj, nonce):
"""Wrap `JSONDeSerializable` object in JWS.
@ -537,7 +559,7 @@ class ClientNetwork(object):
# TODO: response.json() is called twice, once here, and
# once in _get and _post clients
jobj = response.json()
except ValueError as error:
except ValueError:
jobj = None
if not response.ok:
@ -588,7 +610,7 @@ class ClientNetwork(object):
kwargs['verify'] = self.verify_ssl
kwargs.setdefault('headers', {})
kwargs['headers'].setdefault('User-Agent', self.user_agent)
response = requests.request(method, url, *args, **kwargs)
response = self.session.request(method, url, *args, **kwargs)
logging.debug('Received %s. Headers: %s. Content: %r',
response, response.headers, response.content)
return response

View file

@ -34,8 +34,12 @@ class ClientTest(unittest.TestCase):
self.net.get.return_value = self.response
self.directory = messages.Directory({
messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg',
messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert',
messages.NewRegistration:
'https://www.letsencrypt-demo.org/acme/new-reg',
messages.Revocation:
'https://www.letsencrypt-demo.org/acme/revoke-cert',
messages.NewAuthorization:
'https://www.letsencrypt-demo.org/acme/new-authz',
})
from acme.client import Client
@ -127,13 +131,20 @@ class ClientTest(unittest.TestCase):
self.response.json.return_value = self.regr.body.to_json()
self.assertEqual(self.regr, self.client.query_registration(self.regr))
def test_query_registration_updates_new_authzr_uri(self):
self.response.json.return_value = self.regr.body.to_json()
self.response.links = {'next': {'url': 'UPDATED'}}
self.assertEqual(
'UPDATED',
self.client.query_registration(self.regr).new_authzr_uri)
def test_agree_to_tos(self):
self.client.update_registration = mock.Mock()
self.client.agree_to_tos(self.regr)
regr = self.client.update_registration.call_args[0][0]
self.assertEqual(self.regr.terms_of_service, regr.body.agreement)
def test_request_challenges(self):
def _prepare_response_for_request_challenges(self):
self.response.status_code = http_client.CREATED
self.response.headers['Location'] = self.authzr.uri
self.response.json.return_value = self.authz.to_json()
@ -141,10 +152,20 @@ class ClientTest(unittest.TestCase):
'next': {'url': self.authzr.new_cert_uri},
}
self.client.request_challenges(self.identifier, self.authzr.uri)
# TODO: test POST call arguments
def test_request_challenges(self):
self._prepare_response_for_request_challenges()
self.client.request_challenges(self.identifier)
self.net.post.assert_called_once_with(
self.directory.new_authz,
messages.NewAuthorization(identifier=self.identifier))
# TODO: split here and separate test
def test_requets_challenges_custom_uri(self):
self._prepare_response_for_request_challenges()
self.client.request_challenges(self.identifier, 'URI')
self.net.post.assert_called_once_with('URI', mock.ANY)
def test_request_challenges_unexpected_update(self):
self._prepare_response_for_request_challenges()
self.response.json.return_value = self.authz.update(
identifier=self.identifier.update(value='foo')).to_json()
self.assertRaises(
@ -153,15 +174,20 @@ class ClientTest(unittest.TestCase):
def test_request_challenges_missing_next(self):
self.response.status_code = http_client.CREATED
self.assertRaises(
errors.ClientError, self.client.request_challenges,
self.identifier, self.regr)
self.assertRaises(errors.ClientError, self.client.request_challenges,
self.identifier)
def test_request_domain_challenges(self):
self.client.request_challenges = mock.MagicMock()
self.assertEqual(
self.client.request_challenges(self.identifier),
self.client.request_domain_challenges('example.com', self.regr))
self.client.request_domain_challenges('example.com'))
def test_request_domain_challenges_custom_uri(self):
self.client.request_challenges = mock.MagicMock()
self.assertEqual(
self.client.request_challenges(self.identifier, 'URI'),
self.client.request_domain_challenges('example.com', 'URI'))
def test_answer_challenge(self):
self.response.links['up'] = {'url': self.challr.authzr_uri}
@ -196,6 +222,17 @@ class ClientTest(unittest.TestCase):
datetime.datetime(2015, 3, 27, 0, 0, 10),
self.client.retry_after(response=self.response, default=10))
@mock.patch('acme.client.datetime')
def test_retry_after_overflow(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
dt_mock.timedelta = datetime.timedelta
dt_mock.datetime.side_effect = datetime.datetime
self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST"
self.assertEqual(
datetime.datetime(2015, 3, 27, 0, 0, 10),
self.client.retry_after(response=self.response, default=10))
@mock.patch('acme.client.datetime')
def test_retry_after_seconds(self, dt_mock):
dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27)
@ -310,7 +347,10 @@ class ClientTest(unittest.TestCase):
)
cert, updated_authzrs = self.client.poll_and_request_issuance(
csr, authzrs, mintime=mintime)
csr, authzrs, mintime=mintime,
# make sure that max_attempts is per-authorization, rather
# than global
max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries)))
self.assertTrue(cert[0] is csr)
self.assertTrue(cert[1] is updated_authzrs)
self.assertEqual(updated_authzrs[0].uri, 'a...')
@ -331,7 +371,8 @@ class ClientTest(unittest.TestCase):
self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7))
# CA sets invalid | TODO: move to a separate test
invalid_authzr = mock.MagicMock(times=[], retries=[messages.STATUS_INVALID])
invalid_authzr = mock.MagicMock(
times=[], retries=[messages.STATUS_INVALID])
self.assertRaises(
errors.PollError, self.client.poll_and_request_issuance,
csr, authzrs=(invalid_authzr,), mintime=mintime)
@ -443,9 +484,11 @@ class ClientNetworkTest(unittest.TestCase):
def test_check_response_not_ok_jobj_no_error(self):
self.response.ok = False
self.response.json.return_value = {}
# pylint: disable=protected-access
self.assertRaises(
errors.ClientError, self.net._check_response, self.response)
with mock.patch('acme.client.messages.Error.from_json') as from_json:
from_json.side_effect = jose.DeserializationError
# pylint: disable=protected-access
self.assertRaises(
errors.ClientError, self.net._check_response, self.response)
def test_check_response_not_ok_jobj_error(self):
self.response.ok = False
@ -487,40 +530,49 @@ class ClientNetworkTest(unittest.TestCase):
self.assertEqual(
self.response, self.net._check_response(self.response))
@mock.patch('acme.client.requests')
def test_send_request(self, mock_requests):
mock_requests.request.return_value = self.response
def test_send_request(self):
self.net.session = mock.MagicMock()
self.net.session.request.return_value = self.response
# pylint: disable=protected-access
self.assertEqual(self.response, self.net._send_request(
'HEAD', 'url', 'foo', bar='baz'))
mock_requests.request.assert_called_once_with(
'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY)
'HEAD', 'http://example.com/', 'foo', bar='baz'))
self.net.session.request.assert_called_once_with(
'HEAD', 'http://example.com/', 'foo',
headers=mock.ANY, verify=mock.ANY, bar='baz')
@mock.patch('acme.client.requests')
def test_send_request_verify_ssl(self, mock_requests):
def test_send_request_verify_ssl(self):
# pylint: disable=protected-access
for verify in True, False:
mock_requests.request.reset_mock()
mock_requests.request.return_value = self.response
self.net.session = mock.MagicMock()
self.net.session.request.return_value = self.response
self.net.verify_ssl = verify
# pylint: disable=protected-access
self.assertEqual(
self.response, self.net._send_request('GET', 'url'))
mock_requests.request.assert_called_once_with(
'GET', 'url', verify=verify, headers=mock.ANY)
self.response,
self.net._send_request('GET', 'http://example.com/'))
self.net.session.request.assert_called_once_with(
'GET', 'http://example.com/', verify=verify, headers=mock.ANY)
@mock.patch('acme.client.requests')
def test_send_request_user_agent(self, mock_requests):
mock_requests.request.return_value = self.response
def test_send_request_user_agent(self):
self.net.session = mock.MagicMock()
# pylint: disable=protected-access
self.net._send_request('GET', 'url', headers={'bar': 'baz'})
mock_requests.request.assert_called_once_with(
'GET', 'url', verify=mock.ANY,
self.net._send_request('GET', 'http://example.com/',
headers={'bar': 'baz'})
self.net.session.request.assert_called_once_with(
'GET', 'http://example.com/', verify=mock.ANY,
headers={'User-Agent': 'acme-python-test', 'bar': 'baz'})
self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'})
mock_requests.request.assert_called_with(
'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'})
self.net._send_request('GET', 'http://example.com/',
headers={'User-Agent': 'foo2'})
self.net.session.request.assert_called_with(
'GET', 'http://example.com/',
verify=mock.ANY, headers={'User-Agent': 'foo2'})
def test_del(self):
sess = mock.MagicMock()
self.net.session = sess
del self.net
sess.close.assert_called_once_with()
@mock.patch('acme.client.requests')
def test_requests_error_passthrough(self, mock_requests):
@ -573,14 +625,16 @@ class ClientNetworkWithMockedResponseTest(unittest.TestCase):
return self.checked_response
def test_head(self):
self.assertEqual(self.response, self.net.head('url', 'foo', bar='baz'))
self.assertEqual(self.response, self.net.head(
'http://example.com/', 'foo', bar='baz'))
self.send_request.assert_called_once_with(
'HEAD', 'url', 'foo', bar='baz')
'HEAD', 'http://example.com/', 'foo', bar='baz')
def test_get(self):
self.assertEqual(self.checked_response, self.net.get(
'url', content_type=self.content_type, bar='baz'))
self.send_request.assert_called_once_with('GET', 'url', bar='baz')
'http://example.com/', content_type=self.content_type, bar='baz'))
self.send_request.assert_called_once_with(
'GET', 'http://example.com/', bar='baz')
def test_post(self):
# pylint: disable=protected-access

View file

@ -1,11 +1,11 @@
"""Crypto utilities."""
import binascii
import contextlib
import logging
import re
import socket
import sys
from six.moves import range # pylint: disable=import-error,redefined-builtin
import OpenSSL
from acme import errors
@ -70,7 +70,7 @@ class SSLSocket(object): # pylint: disable=too-few-public-methods
class FakeConnection(object):
"""Fake OpenSSL.SSL.Connection."""
# pylint: disable=missing-docstring
# pylint: disable=too-few-public-methods,missing-docstring
def __init__(self, connection):
self._wrapped = connection
@ -161,31 +161,31 @@ def _pyopenssl_cert_or_req_san(cert_or_req):
:rtype: `list` of `unicode`
"""
# constants based on implementation of
# OpenSSL.crypto.X509Error._subjectAltNameString
parts_separator = ", "
# This function finds SANs by dumping the certificate/CSR to text and
# searching for "X509v3 Subject Alternative Name" in the text. This method
# is used to support PyOpenSSL version 0.13 where the
# `_subjectAltNameString` and `get_extensions` methods are not available
# for CSRs.
# constants based on PyOpenSSL certificate/CSR text dump
part_separator = ":"
extension_short_name = b"subjectAltName"
parts_separator = ", "
prefix = "DNS" + part_separator
if hasattr(cert_or_req, 'get_extensions'): # X509Req
extensions = cert_or_req.get_extensions()
else: # X509
extensions = [cert_or_req.get_extension(i)
for i in range(cert_or_req.get_extension_count())]
# pylint: disable=protected-access,no-member
label = OpenSSL.crypto.X509Extension._prefixes[OpenSSL.crypto._lib.GEN_DNS]
assert parts_separator not in label
prefix = label + part_separator
san_extensions = [
ext._subjectAltNameString().split(parts_separator)
for ext in extensions if ext.get_short_name() == extension_short_name]
if isinstance(cert_or_req, OpenSSL.crypto.X509):
func = OpenSSL.crypto.dump_certificate
else:
func = OpenSSL.crypto.dump_certificate_request
text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8")
# WARNING: this function does not support multiple SANs extensions.
# Multiple X509v3 extensions of the same type is disallowed by RFC 5280.
match = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text)
# WARNING: this function assumes that no SAN can include
# parts_separator, hence the split!
sans_parts = [] if match is None else match.group(1).split(parts_separator)
return [part.split(part_separator)[1] for parts in san_extensions
for part in parts if part.startswith(prefix)]
return [part.split(part_separator)[1]
for part in sans_parts if part.startswith(prefix)]
def gen_ss_cert(key, domains, not_before=None,
@ -204,7 +204,7 @@ def gen_ss_cert(key, domains, not_before=None,
"""
assert domains, "Must provide one or more hostnames for the cert."
cert = OpenSSL.crypto.X509()
cert.set_serial_number(1337)
cert.set_serial_number(int(binascii.hexlify(OpenSSL.rand.bytes(16)), 16))
cert.set_version(2)
extensions = [

View file

@ -1,11 +1,15 @@
"""Tests for acme.crypto_util."""
import itertools
import socket
import threading
import time
import unittest
import six
from six.moves import socketserver # pylint: disable=import-error
import OpenSSL
from acme import errors
from acme import jose
from acme import test_util
@ -15,10 +19,10 @@ class SSLSocketAndProbeSNITest(unittest.TestCase):
"""Tests for acme.crypto_util.SSLSocket/probe_sni."""
def setUp(self):
self.cert = test_util.load_cert('cert.pem')
self.cert = test_util.load_comparable_cert('cert.pem')
key = test_util.load_pyopenssl_private_key('rsa512_key.pem')
# pylint: disable=protected-access
certs = {b'foo': (key, self.cert._wrapped)}
certs = {b'foo': (key, self.cert.wrapped)}
from acme.crypto_util import SSLSocket
@ -69,6 +73,15 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
from acme.crypto_util import _pyopenssl_cert_or_req_san
return _pyopenssl_cert_or_req_san(loader(name))
@classmethod
def _get_idn_names(cls):
"""Returns expected names from '{cert,csr}-idnsans.pem'."""
chars = [six.unichr(i) for i in itertools.chain(range(0x3c3, 0x400),
range(0x641, 0x6fc),
range(0x1820, 0x1877))]
return [''.join(chars[i: i + 45]) + '.invalid'
for i in range(0, len(chars), 45)]
def _call_cert(self, name):
return self._call(test_util.load_cert, name)
@ -82,6 +95,14 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
self.assertEqual(self._call_cert('cert-san.pem'),
['example.com', 'www.example.com'])
def test_cert_hundred_sans(self):
self.assertEqual(self._call_cert('cert-100sans.pem'),
['example{0}.com'.format(i) for i in range(1, 101)])
def test_cert_idn_sans(self):
self.assertEqual(self._call_cert('cert-idnsans.pem'),
self._get_idn_names())
def test_csr_no_sans(self):
self.assertEqual(self._call_csr('csr-nosans.pem'), [])
@ -94,10 +115,36 @@ class PyOpenSSLCertOrReqSANTest(unittest.TestCase):
def test_csr_six_sans(self):
self.assertEqual(self._call_csr('csr-6sans.pem'),
["example.com", "example.org", "example.net",
"example.info", "subdomain.example.com",
"other.subdomain.example.com"])
['example.com', 'example.org', 'example.net',
'example.info', 'subdomain.example.com',
'other.subdomain.example.com'])
def test_csr_hundred_sans(self):
self.assertEqual(self._call_csr('csr-100sans.pem'),
['example{0}.com'.format(i) for i in range(1, 101)])
def test_csr_idn_sans(self):
self.assertEqual(self._call_csr('csr-idnsans.pem'),
self._get_idn_names())
if __name__ == "__main__":
class RandomSnTest(unittest.TestCase):
"""Test for random certificate serial numbers."""
def setUp(self):
self.cert_count = 5
self.serial_num = []
self.key = OpenSSL.crypto.PKey()
self.key.generate_key(OpenSSL.crypto.TYPE_RSA, 2048)
def test_sn_collisions(self):
from acme.crypto_util import gen_ss_cert
for _ in range(self.cert_count):
cert = gen_ss_cert(self.key, ['dummy'], force_san=True)
self.serial_num.append(cert.get_serial_number())
self.assertTrue(len(set(self.serial_num)) > 1)
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -56,26 +56,25 @@ class MissingNonce(NonceError):
class PollError(ClientError):
"""Generic error when polling for authorization fails.
This might be caused by either timeout (`waiting` will be non-empty)
This might be caused by either timeout (`exhausted` will be non-empty)
or by some authorization being invalid.
:ivar waiting: Priority queue with `datetime.datatime` (based on
``Retry-After``) as key, and original `.AuthorizationResource`
as value.
:ivar exhausted: Set of `.AuthorizationResource` that didn't finish
within max allowed attempts.
:ivar updated: Mapping from original `.AuthorizationResource`
to the most recently updated one
"""
def __init__(self, waiting, updated):
self.waiting = waiting
def __init__(self, exhausted, updated):
self.exhausted = exhausted
self.updated = updated
super(PollError, self).__init__()
@property
def timeout(self):
"""Was the error caused by timeout?"""
return bool(self.waiting)
return bool(self.exhausted)
def __repr__(self):
return '{0}(waiting={1!r}, updated={2!r})'.format(
self.__class__.__name__, self.waiting, self.updated)
return '{0}(exhausted={1!r}, updated={2!r})'.format(
self.__class__.__name__, self.exhausted, self.updated)

View file

@ -1,5 +1,4 @@
"""Tests for acme.errors."""
import datetime
import unittest
import mock
@ -36,9 +35,9 @@ class PollErrorTest(unittest.TestCase):
def setUp(self):
from acme.errors import PollError
self.timeout = PollError(
waiting=[(datetime.datetime(2015, 11, 29), mock.sentinel.AR)],
exhausted=set([mock.sentinel.AR]),
updated={})
self.invalid = PollError(waiting=[], updated={
self.invalid = PollError(exhausted=set(), updated={
mock.sentinel.AR: mock.sentinel.AR2})
def test_timeout(self):
@ -46,8 +45,8 @@ class PollErrorTest(unittest.TestCase):
self.assertFalse(self.invalid.timeout)
def test_repr(self):
self.assertEqual('PollError(waiting=[], updated={sentinel.AR: '
'sentinel.AR2})', repr(self.invalid))
self.assertEqual('PollError(exhausted=%s, updated={sentinel.AR: '
'sentinel.AR2})' % repr(set()), repr(self.invalid))
if __name__ == "__main__":

View file

@ -226,7 +226,7 @@ class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
:param str name: Name of the field to be encoded.
:raises erors.SerializationError: if field cannot be serialized
:raises errors.SerializationError: if field cannot be serialized
:raises errors.Error: if field could not be found
"""
@ -373,7 +373,7 @@ def encode_cert(cert):
"""
return encode_b64jose(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert))
OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped))
def decode_cert(b64der):
@ -398,7 +398,7 @@ def encode_csr(csr):
"""
return encode_b64jose(OpenSSL.crypto.dump_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, csr))
OpenSSL.crypto.FILETYPE_ASN1, csr.wrapped))
def decode_csr(b64der):

View file

@ -12,8 +12,8 @@ from acme.jose import interfaces
from acme.jose import util
CERT = test_util.load_cert('cert.pem')
CSR = test_util.load_csr('csr.pem')
CERT = test_util.load_comparable_cert('cert.pem')
CSR = test_util.load_comparable_csr('csr.pem')
class FieldTest(unittest.TestCase):

View file

@ -124,7 +124,7 @@ class Header(json_util.JSONObjectWithFields):
@x5c.encoder
def x5c(value): # pylint: disable=missing-docstring,no-self-argument
return [base64.b64encode(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, cert)) for cert in value]
OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value]
@x5c.decoder
def x5c(value): # pylint: disable=missing-docstring,no-self-argument

View file

@ -13,7 +13,7 @@ from acme.jose import jwa
from acme.jose import jwk
CERT = test_util.load_cert('cert.pem')
CERT = test_util.load_comparable_cert('cert.pem')
KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
@ -68,13 +68,12 @@ class HeaderTest(unittest.TestCase):
from acme.jose.jws import Header
header = Header(x5c=(CERT, CERT))
jobj = header.to_partial_json()
cert_b64 = base64.b64encode(OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, CERT))
cert_asn1 = OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped)
cert_b64 = base64.b64encode(cert_asn1)
self.assertEqual(jobj, {'x5c': [cert_b64, cert_b64]})
self.assertEqual(header, Header.from_json(jobj))
jobj['x5c'][0] = base64.b64encode(
b'xxx' + OpenSSL.crypto.dump_certificate(
OpenSSL.crypto.FILETYPE_ASN1, CERT))
jobj['x5c'][0] = base64.b64encode(b'xxx' + cert_asn1)
self.assertRaises(errors.DeserializationError, Header.from_json, jobj)
def test_find_key(self):

View file

@ -29,32 +29,41 @@ class abstractclassmethod(classmethod):
class ComparableX509(object): # pylint: disable=too-few-public-methods
"""Wrapper for OpenSSL.crypto.X509** objects that supports __eq__.
Wraps around:
- :class:`OpenSSL.crypto.X509`
- :class:`OpenSSL.crypto.X509Req`
:ivar wrapped: Wrapped certificate or certificate request.
:type wrapped: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`.
"""
def __init__(self, wrapped):
assert isinstance(wrapped, OpenSSL.crypto.X509) or isinstance(
wrapped, OpenSSL.crypto.X509Req)
self._wrapped = wrapped
self.wrapped = wrapped
def __getattr__(self, name):
return getattr(self._wrapped, name)
return getattr(self.wrapped, name)
def _dump(self, filetype=OpenSSL.crypto.FILETYPE_ASN1):
# pylint: disable=missing-docstring,protected-access
if isinstance(self._wrapped, OpenSSL.crypto.X509):
"""Dumps the object into a buffer with the specified encoding.
:param int filetype: The desired encoding. Should be one of
`OpenSSL.crypto.FILETYPE_ASN1`,
`OpenSSL.crypto.FILETYPE_PEM`, or
`OpenSSL.crypto.FILETYPE_TEXT`.
:returns: Encoded X509 object.
:rtype: str
"""
if isinstance(self.wrapped, OpenSSL.crypto.X509):
func = OpenSSL.crypto.dump_certificate
else: # assert in __init__ makes sure this is X509Req
func = OpenSSL.crypto.dump_certificate_request
return func(filetype, self._wrapped)
return func(filetype, self.wrapped)
def __eq__(self, other):
if not isinstance(other, self.__class__):
return NotImplemented
return self._dump() == other._dump() # pylint: disable=protected-access
# pylint: disable=protected-access
return self._dump() == other._dump()
def __hash__(self):
return hash((self.__class__, self._dump()))
@ -63,7 +72,7 @@ class ComparableX509(object): # pylint: disable=too-few-public-methods
return not self == other
def __repr__(self):
return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped)
return '<{0}({1!r})>'.format(self.__class__.__name__, self.wrapped)
class ComparableKey(object): # pylint: disable=too-few-public-methods
@ -130,7 +139,7 @@ class ImmutableMap(collections.Mapping, collections.Hashable):
"""Immutable key to value mapping with attribute access."""
__slots__ = ()
"""Must be overriden in subclasses."""
"""Must be overridden in subclasses."""
def __init__(self, **kwargs):
if set(kwargs) != set(self.__slots__):

View file

@ -11,14 +11,17 @@ class ComparableX509Test(unittest.TestCase):
"""Tests for acme.jose.util.ComparableX509."""
def setUp(self):
# test_util.load_{csr,cert} return ComparableX509
self.req1 = test_util.load_csr('csr.pem')
self.req2 = test_util.load_csr('csr.pem')
self.req_other = test_util.load_csr('csr-san.pem')
# test_util.load_comparable_{csr,cert} return ComparableX509
self.req1 = test_util.load_comparable_csr('csr.pem')
self.req2 = test_util.load_comparable_csr('csr.pem')
self.req_other = test_util.load_comparable_csr('csr-san.pem')
self.cert1 = test_util.load_cert('cert.pem')
self.cert2 = test_util.load_cert('cert.pem')
self.cert_other = test_util.load_cert('cert-san.pem')
self.cert1 = test_util.load_comparable_cert('cert.pem')
self.cert2 = test_util.load_comparable_cert('cert.pem')
self.cert_other = test_util.load_comparable_cert('cert-san.pem')
def test_getattr_proxy(self):
self.assertTrue(self.cert1.has_expired())
def test_eq(self):
self.assertEqual(self.req1, self.req2)
@ -41,8 +44,8 @@ class ComparableX509Test(unittest.TestCase):
def test_repr(self):
for x509 in self.req1, self.cert1:
self.assertTrue(repr(x509).startswith(
'<ComparableX509(<OpenSSL.crypto.X509'))
self.assertEqual(repr(x509),
'<ComparableX509({0!r})>'.format(x509.wrapped))
class ComparableRSAKeyTest(unittest.TestCase):

View file

@ -22,20 +22,24 @@ class Error(jose.JSONObjectWithFields, errors.Error):
('urn:acme:error:' + name, description) for name, description in (
('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'),
('badNonce', 'The client sent an unacceptable anti-replay nonce'),
('connection', 'The server could not connect to the client for DV'),
('connection', 'The server could not connect to the client to '
'verify the domain'),
('dnssec', 'The server could not validate a DNSSEC signed domain'),
('invalidEmail',
'The provided email for a registration was invalid'),
('malformed', 'The request message was malformed'),
('rateLimited', 'There were too many requests of a given type'),
('serverInternal', 'The server experienced an internal error'),
('tls', 'The server experienced a TLS error during DV'),
('tls', 'The server experienced a TLS error during domain '
'verification'),
('unauthorized', 'The client lacks sufficient authorization'),
('unknownHost', 'The server could not resolve a domain name'),
)
)
typ = jose.Field('type')
typ = jose.Field('type', omitempty=True, default='about:blank')
title = jose.Field('title', omitempty=True)
detail = jose.Field('detail')
detail = jose.Field('detail', omitempty=True)
@property
def description(self):
@ -119,6 +123,12 @@ class Directory(jose.JSONDeSerializable):
_REGISTERED_TYPES = {}
class Meta(jose.JSONObjectWithFields):
"""Directory Meta."""
terms_of_service = jose.Field('terms-of-service', omitempty=True)
website = jose.Field('website', omitempty=True)
caa_identities = jose.Field('caa-identities', omitempty=True)
@classmethod
def _canon_key(cls, key):
return getattr(key, 'resource_type', key)
@ -126,17 +136,13 @@ class Directory(jose.JSONDeSerializable):
@classmethod
def register(cls, resource_body_cls):
"""Register resource."""
assert resource_body_cls.resource_type not in cls._REGISTERED_TYPES
cls._REGISTERED_TYPES[resource_body_cls.resource_type] = resource_body_cls
resource_type = resource_body_cls.resource_type
assert resource_type not in cls._REGISTERED_TYPES
cls._REGISTERED_TYPES[resource_type] = resource_body_cls
return resource_body_cls
def __init__(self, jobj):
canon_jobj = util.map_keys(jobj, self._canon_key)
if not set(canon_jobj).issubset(self._REGISTERED_TYPES):
# TODO: acme-spec is not clear about this: 'It is a JSON
# dictionary, whose keys are the "resource" values listed
# in {{https-requests}}'z
raise ValueError('Wrong directory fields')
# TODO: check that everything is an absolute URL; acme-spec is
# not clear on that
self._jobj = canon_jobj
@ -158,10 +164,8 @@ class Directory(jose.JSONDeSerializable):
@classmethod
def from_json(cls, jobj):
try:
return cls(jobj)
except ValueError as error:
raise jose.DeserializationError(str(error))
jobj['meta'] = cls.Meta.from_json(jobj.pop('meta', {}))
return cls(jobj)
class Resource(jose.JSONObjectWithFields):

View file

@ -8,8 +8,8 @@ from acme import jose
from acme import test_util
CERT = test_util.load_cert('cert.der')
CSR = test_util.load_csr('csr.der')
CERT = test_util.load_comparable_cert('cert.der')
CSR = test_util.load_comparable_csr('csr.der')
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
@ -28,6 +28,14 @@ class ErrorTest(unittest.TestCase):
self.error_custom = Error(typ='custom', detail='bar')
self.jobj_cusom = {'type': 'custom', 'detail': 'bar'}
def test_default_typ(self):
from acme.messages import Error
self.assertEqual(Error().typ, 'about:blank')
def test_from_json_empty(self):
from acme.messages import Error
self.assertEqual(Error(), Error.from_json('{}'))
def test_from_json_hashable(self):
from acme.messages import Error
hash(Error.from_json(self.error.to_json()))
@ -90,11 +98,16 @@ class DirectoryTest(unittest.TestCase):
self.dir = Directory({
'new-reg': 'reg',
mock.MagicMock(resource_type='new-cert'): 'cert',
'meta': Directory.Meta(
terms_of_service='https://example.com/acme/terms',
website='https://www.example.com/',
caa_identities=['example.com'],
),
})
def test_init_wrong_key_value_error(self):
def test_init_wrong_key_value_success(self): # pylint: disable=no-self-use
from acme.messages import Directory
self.assertRaises(ValueError, Directory, {'foo': 'bar'})
Directory({'foo': 'bar'})
def test_getitem(self):
self.assertEqual('reg', self.dir['new-reg'])
@ -111,14 +124,20 @@ class DirectoryTest(unittest.TestCase):
def test_getattr_fails_with_attribute_error(self):
self.assertRaises(AttributeError, self.dir.__getattr__, 'foo')
def test_to_partial_json(self):
self.assertEqual(
self.dir.to_partial_json(), {'new-reg': 'reg', 'new-cert': 'cert'})
def test_to_json(self):
self.assertEqual(self.dir.to_json(), {
'new-reg': 'reg',
'new-cert': 'cert',
'meta': {
'terms-of-service': 'https://example.com/acme/terms',
'website': 'https://www.example.com/',
'caa-identities': ['example.com'],
},
})
def test_from_json_deserialization_error_on_wrong_key(self):
def test_from_json_deserialization_unknown_key_success(self): # pylint: disable=no-self-use
from acme.messages import Directory
self.assertRaises(
jose.DeserializationError, Directory.from_json, {'foo': 'bar'})
Directory.from_json({'foo': 'bar'})
class RegistrationTest(unittest.TestCase):
@ -271,10 +290,8 @@ class AuthorizationTest(unittest.TestCase):
ChallengeBody(uri='http://challb2', status=STATUS_VALID,
chall=challenges.DNS(
token=b'DGyRejmCefe7v4NfDGDKfA')),
ChallengeBody(uri='http://challb3', status=STATUS_VALID,
chall=challenges.RecoveryContact()),
)
combinations = ((0, 2), (1, 2))
combinations = ((0,), (1,))
from acme.messages import Authorization
from acme.messages import Identifier
@ -300,8 +317,8 @@ class AuthorizationTest(unittest.TestCase):
def test_resolved_combinations(self):
self.assertEqual(self.authz.resolved_combinations, (
(self.challbs[0], self.challbs[2]),
(self.challbs[1], self.challbs[2]),
(self.challbs[0],),
(self.challbs[1],),
))

View file

@ -1,67 +0,0 @@
"""Other ACME objects."""
import functools
import logging
import os
from acme import jose
logger = logging.getLogger(__name__)
class Signature(jose.JSONObjectWithFields):
"""ACME signature.
:ivar .JWASignature alg: Signature algorithm.
:ivar bytes sig: Signature.
:ivar bytes nonce: Nonce.
:ivar .JWK jwk: JWK.
"""
NONCE_SIZE = 16
"""Minimum size of nonce in bytes."""
alg = jose.Field('alg', decoder=jose.JWASignature.from_json)
sig = jose.Field('sig', encoder=jose.encode_b64jose,
decoder=jose.decode_b64jose)
nonce = jose.Field(
'nonce', encoder=jose.encode_b64jose, decoder=functools.partial(
jose.decode_b64jose, size=NONCE_SIZE, minimum=True))
jwk = jose.Field('jwk', decoder=jose.JWK.from_json)
@classmethod
def from_msg(cls, msg, key, nonce=None, nonce_size=None, alg=jose.RS256):
"""Create signature with nonce prepended to the message.
:param bytes msg: Message to be signed.
:param key: Key used for signing.
:type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey`
(optionally wrapped in `.ComparableRSAKey`).
:param bytes nonce: Nonce to be used. If None, nonce of
``nonce_size`` will be randomly generated.
:param int nonce_size: Size of the automatically generated nonce.
Defaults to :const:`NONCE_SIZE`.
:param .JWASignature alg:
"""
nonce_size = cls.NONCE_SIZE if nonce_size is None else nonce_size
nonce = os.urandom(nonce_size) if nonce is None else nonce
msg_with_nonce = nonce + msg
sig = alg.sign(key, nonce + msg)
logger.debug('%r signed as %r', msg_with_nonce, sig)
return cls(alg=alg, sig=sig, nonce=nonce,
jwk=alg.kty(key=key.public_key()))
def verify(self, msg):
"""Verify the signature.
:param bytes msg: Message that was used in signing.
"""
# self.alg is not Field, but JWA | pylint: disable=no-member
return self.alg.verify(self.jwk.key, self.nonce + msg, self.sig)

View file

@ -1,94 +0,0 @@
"""Tests for acme.sig."""
import unittest
from acme import jose
from acme import test_util
KEY = test_util.load_rsa_private_key('rsa512_key.pem')
class SignatureTest(unittest.TestCase):
# pylint: disable=too-many-instance-attributes
"""Tests for acme.sig.Signature."""
def setUp(self):
self.msg = b'message'
self.sig = (b'IC\xd8*\xe7\x14\x9e\x19S\xb7\xcf\xec3\x12\xe2\x8a\x03'
b'\x98u\xff\xf0\x94\xe2\xd7<\x8f\xa8\xed\xa4KN\xc3\xaa'
b'\xb9X\xc3w\xaa\xc0_\xd0\x05$y>l#\x10<\x96\xd2\xcdr\xa3'
b'\x1b\xa1\xf5!f\xef\xc64\xb6\x13')
self.nonce = b'\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9'
self.alg = jose.RS256
self.jwk = jose.JWKRSA(key=KEY.public_key())
b64sig = ('SUPYKucUnhlTt8_sMxLiigOYdf_wlOLXPI-o7aRLTsOquVjDd6r'
'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew')
b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ'
self.jsig_to = {
'nonce': b64nonce,
'alg': self.alg,
'jwk': self.jwk,
'sig': b64sig,
}
self.jsig_from = {
'nonce': b64nonce,
'alg': self.alg.to_partial_json(),
'jwk': self.jwk.to_partial_json(),
'sig': b64sig,
}
from acme.other import Signature
self.signature = Signature(
alg=self.alg, sig=self.sig, nonce=self.nonce, jwk=self.jwk)
def test_attributes(self):
self.assertEqual(self.signature.nonce, self.nonce)
self.assertEqual(self.signature.alg, self.alg)
self.assertEqual(self.signature.sig, self.sig)
self.assertEqual(self.signature.jwk, self.jwk)
def test_verify_good_succeeds(self):
self.assertTrue(self.signature.verify(self.msg))
def test_verify_bad_fails(self):
self.assertFalse(self.signature.verify(self.msg + b'x'))
@classmethod
def _from_msg(cls, *args, **kwargs):
from acme.other import Signature
return Signature.from_msg(*args, **kwargs)
def test_create_from_msg(self):
signature = self._from_msg(self.msg, KEY, self.nonce)
self.assertEqual(self.signature, signature)
def test_create_from_msg_random_nonce(self):
signature = self._from_msg(self.msg, KEY)
self.assertEqual(signature.alg, self.alg)
self.assertEqual(signature.jwk, self.jwk)
self.assertTrue(signature.verify(self.msg))
def test_to_partial_json(self):
self.assertEqual(self.signature.to_partial_json(), self.jsig_to)
def test_from_json(self):
from acme.other import Signature
self.assertEqual(
self.signature, Signature.from_json(self.jsig_from))
def test_from_json_non_schema_errors(self):
from acme.other import Signature
jwk = self.jwk.to_partial_json()
self.assertRaises(
jose.DeserializationError, Signature.from_json, {
'alg': 'RS256', 'sig': 'x', 'nonce': '', 'jwk': jwk})
self.assertRaises(
jose.DeserializationError, Signature.from_json, {
'alg': 'RS256', 'sig': '', 'nonce': 'x', 'jwk': jwk})
if __name__ == '__main__':
unittest.main() # pragma: no cover

View file

@ -32,11 +32,10 @@ class TLSSNI01ServerTest(unittest.TestCase):
"""Test for acme.standalone.TLSSNI01Server."""
def setUp(self):
self.certs = {
b'localhost': (test_util.load_pyopenssl_private_key('rsa512_key.pem'),
# pylint: disable=protected-access
test_util.load_cert('cert.pem')._wrapped),
}
self.certs = {b'localhost': (
test_util.load_pyopenssl_private_key('rsa512_key.pem'),
test_util.load_cert('cert.pem'),
)}
from acme.standalone import TLSSNI01Server
self.server = TLSSNI01Server(("", 0), certs=self.certs)
# pylint: disable=no-member
@ -49,7 +48,8 @@ class TLSSNI01ServerTest(unittest.TestCase):
def test_it(self):
host, port = self.server.socket.getsockname()[:2]
cert = crypto_util.probe_sni(b'localhost', host=host, port=port, timeout=1)
cert = crypto_util.probe_sni(
b'localhost', host=host, port=port, timeout=1)
self.assertEqual(jose.ComparableX509(cert),
jose.ComparableX509(self.certs[b'localhost'][1]))
@ -140,13 +140,14 @@ class TestSimpleTLSSNI01Server(unittest.TestCase):
while max_attempts:
max_attempts -= 1
try:
cert = crypto_util.probe_sni(b'localhost', b'0.0.0.0', self.port)
cert = crypto_util.probe_sni(
b'localhost', b'0.0.0.0', self.port)
except errors.Error:
self.assertTrue(max_attempts > 0, "Timeout!")
time.sleep(1) # wait until thread starts
else:
self.assertEqual(jose.ComparableX509(cert),
test_util.load_cert('cert.pem'))
test_util.load_comparable_cert('cert.pem'))
break

View file

@ -40,16 +40,24 @@ def load_cert(*names):
"""Load certificate."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return jose.ComparableX509(OpenSSL.crypto.load_certificate(
loader, load_vector(*names)))
return OpenSSL.crypto.load_certificate(loader, load_vector(*names))
def load_comparable_cert(*names):
"""Load ComparableX509 cert."""
return jose.ComparableX509(load_cert(*names))
def load_csr(*names):
"""Load certificate request."""
loader = _guess_loader(
names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1)
return jose.ComparableX509(OpenSSL.crypto.load_certificate_request(
loader, load_vector(*names)))
return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names))
def load_comparable_csr(*names):
"""Load ComparableX509 certificate request."""
return jose.ComparableX509(load_csr(*names))
def load_rsa_private_key(*names):

44
acme/acme/testdata/cert-100sans.pem vendored Normal file
View file

@ -0,0 +1,44 @@
-----BEGIN CERTIFICATE-----
MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv
bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X
DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU
BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp
ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B
AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580
rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T
BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t
ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt
cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j
b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN
ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh
bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs
ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx
LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv
bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN
ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh
bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs
ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3
LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv
bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN
ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh
bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs
ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz
LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv
bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN
ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh
bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs
ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5
LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv
bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN
ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh
bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs
ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1
LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv
bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN
ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh
bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs
ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN
AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ
XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG
-----END CERTIFICATE-----

30
acme/acme/testdata/cert-idnsans.pem vendored Normal file
View file

@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV
BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv
bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X
DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU
BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp
ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B
AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580
rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T
BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I
z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g
z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z
z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM
2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf
2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3
2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi
2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi
2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs
aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN
247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p
bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4
27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt
4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh
oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh
oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh
oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm
4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2
LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT
TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G
-----END CERTIFICATE-----

41
acme/acme/testdata/csr-100sans.pem vendored Normal file
View file

@ -0,0 +1,41 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz
Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG
A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt
H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6
lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud
EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv
bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh
bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu
Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C
DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4
YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w
bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy
MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j
b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C
DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4
YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w
bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz
Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j
b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C
DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4
YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w
bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1
My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j
b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C
DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4
YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w
bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2
OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j
b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C
DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4
YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w
bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4
NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j
b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C
DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4
YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w
bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3
DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo
duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg==
-----END CERTIFICATE REQUEST-----

27
acme/acme/testdata/csr-idnsans.pem vendored Normal file
View file

@ -0,0 +1,27 @@
-----BEGIN CERTIFICATE REQUEST-----
MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz
Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG
A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt
H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6
lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud
EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP
iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P
oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP
s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ
jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z
n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ
t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC
YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa
otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh
bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb
jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu
aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb
uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg
reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5
4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ
4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ
4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh
puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh
ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr
dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng==
-----END CERTIFICATE REQUEST-----

View file

@ -28,8 +28,7 @@ acme = client.Client(DIRECTORY_URL, key)
regr = acme.register()
logging.info('Auto-accepting TOS: %s', regr.terms_of_service)
acme.update_registration(regr.update(
body=regr.body.update(agreement=regr.terms_of_service)))
acme.agree_to_tos(regr)
logging.debug(regr)
authzr = acme.request_challenges(
@ -43,7 +42,7 @@ csr = OpenSSL.crypto.load_certificate_request(
OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string(
'acme', os.path.join('testdata', 'csr.der')))
try:
acme.request_issuance(csr, (authzr,))
acme.request_issuance(jose.util.ComparableX509(csr), (authzr,))
except messages.Error as error:
print ("This script is doomed to fail as no authorization "
"challenges are ever solved. Error from server: {0}".format(error))

2
acme/setup.cfg Normal file
View file

@ -0,0 +1,2 @@
[bdist_wheel]
universal = 1

View file

@ -4,25 +4,28 @@ from setuptools import setup
from setuptools import find_packages
version = '0.2.0.dev0'
version = '0.8.0.dev0'
# Please update tox.ini when modifying dependency version requirements
install_requires = [
# load_pem_private/public_key (>=0.6)
# rsa_recover_prime_factors (>=0.8)
'cryptography>=0.8',
'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304)
'pyasn1', # urllib3 InsecurePlatformWarning (#304)
# Connection.set_tlsext_host_name (>=0.13), X509Req.get_extensions (>=0.15)
'PyOpenSSL>=0.15',
# Connection.set_tlsext_host_name (>=0.13)
'PyOpenSSL>=0.13',
'pyrfc3339',
'pytz',
'requests',
'setuptools', # pkg_resources
# For pkg_resources. >=1.0 so pip resolves it to a version cryptography
# will tolerate; see #2599:
'setuptools>=1.0',
'six',
'werkzeug',
]
# env markers in extras_require cause problems with older pip: #517
# Keep in sync with conditional_requirements.py.
if sys.version_info < (2, 7):
install_requires.extend([
# only some distros recognize stdlib argparse as already satisfying
@ -32,24 +35,25 @@ if sys.version_info < (2, 7):
else:
install_requires.append('mock')
dev_extras = [
'nose',
'pep8',
'tox',
]
docs_extras = [
'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags
'sphinx_rtd_theme',
'sphinxcontrib-programoutput',
]
testing_extras = [
'nose',
'tox',
]
setup(
name='acme',
version=version,
description='ACME protocol implementation in Python',
url='https://github.com/letsencrypt/letsencrypt',
author="Let's Encrypt Project",
author="Certbot Project",
author_email='client-dev@letsencrypt.org',
license='Apache License 2.0',
classifiers=[
@ -63,6 +67,7 @@ setup(
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Topic :: Internet :: WWW/HTTP',
'Topic :: Security',
],
@ -71,8 +76,8 @@ setup(
include_package_data=True,
install_requires=install_requires,
extras_require={
'dev': dev_extras,
'docs': docs_extras,
'testing': testing_extras,
},
entry_points={
'console_scripts': [

View file

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

View file

@ -1,27 +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="
git
python2
python-virtualenv
gcc
dialog
augeas
openssl
libffi
ca-certificates
pkg-config
"
missing=$(pacman -T $deps)
if [ "$missing" ]; then
pacman -S --needed $missing
fi

View file

@ -1,50 +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 ; then
virtualenv="virtualenv"
fi
if apt-cache show python-virtualenv > /dev/null ; then
virtualenv="$virtualenv python-virtualenv"
fi
apt-get install -y --no-install-recommends \
git \
python \
python-dev \
$virtualenv \
gcc \
dialog \
libaugeas0 \
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-vcs/git
dev-lang/python:2.7
dev-python/virtualenv
dev-util/dialog
app-admin/augeas
dev-libs/openssl
dev-libs/libffi
app-misc/ca-certificates
virtual/pkgconfig"
case "$PACKAGE_MANAGER" in
(paludis)
cave resolve --keep-targets if-possible $PACKAGES -x
;;
(pkgcore)
pmerge --noreplace $PACKAGES
;;
(portage|*)
emerge --noreplace $PACKAGES
;;
esac

View file

@ -1,49 +0,0 @@
#!/bin/sh
# Tested with:
# - Fedora 22, 23 (x64)
# - Centos 7 (x64: onD igitalOcean droplet)
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
then
if ! $tool install -y \
python27 \
python27-devel \
python27-virtualenv
then
echo "Could not install Python dependencies. Aborting bootstrap!"
exit 1
fi
fi
# "git-core" seems to be an alias for "git" in CentOS 7 (yum search fails)
if ! $tool install -y \
git-core \
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

View file

@ -1,14 +0,0 @@
#!/bin/sh
# SLE12 don't have python-virtualenv
zypper -nq in -l git-core \
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,13 +0,0 @@
#!/bin/sh -xe
# Developer virtualenv setup for Let's Encrypt client
export VENV_ARGS="--python python2"
./bootstrap/dev/_venv_common.sh \
-r py26reqs.txt \
-e acme[testing] \
-e .[dev,docs,testing] \
-e letsencrypt-apache \
-e letsencrypt-nginx \
-e letshelp-letsencrypt \
-e letsencrypt-compatibility-test

View file

@ -1,8 +0,0 @@
#!/bin/sh -xe
# Developer Python3 virtualenv setup for Let's Encrypt
export VENV_NAME="${VENV_NAME:-venv3}"
export VENV_ARGS="--python python3"
./bootstrap/dev/_venv_common.sh \
-e acme[testing] \

View file

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

View file

@ -1,8 +0,0 @@
#!/bin/sh -xe
pkg install -Ay \
git \
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 -r py26reqs.txt 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

@ -0,0 +1,7 @@
include LICENSE.txt
include README.rst
recursive-include docs *
recursive-include certbot_apache/tests/testdata *
include certbot_apache/centos-options-ssl-apache.conf
include certbot_apache/options-ssl-apache.conf
recursive-include certbot_apache/augeas_lens *.aug

View file

@ -0,0 +1 @@
Apache plugin for Certbot

View file

@ -0,0 +1 @@
"""Certbot Apache plugin."""

View file

@ -3,11 +3,11 @@ import logging
import augeas
from letsencrypt import errors
from letsencrypt import reverter
from letsencrypt.plugins import common
from certbot import errors
from certbot import reverter
from certbot.plugins import common
from letsencrypt_apache import constants
from certbot_apache import constants
logger = logging.getLogger(__name__)
@ -16,14 +16,14 @@ class AugeasConfigurator(common.Plugin):
"""Base Augeas Configurator class.
:ivar config: Configuration.
:type config: :class:`~letsencrypt.interfaces.IConfig`
:type config: :class:`~certbot.interfaces.IConfig`
:ivar aug: Augeas object
:type aug: :class:`augeas.Augeas`
:ivar str save_notes: Human-readable configuration change notes
:ivar reverter: saves and reverts checkpoints
:type reverter: :class:`letsencrypt.reverter.Reverter`
:type reverter: :class:`certbot.reverter.Reverter`
"""
def __init__(self, *args, **kwargs):
@ -120,7 +120,8 @@ class AugeasConfigurator(common.Plugin):
self.reverter.add_to_temp_checkpoint(
save_files, self.save_notes)
else:
self.reverter.add_to_checkpoint(save_files, self.save_notes)
self.reverter.add_to_checkpoint(save_files,
self.save_notes)
except errors.ReverterError as err:
raise errors.PluginError(str(err))

View file

@ -0,0 +1,2 @@
Certbot includes the very latest Augeas lenses in order to ship bug fixes
to Apache configuration handling bugs as quickly as possible

View file

@ -45,13 +45,12 @@ autoload xfm
let dels (s:string) = del s s
(* deal with continuation lines *)
let sep_spc = del /([ \t]+|[ \t]*\\\\\r?\n[ \t]*)/ " "
let sep_osp = Sep.opt_space
let sep_spc = del /([ \t]+|[ \t]*\\\\\r?\n[ \t]*)+/ " "
let sep_osp = del /([ \t]*|[ \t]*\\\\\r?\n[ \t]*)*/ ""
let sep_eq = del /[ \t]*=[ \t]*/ "="
let nmtoken = /[a-zA-Z:_][a-zA-Z0-9:_.-]*/
let word = /[a-zA-Z][a-zA-Z0-9._-]*/
let word = /[a-z][a-z0-9._-]*/i
let comment = Util.comment
let eol = Util.doseol
@ -59,13 +58,18 @@ let empty = Util.empty_dos
let indent = Util.indent
(* borrowed from shellvars.aug *)
let char_arg_dir = /([^\\ '"\t\r\n]|[^\\ '"\t\r\n][^ '"\t\r\n]*[^\\ '"\t\r\n])|\\\\"|\\\\'/
let char_arg_sec = /[^ '"\t\r\n>]|\\\\"|\\\\'/
let char_arg_dir = /([^\\ '"{\t\r\n]|[^ '"{\t\r\n]+[^\\ \t\r\n])|\\\\"|\\\\'|\\\\ /
let char_arg_sec = /([^\\ '"\t\r\n>]|[^ '"\t\r\n>]+[^\\ \t\r\n>])|\\\\"|\\\\'|\\\\ /
let char_arg_wl = /([^\\ '"},\t\r\n]|[^ '"},\t\r\n]+[^\\ '"},\t\r\n])/
let cdot = /\\\\./
let cl = /\\\\\n/
let dquot =
let no_dquot = /[^"\\\r\n]/
in /"/ . (no_dquot|cdot|cl)* . /"/
let dquot_msg =
let no_dquot = /([^ \t"\\\r\n]|[^"\\\r\n]+[^ \t"\\\r\n])/
in /"/ . (no_dquot|cdot|cl)*
let squot =
let no_squot = /[^'\\\r\n]/
in /'/ . (no_squot|cdot|cl)* . /'/
@ -76,12 +80,24 @@ let comp = /[<>=]?=/
*****************************************************************)
let arg_dir = [ label "arg" . store (char_arg_dir+|dquot|squot) ]
(* message argument starts with " but ends at EOL *)
let arg_dir_msg = [ label "arg" . store dquot_msg ]
let arg_sec = [ label "arg" . store (char_arg_sec+|comp|dquot|squot) ]
let arg_wl = [ label "arg" . store (char_arg_wl+|dquot|squot) ]
(* comma-separated wordlist as permitted in the SSLRequire directive *)
let arg_wordlist =
let wl_start = Util.del_str "{" in
let wl_end = Util.del_str "}" in
let wl_sep = del /[ \t]*,[ \t]*/ ", "
in [ label "wordlist" . wl_start . arg_wl . (wl_sep . arg_wl)* . wl_end ]
let argv (l:lens) = l . (sep_spc . l)*
let directive = [ indent . label "directive" . store word .
(sep_spc . argv arg_dir)? . eol ]
let directive =
(* arg_dir_msg may be the last or only argument *)
let dir_args = (argv (arg_dir|arg_wordlist) . (sep_spc . arg_dir_msg)?) | arg_dir_msg
in [ indent . label "directive" . store word . (sep_spc . dir_args)? . eol ]
let section (body:lens) =
(* opt_eol includes empty lines *)
@ -89,11 +105,17 @@ let section (body:lens) =
let inner = (sep_spc . argv arg_sec)? . sep_osp .
dels ">" . opt_eol . ((body|comment) . (body|empty|comment)*)? .
indent . dels "</" in
let kword = key word in
let dword = del word "a" in
[ indent . dels "<" . square kword inner dword . del ">" ">" . eol ]
let kword = key (word - /perl/i) in
let dword = del (word - /perl/i) "a" in
[ indent . dels "<" . square kword inner dword . del />[ \t\n\r]*/ ">\n" ]
let perl_section = [ indent . label "Perl" . del /<perl>/i "<Perl>"
. store /[^<]*/
. del /<\/perl>/i "</Perl>" . eol ]
let rec content = section (content|directive)
| perl_section
let lns = (content|directive|comment|empty)*
@ -104,6 +126,7 @@ let filter = (incl "/etc/apache2/apache2.conf") .
(incl "/etc/apache2/conf-available/*.conf") .
(incl "/etc/apache2/mods-available/*") .
(incl "/etc/apache2/sites-available/*") .
(incl "/etc/apache2/vhosts.d/*.conf") .
(incl "/etc/httpd/conf.d/*.conf") .
(incl "/etc/httpd/httpd.conf") .
(incl "/etc/httpd/conf/httpd.conf") .

View file

@ -0,0 +1,21 @@
# Baseline setting to Include for SSL sites
SSLEngine on
# Intermediate configuration, tweak to your needs
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
SSLHonorCipherOrder on
SSLOptions +StrictRequire
# Add vhost name to log entries:
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
#CustomLog /var/log/apache2/access.log vhost_combined
#LogLevel warn
#ErrorLog /var/log/apache2/error.log
# Always ensure Cookies have "Secure" set (JAH 2012/1)
#Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"

View file

@ -0,0 +1,127 @@
"""Apache plugin constants."""
import pkg_resources
from certbot import util
CLI_DEFAULTS_DEBIAN = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/sites-available",
vhost_files="*",
version_cmd=['apache2ctl', '-v'],
define_cmd=['apache2ctl', '-t', '-D', 'DUMP_RUN_CFG'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod="a2enmod",
dismod="a2dismod",
le_vhost_ext="-le-ssl.conf",
handle_mods=True,
handle_sites=True,
challenge_location="/etc/apache2",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
)
CLI_DEFAULTS_CENTOS = dict(
server_root="/etc/httpd",
vhost_root="/etc/httpd/conf.d",
vhost_files="*.conf",
version_cmd=['apachectl', '-v'],
define_cmd=['apachectl', '-t', '-D', 'DUMP_RUN_CFG'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_sites=False,
challenge_location="/etc/httpd/conf.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "centos-options-ssl-apache.conf")
)
CLI_DEFAULTS_GENTOO = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/vhosts.d",
vhost_files="*.conf",
version_cmd=['/usr/sbin/apache2', '-v'],
define_cmd=['apache2ctl', 'virtualhosts'],
restart_cmd=['apache2ctl', 'graceful'],
conftest_cmd=['apache2ctl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_sites=False,
challenge_location="/etc/apache2/vhosts.d",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
)
CLI_DEFAULTS_DARWIN = dict(
server_root="/etc/apache2",
vhost_root="/etc/apache2/other",
vhost_files="*.conf",
version_cmd=['/usr/sbin/httpd', '-v'],
define_cmd=['/usr/sbin/httpd', '-t', '-D', 'DUMP_RUN_CFG'],
restart_cmd=['apachectl', 'graceful'],
conftest_cmd=['apachectl', 'configtest'],
enmod=None,
dismod=None,
le_vhost_ext="-le-ssl.conf",
handle_mods=False,
handle_sites=False,
challenge_location="/etc/apache2/other",
MOD_SSL_CONF_SRC=pkg_resources.resource_filename(
"certbot_apache", "options-ssl-apache.conf")
)
CLI_DEFAULTS = {
"debian": CLI_DEFAULTS_DEBIAN,
"ubuntu": CLI_DEFAULTS_DEBIAN,
"centos": CLI_DEFAULTS_CENTOS,
"centos linux": CLI_DEFAULTS_CENTOS,
"fedora": CLI_DEFAULTS_CENTOS,
"red hat enterprise linux server": CLI_DEFAULTS_CENTOS,
"rhel": CLI_DEFAULTS_CENTOS,
"amazon": CLI_DEFAULTS_CENTOS,
"gentoo": CLI_DEFAULTS_GENTOO,
"gentoo base system": CLI_DEFAULTS_GENTOO,
"darwin": CLI_DEFAULTS_DARWIN,
}
"""CLI defaults."""
MOD_SSL_CONF_DEST = "options-ssl-apache.conf"
"""Name of the mod_ssl config file as saved in `IConfig.config_dir`."""
AUGEAS_LENS_DIR = pkg_resources.resource_filename(
"certbot_apache", "augeas_lens")
"""Path to the Augeas lens directory"""
REWRITE_HTTPS_ARGS = [
"^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[L,QSA,R=permanent]"]
"""Apache version<2.3.9 rewrite rule arguments used for redirections to
https vhost"""
REWRITE_HTTPS_ARGS_WITH_END = [
"^", "https://%{SERVER_NAME}%{REQUEST_URI}", "[END,QSA,R=permanent]"]
"""Apache version >= 2.3.9 rewrite rule arguments used for redirections to
https vhost"""
HSTS_ARGS = ["always", "set", "Strict-Transport-Security",
"\"max-age=31536000\""]
"""Apache header arguments for HSTS"""
UIR_ARGS = ["always", "set", "Content-Security-Policy",
"upgrade-insecure-requests"]
HEADER_ARGS = {"Strict-Transport-Security": HSTS_ARGS,
"Upgrade-Insecure-Requests": UIR_ARGS}
def os_constant(key):
"""Get a constant value for operating system
:param key: name of cli constant
:return: value of constant for active os
"""
os_info = util.get_os_info()
try:
constants = CLI_DEFAULTS[os_info[0].lower()]
except KeyError:
constants = CLI_DEFAULTS["debian"]
return constants[key]

View file

@ -4,9 +4,10 @@ import os
import zope.component
from letsencrypt import interfaces
from certbot import errors
from certbot import interfaces
import letsencrypt.display.util as display_util
import certbot.display.util as display_util
logger = logging.getLogger(__name__)
@ -49,7 +50,7 @@ def _vhost_menu(domain, vhosts):
if free_chars < 2:
logger.debug("Display size is too small for "
"letsencrypt_apache.display_ops._vhost_menu()")
"certbot_apache.display_ops._vhost_menu()")
# This runs the edge off the screen, but it doesn't cause an "error"
filename_size = 1
disp_name_size = 1
@ -78,11 +79,19 @@ def _vhost_menu(domain, vhosts):
name_size=disp_name_size)
)
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
"We were unable to find a vhost with a ServerName or Address of {0}.{1}"
"Which virtual host would you like to choose?".format(
domain, os.linesep),
choices, help_label="More Info", ok_label="Select")
try:
code, tag = zope.component.getUtility(interfaces.IDisplay).menu(
"We were unable to find a vhost with a ServerName "
"or Address of {0}.{1}Which virtual host would you "
"like to choose?\n(note: conf files with multiple "
"vhosts are not yet supported)".format(domain, os.linesep),
choices, help_label="More Info", ok_label="Select")
except errors.MissingCommandlineFlag as e:
msg = ("Failed to run Apache plugin non-interactively{1}{0}{1}"
"(The best solution is to add ServerName or ServerAlias "
"entries to the VirtualHost directives of your apache "
"configuration files.)".format(e, os.linesep))
raise errors.MissingCommandlineFlag(msg)
return code, tag

View file

@ -1,7 +1,7 @@
"""Module contains classes used by the Apache Configurator."""
import re
from letsencrypt.plugins import common
from certbot.plugins import common
class Addr(common.Addr):
@ -189,22 +189,29 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
return True
return False
def same_server(self, vhost):
def same_server(self, vhost, generic=False):
"""Determines if the vhost is the same 'server'.
Used in redirection - indicates whether or not the two virtual hosts
serve on the exact same IP combinations, but different ports.
The generic flag indicates that that we're trying to match to a
default or generic vhost
.. todo:: Handle _default_
"""
if vhost.get_names() != self.get_names():
return False
if not generic:
if vhost.get_names() != self.get_names():
return False
# If equal and set is not empty... assume same server
if self.name is not None or self.aliases:
return True
# If equal and set is not empty... assume same server
if self.name is not None or self.aliases:
return True
# If we're looking for a generic vhost,
# don't return one with a ServerName
elif self.name:
return False
# Both sets of names are empty.

View file

@ -14,9 +14,9 @@ SSLOptions +StrictRequire
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\"" vhost_combined
LogFormat "%v %h %l %u %t \"%r\" %>s %b" vhost_common
CustomLog /var/log/apache2/access.log vhost_combined
LogLevel warn
ErrorLog /var/log/apache2/error.log
#CustomLog /var/log/apache2/access.log vhost_combined
#LogLevel warn
#ErrorLog /var/log/apache2/error.log
# Always ensure Cookies have "Secure" set (JAH 2012/1)
#Header edit Set-Cookie (?i)^(.*)(;\s*secure)??((\s*;)?(.*)) "$1; Secure$3$4"

View file

@ -6,8 +6,9 @@ import os
import re
import subprocess
from letsencrypt import errors
from certbot import errors
from certbot_apache import constants
logger = logging.getLogger(__name__)
@ -19,7 +20,6 @@ class ApacheParser(object):
:ivar str root: Normalized absolute path to the server root
directory. Without trailing slash.
:ivar str root: Server root
:ivar set modules: All module names that are currently enabled.
:ivar dict loc: Location to place directives, root - configuration origin,
default - user config file, name - NameVirtualHost,
@ -28,15 +28,17 @@ class ApacheParser(object):
arg_var_interpreter = re.compile(r"\$\{[^ \}]*}")
fnmatch_chars = set(["*", "?", "\\", "[", "]"])
def __init__(self, aug, root, ctl):
def __init__(self, aug, root, vhostroot, version=(2, 4)):
# Note: Order is important here.
# This uses the binary, so it can be done first.
# https://httpd.apache.org/docs/2.4/mod/core.html#define
# https://httpd.apache.org/docs/2.4/mod/core.html#ifdefine
# This only handles invocation parameters and Define directives!
self.parser_paths = {}
self.variables = {}
self.update_runtime_variables(ctl)
if version >= (2, 4):
self.update_runtime_variables()
self.aug = aug
# Find configuration root and make sure augeas can parse it.
@ -44,6 +46,8 @@ class ApacheParser(object):
self.loc = {"root": self._find_config_root()}
self._parse_file(self.loc["root"])
self.vhostroot = os.path.abspath(vhostroot)
# This problem has been fixed in Augeas 1.0
self.standardize_excl()
@ -56,9 +60,14 @@ class ApacheParser(object):
# Set up rest of locations
self.loc.update(self._set_locations())
# Must also attempt to parse sites-available or equivalent
# Sites-available is not included naturally in configuration
self._parse_file(os.path.join(self.root, "sites-available") + "/*")
# Must also attempt to parse virtual host root
self._parse_file(self.vhostroot + "/" +
constants.os_constant("vhost_files"))
# check to see if there were unparsed define statements
if version < (2, 4):
if self.find_dir("Define", exclude=False):
raise errors.PluginError("Error parsing runtime variables")
def init_modules(self):
"""Iterates on the configuration until no new modules are loaded.
@ -84,29 +93,30 @@ class ApacheParser(object):
self.modules.add(
os.path.basename(self.get_arg(match_filename))[:-2] + "c")
def update_runtime_variables(self, ctl):
def update_runtime_variables(self):
""""
.. note:: Compile time variables (apache2ctl -V) are not used within the
dynamic configuration files. These should not be parsed or
.. note:: Compile time variables (apache2ctl -V) are not used within
the dynamic configuration files. These should not be parsed or
interpreted.
.. todo:: Create separate compile time variables... simply for arg_get()
.. todo:: Create separate compile time variables...
simply for arg_get()
"""
stdout = self._get_runtime_cfg(ctl)
stdout = self._get_runtime_cfg()
variables = dict()
matches = re.compile(r"Define: ([^ \n]*)").findall(stdout)
try:
matches.remove("DUMP_RUN_CFG")
except ValueError:
raise errors.PluginError("Unable to parse runtime variables")
return
for match in matches:
if match.count("=") > 1:
logger.error("Unexpected number of equal signs in "
"apache2ctl -D DUMP_RUN_CFG")
"runtime config dump.")
raise errors.PluginError(
"Error parsing Apache runtime variables")
parts = match.partition("=")
@ -114,7 +124,7 @@ class ApacheParser(object):
self.variables = variables
def _get_runtime_cfg(self, ctl): # pylint: disable=no-self-use
def _get_runtime_cfg(self): # pylint: disable=no-self-use
"""Get runtime configuration info.
:returns: stdout from DUMP_RUN_CFG
@ -122,16 +132,18 @@ class ApacheParser(object):
"""
try:
proc = subprocess.Popen(
[ctl, "-t", "-D", "DUMP_RUN_CFG"],
constants.os_constant("define_cmd"),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout, stderr = proc.communicate()
except (OSError, ValueError):
logger.error(
"Error accessing %s for runtime parameters!%s", ctl, os.linesep)
"Error running command %s for runtime parameters!%s",
constants.os_constant("define_cmd"), os.linesep)
raise errors.MisconfigurationError(
"Error accessing loaded Apache parameters: %s", ctl)
"Error accessing loaded Apache parameters: %s",
constants.os_constant("define_cmd"))
# Small errors that do not impede
if proc.returncode != 0:
logger.warn("Error in checking parameter list: %s", stderr)
@ -166,7 +178,8 @@ class ApacheParser(object):
# Make sure we don't cause an IndexError (end of list)
# Check to make sure arg + 1 doesn't exist
if (i == (len(matches) - 1) or
not matches[i + 1].endswith("/arg[%d]" % (args + 1))):
not matches[i + 1].endswith("/arg[%d]" %
(args + 1))):
filtered.append(matches[i][:-len("/arg[%d]" % args)])
return filtered
@ -300,8 +313,6 @@ class ApacheParser(object):
for match in matches:
dir_ = self.aug.get(match).lower()
if dir_ == "include" or dir_ == "includeoptional":
# start[6:] to strip off /files
#print self._get_include_path(self.get_arg(match +"/arg")), directive, arg
ordered_matches.extend(self.find_dir(
directive, arg,
self._get_include_path(self.get_arg(match + "/arg")),
@ -320,8 +331,8 @@ class ApacheParser(object):
"""
value = self.aug.get(match)
# No need to strip quotes for variables, as apache2ctl already does this
# but we do need to strip quotes for all normal arguments.
# No need to strip quotes for variables, as apache2ctl already does
# this, but we do need to strip quotes for all normal arguments.
# Note: normal argument may be a quoted variable
# e.g. strip now, not later
@ -443,7 +454,7 @@ class ApacheParser(object):
https://apr.apache.org/docs/apr/2.0/apr__fnmatch_8h_source.html
http://apache2.sourcearchive.com/documentation/2.2.16-6/apr__fnmatch_8h_source.html
:param str clean_fn_match: Apache style filename match, similar to globs
:param str clean_fn_match: Apache style filename match, like globs
:returns: regex suitable for augeas
:rtype: str
@ -461,16 +472,63 @@ class ApacheParser(object):
:param str filepath: Apache config file path
"""
use_new, remove_old = self._check_path_actions(filepath)
# Test if augeas included file for Httpd.lens
# Note: This works for augeas globs, ie. *.conf
inc_test = self.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % filepath)
if not inc_test:
# Load up files
# This doesn't seem to work on TravisCI
# self.aug.add_transform("Httpd.lns", [filepath])
self._add_httpd_transform(filepath)
self.aug.load()
if use_new:
inc_test = self.aug.match(
"/augeas/load/Httpd['%s' =~ glob(incl)]" % filepath)
if not inc_test:
# Load up files
# This doesn't seem to work on TravisCI
# self.aug.add_transform("Httpd.lns", [filepath])
if remove_old:
self._remove_httpd_transform(filepath)
self._add_httpd_transform(filepath)
self.aug.load()
def _check_path_actions(self, filepath):
"""Determine actions to take with a new augeas path
This helper function will return a tuple that defines
if we should try to append the new filepath to augeas
parser paths, and / or remove the old one with more
narrow matching.
:param str filepath: filepath to check the actions for
"""
try:
new_file_match = os.path.basename(filepath)
existing_matches = self.parser_paths[os.path.dirname(filepath)]
if "*" in existing_matches:
use_new = False
else:
use_new = True
if new_file_match == "*":
remove_old = True
else:
remove_old = False
except KeyError:
use_new = True
remove_old = False
return use_new, remove_old
def _remove_httpd_transform(self, filepath):
"""Remove path from Augeas transform
:param str filepath: filepath to remove
"""
remove_basenames = self.parser_paths[os.path.dirname(filepath)]
remove_dirname = os.path.dirname(filepath)
for name in remove_basenames:
remove_path = remove_dirname + "/" + name
remove_inc = self.aug.match(
"/augeas/load/Httpd/incl [. ='%s']" % remove_path)
self.aug.remove(remove_inc[0])
self.parser_paths.pop(remove_dirname)
def _add_httpd_transform(self, incl):
"""Add a transform to Augeas.
@ -492,6 +550,13 @@ class ApacheParser(object):
# Augeas uses base 1 indexing... insert at beginning...
self.aug.set("/augeas/load/Httpd/lens", "Httpd.lns")
self.aug.set("/augeas/load/Httpd/incl", incl)
# Add included path to paths dictionary
try:
self.parser_paths[os.path.dirname(incl)].append(
os.path.basename(incl))
except KeyError:
self.parser_paths[os.path.dirname(incl)] = [
os.path.basename(incl)]
def standardize_excl(self):
"""Standardize the excl arguments for the Httpd lens in Augeas.
@ -532,7 +597,7 @@ class ApacheParser(object):
.. todo:: Make sure that files are included
"""
default = self._set_user_config_file()
default = self.loc["root"]
temp = os.path.join(self.root, "ports.conf")
if os.path.isfile(temp):
@ -546,31 +611,13 @@ class ApacheParser(object):
def _find_config_root(self):
"""Find the Apache Configuration Root file."""
location = ["apache2.conf", "httpd.conf"]
location = ["apache2.conf", "httpd.conf", "conf/httpd.conf"]
for name in location:
if os.path.isfile(os.path.join(self.root, name)):
return os.path.join(self.root, name)
raise errors.NoInstallationError("Could not find configuration root")
def _set_user_config_file(self):
"""Set the appropriate user configuration file
.. todo:: This will have to be updated for other distros versions
:param str root: pathname which contains the user config
"""
# Basic check to see if httpd.conf exists and
# in hierarchy via direct include
# httpd.conf was very common as a user file in Apache 2.2
if (os.path.isfile(os.path.join(self.root, "httpd.conf")) and
self.find_dir("Include", "httpd.conf", self.loc["root"])):
return os.path.join(self.root, "httpd.conf")
else:
return os.path.join(self.root, "apache2.conf")
def case_i(string):
"""Returns case insensitive regex.

View file

@ -0,0 +1 @@
"""Certbot Apache Tests"""

View file

@ -0,0 +1,6 @@
Issues for which some kind of test case should be constructable, but we do not
currently have one:
https://github.com/certbot/certbot/issues/1213
https://github.com/certbot/certbot/issues/1602

View file

@ -0,0 +1,77 @@
#!/bin/bash
# A hackish script to see if the client is behaving as expected
# with each of the "passing" conf files.
export EA=/etc/apache2/
TESTDIR="`dirname $0`"
cd $TESTDIR/passing
function CleanupExit() {
echo control c, exiting tests...
if [ "$f" != "" ] ; then
Cleanup
fi
exit 1
}
function Setup() {
if [ "$APPEND_APACHECONF" = "" ] ; then
sudo cp "$f" "$EA"/sites-available/
sudo ln -sf "$EA/sites-available/$f" "$EA/sites-enabled/$f"
echo "
<VirtualHost *:80>
ServerName example.com
DocumentRoot /tmp/
ErrorLog /tmp/error.log
CustomLog /tmp/requests.log combined
</VirtualHost>" | sudo tee $EA/sites-available/throwaway-example.conf >/dev/null
else
TMP="/tmp/`basename \"$APPEND_APACHECONF\"`.$$"
sudo cp -a "$APPEND_APACHECONF" "$TMP"
sudo bash -c "cat \"$f\" >> \"$APPEND_APACHECONF\""
fi
}
function Cleanup() {
if [ "$APPEND_APACHECONF" = "" ] ; then
sudo rm /etc/apache2/sites-{enabled,available}/"$f"
sudo rm $EA/sites-available/throwaway-example.conf
else
sudo mv "$TMP" "$APPEND_APACHECONF"
fi
}
# if our environment asks us to enable modules, do our best!
if [ "$1" = --debian-modules ] ; then
sudo apt-get install -y libapache2-mod-wsgi
sudo apt-get install -y libapache2-mod-macro
for mod in ssl rewrite macro wsgi deflate userdir version mime setenvif ; do
echo -n enabling $mod
sudo a2enmod $mod
done
fi
FAILS=0
trap CleanupExit INT
for f in *.conf ; do
echo -n testing "$f"...
Setup
RESULT=`echo c | sudo $(command -v certbot) -vvvv --debug --staging --apache --register-unsafely-without-email --agree-tos certonly -t 2>&1`
if echo $RESULT | grep -Eq \("Which names would you like"\|"mod_macro is not yet"\) ; then
echo passed
else
echo failed
echo $RESULT
echo
echo
FAILS=`expr $FAILS + 1`
fi
Cleanup
done
if [ "$FAILS" -ne 0 ] ; then
exit 1
fi
exit 0

View file

@ -0,0 +1,52 @@
<VirtualHost *:443>
ServerAdmin webmaster@localhost
ServerAlias www.example.com
ServerName example.com
DocumentRoot /var/www/example.com/www/
SSLEngine on
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
<Directory /var/www/example.com/www>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
# This directive allows us to have apache2's default start page
# in /apache2-default/, but still have / go to the right place
</Directory>
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>
ErrorLog /var/log/apache2/error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access.log combined
ServerSignature On
Alias /apache_doc/ "/usr/share/doc/"
<Directory "/usr/share/doc/">
Options Indexes MultiViews FollowSymLinks
AllowOverride None
Order deny,allow
Deny from all
Allow from 127.0.0.0/255.0.0.0 ::1/128
</Directory>
</VirtualHost>

View file

@ -0,0 +1,37 @@
<VirtualHost *:80>
ServerAdmin denver@ossguy.com
ServerName c-beta.ossguy.com
Alias /robots.txt /home/denver/www/c-beta.ossguy.com/static/robots.txt
Alias /favicon.ico /home/denver/www/c-beta.ossguy.com/static/favicon.ico
AliasMatch /(.*\.css) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.js) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.png) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.gif) /home/denver/www/c-beta.ossguy.com/static/$1
AliasMatch /(.*\.jpg) /home/denver/www/c-beta.ossguy.com/static/$1
WSGIScriptAlias / /home/denver/www/c-beta.ossguy.com/django.wsgi
WSGIDaemonProcess c-beta-ossguy user=www-data group=www-data home=/var/www processes=5 threads=10 maximum-requests=1000 umask=0007 display-name=c-beta-ossguy
WSGIProcessGroup c-beta-ossguy
WSGIApplicationGroup %{GLOBAL}
DocumentRoot /home/denver/www/c-beta.ossguy.com/static
<Directory /home/denver/www/c-beta.ossguy.com/static>
Options -Indexes +FollowSymLinks -MultiViews
Require all granted
AllowOverride None
</Directory>
<Directory /home/denver/www/c-beta.ossguy.com/static/source>
Options +Indexes +FollowSymLinks -MultiViews
Require all granted
AllowOverride None
</Directory>
# Custom log file locations
LogLevel warn
ErrorLog /tmp/error.log
CustomLog /tmp/access.log combined
</VirtualHost>

View file

@ -0,0 +1,6 @@
# Modules required to parse these conf files:
ssl
rewrite
macro
wsgi
deflate

View file

@ -0,0 +1,116 @@
#
# Apache/PHP/Drupal settings:
#
# Protect files and directories from prying eyes.
<FilesMatch "\.(engine|inc|info|install|make|module|profile|test|po|sh|.*sql|theme|tpl(\.php)?|xtmpl|svn-base)$|^(code-style\.pl|Entries.*|Repository|Root|Tag|Template|all-wcprops|entries|format)$">
Order allow,deny
</FilesMatch>
# Don't show directory listings for URLs which map to a directory.
Options -Indexes
# Follow symbolic links in this directory.
Options +FollowSymLinks
# Make Drupal handle any 404 errors.
ErrorDocument 404 /index.php
# Force simple error message for requests for non-existent favicon.ico.
<Files favicon.ico>
# There is no end quote below, for compatibility with Apache 1.3.
ErrorDocument 404 "The requested file favicon.ico was not found.
</Files>
# Set the default handler.
DirectoryIndex index.php
# Override PHP settings. More in sites/default/settings.php
# but the following cannot be changed at runtime.
# PHP 4, Apache 1.
<IfModule mod_php4.c>
php_value magic_quotes_gpc 0
php_value register_globals 0
php_value session.auto_start 0
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_value mbstring.encoding_translation 0
</IfModule>
# PHP 4, Apache 2.
<IfModule sapi_apache2.c>
php_value magic_quotes_gpc 0
php_value register_globals 0
php_value session.auto_start 0
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_value mbstring.encoding_translation 0
</IfModule>
# PHP 5, Apache 1 and 2.
<IfModule mod_php5.c>
php_value magic_quotes_gpc 0
php_value register_globals 0
php_value session.auto_start 0
php_value mbstring.http_input pass
php_value mbstring.http_output pass
php_value mbstring.encoding_translation 0
</IfModule>
# Requires mod_expires to be enabled.
<IfModule mod_expires.c>
# Enable expirations.
ExpiresActive On
# Cache all files for 2 weeks after access (A).
ExpiresDefault A1209600
<FilesMatch \.php$>
# Do not allow PHP scripts to be cached unless they explicitly send cache
# headers themselves. Otherwise all scripts would have to overwrite the
# headers set by mod_expires if they want another caching behavior. This may
# fail if an error occurs early in the bootstrap process, and it may cause
# problems if a non-Drupal PHP file is installed in a subdirectory.
ExpiresActive Off
</FilesMatch>
</IfModule>
# Various rewrite rules.
<IfModule mod_rewrite.c>
RewriteEngine on
# If your site can be accessed both with and without the 'www.' prefix, you
# can use one of the following settings to redirect users to your preferred
# URL, either WITH or WITHOUT the 'www.' prefix. Choose ONLY one option:
#
# To redirect all users to access the site WITH the 'www.' prefix,
# (http://example.com/... will be redirected to http://www.example.com/...)
# adapt and uncomment the following:
# RewriteCond %{HTTP_HOST} ^example\.com$ [NC]
# RewriteRule ^(.*)$ http://www.example.com/$1 [L,R=301]
#
# To redirect all users to access the site WITHOUT the 'www.' prefix,
# (http://www.example.com/... will be redirected to http://example.com/...)
# uncomment and adapt the following:
# RewriteCond %{HTTP_HOST} ^www\.example\.com$ [NC]
# RewriteRule ^(.*)$ http://example.com/$1 [L,R=301]
# Modify the RewriteBase if you are using Drupal in a subdirectory or in a
# VirtualDocumentRoot and the rewrite rules are not working properly.
# For example if your site is at http://example.com/drupal uncomment and
# modify the following line:
# RewriteBase /drupal
#
# If your site is running in a VirtualDocumentRoot at http://example.com/,
# uncomment the following line:
# RewriteBase /
# Rewrite URLs of the form 'x' to the form 'index.php?q=x'.
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_URI} !=/favicon.ico
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
</IfModule>
# $Id$

View file

@ -0,0 +1,2 @@
RewriteCond %{HTTP:Content-Disposition} \.php [NC]
RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /.+/trackback/?\ HTTP/ [NC]

View file

@ -0,0 +1,36 @@
<VirtualHost *:80>
# The ServerName directive sets the request scheme, hostname and port that
# the server uses to identify itself. This is used when creating
# redirection URLs. In the context of virtual hosts, the ServerName
# specifies what hostname must appear in the request's Host: header to
# match this virtual host. For the default virtual host (this file) this
# value is not decisive as it is used as a last resort host regardless.
# However, you must set it for any further virtual host explicitly.
ServerName www.example.com
ServerAlias example.com
SetOutputFilter DEFLATE
# Do not attempt to compress the following extensions
SetEnvIfNoCase Request_URI \
\.(?:gif|jpe?g|png|swf|flv|zip|gz|tar|mp3|mp4|m4v)$ no-gzip dont-vary
ServerAdmin webmaster@localhost
DocumentRoot /var/www/proof
# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the loglevel for particular
# modules, e.g.
#LogLevel info ssl:warn
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
# For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to
# include a line for only one particular virtual host. For example the
# following line enables the CGI configuration for this host only
# after it has been globally disabled with "a2disconf".
#Include conf-available/serve-cgi-bin.conf
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

View file

@ -39,7 +39,7 @@
# certificate chain for the server certificate. Alternatively
# the referenced file can be the same as SSLCertificateFile
# when the CA certificates are directly appended to the server
# certificate for convinience.
# certificate for convenience.
#SSLCertificateChainFile /etc/apache2/ssl.crt/server-ca.crt
# Certificate Authority (CA):

View file

@ -0,0 +1,21 @@
<VirtualHost *:80>
WSGIDaemonProcess _graphite processes=5 threads=5 display-name='%{GROUP}' inactivity-timeout=120 user=www-data group=www-data
WSGIProcessGroup _graphite
WSGIImportScript /usr/share/graphite-web/graphite.wsgi process-group=_graphite application-group=%{GLOBAL}
WSGIScriptAlias / /usr/share/graphite-web/graphite.wsgi
Alias /content/ /usr/share/graphite-web/static/
<Location "/content/">
SetHandler None
</Location>
ErrorLog ${APACHE_LOG_DIR}/graphite-web_error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog ${APACHE_LOG_DIR}/graphite-web_access.log combined
</VirtualHost>

View file

@ -0,0 +1,9 @@
<VirtualHost *:80 [::]:80>
DocumentRoot /tmp
ServerName example.com
ServerAlias www.example.com
CustomLog ${APACHE_LOG_DIR}/example.log combined
<Directory "/tmp">
AllowOverride All
</Directory>
</VirtualHost>

View file

@ -0,0 +1,18 @@
<VirtualHost *:443 [::]:443>
DocumentRoot /tmp
ServerName example.com
ServerAlias www.example.com
CustomLog ${APACHE_LOG_DIR}/example.log combined
<Directory "/tmp">
AllowOverride All
</Directory>
SSLEngine on
SSLHonorCipherOrder On
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
</VirtualHost>

View file

@ -0,0 +1,9 @@
<VirtualHost [::]:80 *:80>
DocumentRoot /tmp
ServerName example.com
ServerAlias www.example.com
CustomLog ${APACHE_LOG_DIR}/example.log combined
<Directory "/tmp">
AllowOverride All
</Directory>
</VirtualHost>

View file

@ -0,0 +1,18 @@
<VirtualHost [::]:443 *:443>
DocumentRoot /tmp
ServerName example.com
ServerAlias www.example.com
CustomLog ${APACHE_LOG_DIR}/example.log combined
<Directory "/tmp">
AllowOverride All
</Directory>
SSLEngine on
SSLHonorCipherOrder On
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH +aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS"
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
</VirtualHost>

View file

@ -0,0 +1,52 @@
<VirtualHost *:443>
ServerAdmin webmaster@localhost
ServerAlias www.example.com
ServerName example.com
DocumentRoot /var/www/example.com/www/
SSLEngine on
SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRS$
SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
<Directory />
Options FollowSymLinks
AllowOverride All
</Directory>
<Directory /var/www/example.com/www>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
# This directive allows us to have apache2's default start page
# in /apache2-default/, but still have / go to the right place
</Directory>
ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
<Directory "/usr/lib/cgi-bin">
AllowOverride None
Options +ExecCGI -MultiViews +SymLinksIfOwnerMatch
Order allow,deny
Allow from all
</Directory>
ErrorLog /var/log/apache2/error.log
# Possible values include: debug, info, notice, warn, error, crit,
# alert, emerg.
LogLevel warn
CustomLog /var/log/apache2/access.log combined
ServerSignature On
Alias /apache_doc/ "/usr/share/doc/"
<Directory "/usr/share/doc/">
Options Indexes MultiViews FollowSymLinks
AllowOverride None
Order deny,allow
Deny from all
Allow from 127.0.0.0/255.0.0.0 ::1/128
</Directory>
</VirtualHost>

Some files were not shown because too many files have changed in this diff Show more