Compare commits
57 Commits
nginx-reve
...
bleeding-e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b13b001aa | ||
|
|
5d58a3d847 | ||
|
|
28dad825af | ||
|
|
f0f5defb6f | ||
|
|
fa97877cfb | ||
|
|
2ba334a182 | ||
|
|
9e95208101 | ||
|
|
39472f88de | ||
|
|
3acf5d1ef9 | ||
|
|
00634394f2 | ||
|
|
6eb459354f | ||
|
|
f5a02714cd | ||
|
|
887a6bcfce | ||
|
|
288c4d956c | ||
|
|
62ffcf5373 | ||
|
|
d557475bb6 | ||
|
|
e02adec26b | ||
|
|
24ddc65cd4 | ||
|
|
8585cdd861 | ||
|
|
18f6deada8 | ||
|
|
a1713c0b79 | ||
|
|
a3a66cd25d | ||
|
|
a7d00ee21b | ||
|
|
5388842e5b | ||
|
|
ed2168aaa8 | ||
|
|
d6b11fea72 | ||
|
|
a1aea021e7 | ||
|
|
1b6005cc61 | ||
|
|
0e92d4ea98 | ||
|
|
2abc94661a | ||
|
|
8bc785ed46 | ||
|
|
0046428382 | ||
|
|
5d0888809f | ||
|
|
8096b91496 | ||
|
|
e696766ed1 | ||
|
|
8b5d6879cc | ||
|
|
d039106b68 | ||
|
|
abed73a8e4 | ||
|
|
3951baf6c0 | ||
|
|
716f25743c | ||
|
|
b3ca6bb2b1 | ||
|
|
78d97ca023 | ||
|
|
f1554324da | ||
|
|
840c943711 | ||
|
|
abdde886fa | ||
|
|
20bca19420 | ||
|
|
e795a79547 | ||
|
|
02126c0961 | ||
|
|
0b843bb851 | ||
|
|
4d60f32865 | ||
|
|
069ce1c55f | ||
|
|
eb26e0aacf | ||
|
|
1173acfaf0 | ||
|
|
0aa9322280 | ||
|
|
89485f7463 | ||
|
|
4e73d7ce00 | ||
|
|
0137055c24 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -35,3 +35,6 @@ tests/letstest/*.pem
|
||||
tests/letstest/venv/
|
||||
|
||||
.venv
|
||||
|
||||
# pytest cache
|
||||
.cache
|
||||
|
||||
68
.travis.yml
68
.travis.yml
@@ -5,7 +5,7 @@ cache:
|
||||
- $HOME/.cache/pip
|
||||
|
||||
before_install:
|
||||
- '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3)'
|
||||
- '([ $TRAVIS_OS_NAME == linux ] && dpkg -s libaugeas0) || (brew update && brew install augeas python3 && brew upgrade python && brew link python)'
|
||||
|
||||
before_script:
|
||||
- 'if [ $TRAVIS_OS_NAME = osx ] ; then ulimit -n 1024 ; fi'
|
||||
@@ -13,19 +13,32 @@ before_script:
|
||||
matrix:
|
||||
include:
|
||||
- python: "2.7"
|
||||
env: TOXENV=cover FYI="this also tests py27"
|
||||
- python: "2.7"
|
||||
env: TOXENV=lint
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27-oldest
|
||||
env: TOXENV=py27_install BOULDER_INTEGRATION=1
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "2.7"
|
||||
env: TOXENV=cover FYI="this also tests py27"
|
||||
- sudo: required
|
||||
env: TOXENV=nginx_compat
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- python: "2.7"
|
||||
env: TOXENV=lint
|
||||
- python: "2.6"
|
||||
env: TOXENV=py26
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "2.7"
|
||||
env: TOXENV=py27_install BOULDER_INTEGRATION=1
|
||||
env: TOXENV=py27-oldest
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.3"
|
||||
env: TOXENV=py33
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
sudo: required
|
||||
services: docker
|
||||
- sudo: required
|
||||
@@ -33,55 +46,14 @@ matrix:
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=nginx_compat
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto_precise
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto_trusty
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto_wheezy
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=le_auto_centos6
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- sudo: required
|
||||
env: TOXENV=docker_dev
|
||||
services: docker
|
||||
before_install:
|
||||
addons:
|
||||
- python: "2.7"
|
||||
env: TOXENV=apacheconftest
|
||||
sudo: required
|
||||
- python: "3.3"
|
||||
env: TOXENV=py33
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.4"
|
||||
env: TOXENV=py34
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.5"
|
||||
env: TOXENV=py35
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "3.6"
|
||||
env: TOXENV=py36
|
||||
sudo: required
|
||||
services: docker
|
||||
- python: "2.7"
|
||||
env: TOXENV=nginxroundtrip
|
||||
|
||||
|
||||
36
CHANGELOG.md
36
CHANGELOG.md
@@ -2,6 +2,42 @@
|
||||
|
||||
Certbot adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## 0.20.0 - 2017-12-06
|
||||
|
||||
### Added
|
||||
|
||||
* Certbot's ACME library now recognizes URL fields in challenge objects in
|
||||
preparation for Let's Encrypt's new ACME endpoint. The value is still
|
||||
accessible in our ACME library through the name "uri".
|
||||
|
||||
### Changed
|
||||
|
||||
* The Apache plugin now parses some distro specific Apache configuration files
|
||||
on non-Debian systems allowing it to get a clearer picture on the running
|
||||
configuration. Internally, these changes were structured so that external
|
||||
contributors can easily write patches to make the plugin work in new Apache
|
||||
configurations.
|
||||
* Certbot better reports network failures by removing information about
|
||||
connection retries from the error output.
|
||||
* An unnecessary question when using Certbot's webroot plugin interactively has
|
||||
been removed.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Certbot's NGINX plugin no longer sometimes incorrectly reports that it was
|
||||
unable to deploy a HTTP->HTTPS redirect when requesting Certbot to enable a
|
||||
redirect for multiple domains.
|
||||
* Problems where the Apache plugin was failing to find directives and
|
||||
duplicating existing directives on openSUSE have been resolved.
|
||||
* An issue running the test shipped with Certbot and some our DNS plugins with
|
||||
older versions of mock have been resolved.
|
||||
* On some systems, users reported strangely interleaved output depending on
|
||||
when stdout and stderr were flushed. This problem was resolved by having
|
||||
Certbot regularly flush these streams.
|
||||
|
||||
More details about these changes can be found on our GitHub repo:
|
||||
https://github.com/certbot/certbot/milestone/44?closed=1
|
||||
|
||||
## 0.19.0 - 2017-10-04
|
||||
|
||||
### Added
|
||||
|
||||
@@ -10,3 +10,13 @@ supported version: `draft-ietf-acme-01`_.
|
||||
https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01
|
||||
|
||||
"""
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
for (major, minor) in [(2, 6), (3, 3)]:
|
||||
if sys.version_info[:2] == (major, minor):
|
||||
warnings.warn(
|
||||
"Python {0}.{1} support will be dropped in the next release of "
|
||||
"acme. Please upgrade your Python version.".format(major, minor),
|
||||
DeprecationWarning,
|
||||
) #pragma: no cover
|
||||
|
||||
@@ -6,13 +6,13 @@ import logging
|
||||
import socket
|
||||
|
||||
from cryptography.hazmat.primitives import hashes # type: ignore
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
import requests
|
||||
|
||||
from acme import errors
|
||||
from acme import crypto_util
|
||||
from acme import fields
|
||||
from acme import jose
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Tests for acme.challenges."""
|
||||
import unittest
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
import OpenSSL
|
||||
import requests
|
||||
@@ -8,7 +9,6 @@ import requests
|
||||
from six.moves.urllib import parse as urllib_parse # pylint: disable=import-error
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import test_util
|
||||
|
||||
CERT = test_util.load_comparable_cert('cert.pem')
|
||||
|
||||
@@ -10,13 +10,13 @@ import time
|
||||
import six
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
import re
|
||||
import requests
|
||||
import sys
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import jws
|
||||
from acme import messages
|
||||
|
||||
@@ -408,7 +408,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes
|
||||
:param str uri: URI of certificate
|
||||
|
||||
:returns: tuple of the form
|
||||
(response, :class:`acme.jose.ComparableX509`)
|
||||
(response, :class:`josepy.util.ComparableX509`)
|
||||
:rtype: tuple
|
||||
|
||||
"""
|
||||
|
||||
@@ -5,12 +5,12 @@ import unittest
|
||||
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import jws as acme_jws
|
||||
from acme import messages
|
||||
from acme import messages_test
|
||||
|
||||
@@ -8,10 +8,10 @@ import unittest
|
||||
import six
|
||||
from six.moves import socketserver #type: ignore # pylint: disable=import-error
|
||||
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import test_util
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""ACME errors."""
|
||||
from acme.jose import errors as jose_errors
|
||||
from josepy import errors as jose_errors
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"""ACME JSON fields."""
|
||||
import logging
|
||||
|
||||
import josepy as jose
|
||||
import pyrfc3339
|
||||
|
||||
from acme import jose
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
import datetime
|
||||
import unittest
|
||||
|
||||
import josepy as jose
|
||||
import pytz
|
||||
|
||||
from acme import jose
|
||||
|
||||
|
||||
class FixedTest(unittest.TestCase):
|
||||
"""Tests for acme.fields.Fixed."""
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
"""Javascript Object Signing and Encryption (jose).
|
||||
|
||||
This package is a Python implementation of the standards developed by
|
||||
IETF `Javascript Object Signing and Encryption (Active WG)`_, in
|
||||
particular the following RFCs:
|
||||
|
||||
- `JSON Web Algorithms (JWA)`_
|
||||
- `JSON Web Key (JWK)`_
|
||||
- `JSON Web Signature (JWS)`_
|
||||
|
||||
|
||||
.. _`Javascript Object Signing and Encryption (Active WG)`:
|
||||
https://tools.ietf.org/wg/jose/
|
||||
|
||||
.. _`JSON Web Algorithms (JWA)`:
|
||||
https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-algorithms/
|
||||
|
||||
.. _`JSON Web Key (JWK)`:
|
||||
https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-key/
|
||||
|
||||
.. _`JSON Web Signature (JWS)`:
|
||||
https://datatracker.ietf.org/doc/draft-ietf-jose-json-web-signature/
|
||||
|
||||
"""
|
||||
from acme.jose.b64 import (
|
||||
b64decode,
|
||||
b64encode,
|
||||
)
|
||||
|
||||
from acme.jose.errors import (
|
||||
DeserializationError,
|
||||
SerializationError,
|
||||
Error,
|
||||
UnrecognizedTypeError,
|
||||
)
|
||||
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
|
||||
from acme.jose.json_util import (
|
||||
Field,
|
||||
JSONObjectWithFields,
|
||||
TypedJSONObjectWithFields,
|
||||
decode_b64jose,
|
||||
decode_cert,
|
||||
decode_csr,
|
||||
decode_hex16,
|
||||
encode_b64jose,
|
||||
encode_cert,
|
||||
encode_csr,
|
||||
encode_hex16,
|
||||
)
|
||||
|
||||
from acme.jose.jwa import (
|
||||
HS256,
|
||||
HS384,
|
||||
HS512,
|
||||
JWASignature,
|
||||
PS256,
|
||||
PS384,
|
||||
PS512,
|
||||
RS256,
|
||||
RS384,
|
||||
RS512,
|
||||
)
|
||||
|
||||
from acme.jose.jwk import (
|
||||
JWK,
|
||||
JWKRSA,
|
||||
)
|
||||
|
||||
from acme.jose.jws import (
|
||||
Header,
|
||||
JWS,
|
||||
Signature,
|
||||
)
|
||||
|
||||
from acme.jose.util import (
|
||||
ComparableX509,
|
||||
ComparableKey,
|
||||
ComparableRSAKey,
|
||||
ImmutableMap,
|
||||
)
|
||||
@@ -1,61 +0,0 @@
|
||||
"""JOSE Base64.
|
||||
|
||||
`JOSE Base64`_ is defined as:
|
||||
|
||||
- URL-safe Base64
|
||||
- padding stripped
|
||||
|
||||
|
||||
.. _`JOSE Base64`:
|
||||
https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-37#appendix-C
|
||||
|
||||
.. Do NOT try to call this module "base64", as it will "shadow" the
|
||||
standard library.
|
||||
|
||||
"""
|
||||
import base64
|
||||
|
||||
import six
|
||||
|
||||
|
||||
def b64encode(data):
|
||||
"""JOSE Base64 encode.
|
||||
|
||||
:param data: Data to be encoded.
|
||||
:type data: `bytes`
|
||||
|
||||
:returns: JOSE Base64 string.
|
||||
:rtype: bytes
|
||||
|
||||
:raises TypeError: if `data` is of incorrect type
|
||||
|
||||
"""
|
||||
if not isinstance(data, six.binary_type):
|
||||
raise TypeError('argument should be {0}'.format(six.binary_type))
|
||||
return base64.urlsafe_b64encode(data).rstrip(b'=')
|
||||
|
||||
|
||||
def b64decode(data):
|
||||
"""JOSE Base64 decode.
|
||||
|
||||
:param data: Base64 string to be decoded. If it's unicode, then
|
||||
only ASCII characters are allowed.
|
||||
:type data: `bytes` or `unicode`
|
||||
|
||||
:returns: Decoded data.
|
||||
:rtype: bytes
|
||||
|
||||
:raises TypeError: if input is of incorrect type
|
||||
:raises ValueError: if input is unicode with non-ASCII characters
|
||||
|
||||
"""
|
||||
if isinstance(data, six.string_types):
|
||||
try:
|
||||
data = data.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError(
|
||||
'unicode argument should contain only ASCII characters')
|
||||
elif not isinstance(data, six.binary_type):
|
||||
raise TypeError('argument should be a str or unicode')
|
||||
|
||||
return base64.urlsafe_b64decode(data + b'=' * (4 - (len(data) % 4)))
|
||||
@@ -1,77 +0,0 @@
|
||||
"""Tests for acme.jose.b64."""
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
|
||||
# https://en.wikipedia.org/wiki/Base64#Examples
|
||||
B64_PADDING_EXAMPLES = {
|
||||
b'any carnal pleasure.': (b'YW55IGNhcm5hbCBwbGVhc3VyZS4', b'='),
|
||||
b'any carnal pleasure': (b'YW55IGNhcm5hbCBwbGVhc3VyZQ', b'=='),
|
||||
b'any carnal pleasur': (b'YW55IGNhcm5hbCBwbGVhc3Vy', b''),
|
||||
b'any carnal pleasu': (b'YW55IGNhcm5hbCBwbGVhc3U', b'='),
|
||||
b'any carnal pleas': (b'YW55IGNhcm5hbCBwbGVhcw', b'=='),
|
||||
}
|
||||
|
||||
|
||||
B64_URL_UNSAFE_EXAMPLES = {
|
||||
six.int2byte(251) + six.int2byte(239): b'--8',
|
||||
six.int2byte(255) * 2: b'__8',
|
||||
}
|
||||
|
||||
|
||||
class B64EncodeTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.b64.b64encode."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, data):
|
||||
from acme.jose.b64 import b64encode
|
||||
return b64encode(data)
|
||||
|
||||
def test_empty(self):
|
||||
self.assertEqual(self._call(b''), b'')
|
||||
|
||||
def test_unsafe_url(self):
|
||||
for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES):
|
||||
self.assertEqual(self._call(text), b64)
|
||||
|
||||
def test_different_paddings(self):
|
||||
for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES):
|
||||
self.assertEqual(self._call(text), b64)
|
||||
|
||||
def test_unicode_fails_with_type_error(self):
|
||||
self.assertRaises(TypeError, self._call, u'some unicode')
|
||||
|
||||
|
||||
class B64DecodeTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.b64.b64decode."""
|
||||
|
||||
@classmethod
|
||||
def _call(cls, data):
|
||||
from acme.jose.b64 import b64decode
|
||||
return b64decode(data)
|
||||
|
||||
def test_unsafe_url(self):
|
||||
for text, b64 in six.iteritems(B64_URL_UNSAFE_EXAMPLES):
|
||||
self.assertEqual(self._call(b64), text)
|
||||
|
||||
def test_input_without_padding(self):
|
||||
for text, (b64, _) in six.iteritems(B64_PADDING_EXAMPLES):
|
||||
self.assertEqual(self._call(b64), text)
|
||||
|
||||
def test_input_with_padding(self):
|
||||
for text, (b64, pad) in six.iteritems(B64_PADDING_EXAMPLES):
|
||||
self.assertEqual(self._call(b64 + pad), text)
|
||||
|
||||
def test_unicode_with_ascii(self):
|
||||
self.assertEqual(self._call(u'YQ'), b'a')
|
||||
|
||||
def test_non_ascii_unicode_fails(self):
|
||||
self.assertRaises(ValueError, self._call, u'\u0105')
|
||||
|
||||
def test_type_error_no_unicode_or_bytes(self):
|
||||
self.assertRaises(TypeError, self._call, object())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,35 +0,0 @@
|
||||
"""JOSE errors."""
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
"""Generic JOSE Error."""
|
||||
|
||||
|
||||
class DeserializationError(Error):
|
||||
"""JSON deserialization error."""
|
||||
|
||||
def __str__(self):
|
||||
return "Deserialization error: {0}".format(
|
||||
super(DeserializationError, self).__str__())
|
||||
|
||||
|
||||
class SerializationError(Error):
|
||||
"""JSON serialization error."""
|
||||
|
||||
|
||||
class UnrecognizedTypeError(DeserializationError):
|
||||
"""Unrecognized type error.
|
||||
|
||||
:ivar str typ: The unrecognized type of the JSON object.
|
||||
:ivar jobj: Full JSON object.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, typ, jobj):
|
||||
self.typ = typ
|
||||
self.jobj = jobj
|
||||
super(UnrecognizedTypeError, self).__init__(str(self))
|
||||
|
||||
def __str__(self):
|
||||
return '{0} was not recognized, full message: {1}'.format(
|
||||
self.typ, self.jobj)
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Tests for acme.jose.errors."""
|
||||
import unittest
|
||||
|
||||
|
||||
class UnrecognizedTypeErrorTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
from acme.jose.errors import UnrecognizedTypeError
|
||||
self.error = UnrecognizedTypeError('foo', {'type': 'foo'})
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(
|
||||
"foo was not recognized, full message: {'type': 'foo'}",
|
||||
str(self.error))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,216 +0,0 @@
|
||||
"""JOSE interfaces."""
|
||||
import abc
|
||||
import collections
|
||||
import json
|
||||
|
||||
import six
|
||||
|
||||
from acme.jose import errors
|
||||
from acme.jose import util
|
||||
|
||||
# pylint: disable=no-self-argument,no-method-argument,no-init,inherit-non-class
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class JSONDeSerializable(object):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Interface for (de)serializable JSON objects.
|
||||
|
||||
Please recall, that standard Python library implements
|
||||
:class:`json.JSONEncoder` and :class:`json.JSONDecoder` that perform
|
||||
translations based on respective :ref:`conversion tables
|
||||
<conversion-table>` that look pretty much like the one below (for
|
||||
complete tables see relevant Python documentation):
|
||||
|
||||
.. _conversion-table:
|
||||
|
||||
====== ======
|
||||
JSON Python
|
||||
====== ======
|
||||
object dict
|
||||
... ...
|
||||
====== ======
|
||||
|
||||
While the above **conversion table** is about translation of JSON
|
||||
documents to/from the basic Python types only,
|
||||
:class:`JSONDeSerializable` introduces the following two concepts:
|
||||
|
||||
serialization
|
||||
Turning an arbitrary Python object into Python object that can
|
||||
be encoded into a JSON document. **Full serialization** produces
|
||||
a Python object composed of only basic types as required by the
|
||||
:ref:`conversion table <conversion-table>`. **Partial
|
||||
serialization** (accomplished by :meth:`to_partial_json`)
|
||||
produces a Python object that might also be built from other
|
||||
:class:`JSONDeSerializable` objects.
|
||||
|
||||
deserialization
|
||||
Turning a decoded Python object (necessarily one of the basic
|
||||
types as required by the :ref:`conversion table
|
||||
<conversion-table>`) into an arbitrary Python object.
|
||||
|
||||
Serialization produces **serialized object** ("partially serialized
|
||||
object" or "fully serialized object" for partial and full
|
||||
serialization respectively) and deserialization produces
|
||||
**deserialized object**, both usually denoted in the source code as
|
||||
``jobj``.
|
||||
|
||||
Wording in the official Python documentation might be confusing
|
||||
after reading the above, but in the light of those definitions, one
|
||||
can view :meth:`json.JSONDecoder.decode` as decoder and
|
||||
deserializer of basic types, :meth:`json.JSONEncoder.default` as
|
||||
serializer of basic types, :meth:`json.JSONEncoder.encode` as
|
||||
serializer and encoder of basic types.
|
||||
|
||||
One could extend :mod:`json` to support arbitrary object
|
||||
(de)serialization either by:
|
||||
|
||||
- overriding :meth:`json.JSONDecoder.decode` and
|
||||
:meth:`json.JSONEncoder.default` in subclasses
|
||||
|
||||
- or passing ``object_hook`` argument (or ``object_hook_pairs``)
|
||||
to :func:`json.load`/:func:`json.loads` or ``default`` argument
|
||||
for :func:`json.dump`/:func:`json.dumps`.
|
||||
|
||||
Interestingly, ``default`` is required to perform only partial
|
||||
serialization, as :func:`json.dumps` applies ``default``
|
||||
recursively. This is the idea behind making :meth:`to_partial_json`
|
||||
produce only partial serialization, while providing custom
|
||||
:meth:`json_dumps` that dumps with ``default`` set to
|
||||
:meth:`json_dump_default`.
|
||||
|
||||
To make further documentation a bit more concrete, please, consider
|
||||
the following imaginatory implementation example::
|
||||
|
||||
class Foo(JSONDeSerializable):
|
||||
def to_partial_json(self):
|
||||
return 'foo'
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return Foo()
|
||||
|
||||
class Bar(JSONDeSerializable):
|
||||
def to_partial_json(self):
|
||||
return [Foo(), Foo()]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return Bar()
|
||||
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
def to_partial_json(self): # pragma: no cover
|
||||
"""Partially serialize.
|
||||
|
||||
Following the example, **partial serialization** means the following::
|
||||
|
||||
assert isinstance(Bar().to_partial_json()[0], Foo)
|
||||
assert isinstance(Bar().to_partial_json()[1], Foo)
|
||||
|
||||
# in particular...
|
||||
assert Bar().to_partial_json() != ['foo', 'foo']
|
||||
|
||||
:raises acme.jose.errors.SerializationError:
|
||||
in case of any serialization error.
|
||||
:returns: Partially serializable object.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def to_json(self):
|
||||
"""Fully serialize.
|
||||
|
||||
Again, following the example from before, **full serialization**
|
||||
means the following::
|
||||
|
||||
assert Bar().to_json() == ['foo', 'foo']
|
||||
|
||||
:raises acme.jose.errors.SerializationError:
|
||||
in case of any serialization error.
|
||||
:returns: Fully serialized object.
|
||||
|
||||
"""
|
||||
def _serialize(obj):
|
||||
if isinstance(obj, JSONDeSerializable):
|
||||
return _serialize(obj.to_partial_json())
|
||||
if isinstance(obj, six.string_types): # strings are Sequence
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [_serialize(subobj) for subobj in obj]
|
||||
elif isinstance(obj, collections.Sequence):
|
||||
# default to tuple, otherwise Mapping could get
|
||||
# unhashable list
|
||||
return tuple(_serialize(subobj) for subobj in obj)
|
||||
elif isinstance(obj, collections.Mapping):
|
||||
return dict((_serialize(key), _serialize(value))
|
||||
for key, value in six.iteritems(obj))
|
||||
else:
|
||||
return obj
|
||||
|
||||
return _serialize(self)
|
||||
|
||||
@util.abstractclassmethod
|
||||
def from_json(cls, jobj): # pylint: disable=unused-argument
|
||||
"""Deserialize a decoded JSON document.
|
||||
|
||||
:param jobj: Python object, composed of only other basic data
|
||||
types, as decoded from JSON document. Not necessarily
|
||||
:class:`dict` (as decoded from "JSON object" document).
|
||||
|
||||
:raises acme.jose.errors.DeserializationError:
|
||||
if decoding was unsuccessful, e.g. in case of unparseable
|
||||
X509 certificate, or wrong padding in JOSE base64 encoded
|
||||
string, etc.
|
||||
|
||||
"""
|
||||
# TypeError: Can't instantiate abstract class <cls> with
|
||||
# abstract methods from_json, to_partial_json
|
||||
return cls() # pylint: disable=abstract-class-instantiated
|
||||
|
||||
@classmethod
|
||||
def json_loads(cls, json_string):
|
||||
"""Deserialize from JSON document string."""
|
||||
try:
|
||||
loads = json.loads(json_string)
|
||||
except ValueError as error:
|
||||
raise errors.DeserializationError(error)
|
||||
return cls.from_json(loads)
|
||||
|
||||
def json_dumps(self, **kwargs):
|
||||
"""Dump to JSON string using proper serializer.
|
||||
|
||||
:returns: JSON document string.
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return json.dumps(self, default=self.json_dump_default, **kwargs)
|
||||
|
||||
def json_dumps_pretty(self):
|
||||
"""Dump the object to pretty JSON document string.
|
||||
|
||||
:rtype: str
|
||||
|
||||
"""
|
||||
return self.json_dumps(sort_keys=True, indent=4, separators=(',', ': '))
|
||||
|
||||
@classmethod
|
||||
def json_dump_default(cls, python_object):
|
||||
"""Serialize Python object.
|
||||
|
||||
This function is meant to be passed as ``default`` to
|
||||
:func:`json.dump` or :func:`json.dumps`. They call
|
||||
``default(python_object)`` only for non-basic Python types, so
|
||||
this function necessarily raises :class:`TypeError` if
|
||||
``python_object`` is not an instance of
|
||||
:class:`IJSONSerializable`.
|
||||
|
||||
Please read the class docstring for more information.
|
||||
|
||||
"""
|
||||
if isinstance(python_object, JSONDeSerializable):
|
||||
return python_object.to_partial_json()
|
||||
else: # this branch is necessary, cannot just "return"
|
||||
raise TypeError(repr(python_object) + ' is not JSON serializable')
|
||||
@@ -1,114 +0,0 @@
|
||||
"""Tests for acme.jose.interfaces."""
|
||||
import unittest
|
||||
|
||||
|
||||
class JSONDeSerializableTest(unittest.TestCase):
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
|
||||
# pylint: disable=missing-docstring,invalid-name
|
||||
|
||||
class Basic(JSONDeSerializable):
|
||||
def __init__(self, v):
|
||||
self.v = v
|
||||
|
||||
def to_partial_json(self):
|
||||
return self.v
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return cls(jobj)
|
||||
|
||||
class Sequence(JSONDeSerializable):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_partial_json(self):
|
||||
return [self.x, self.y]
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return cls(
|
||||
Basic.from_json(jobj[0]), Basic.from_json(jobj[1]))
|
||||
|
||||
class Mapping(JSONDeSerializable):
|
||||
def __init__(self, x, y):
|
||||
self.x = x
|
||||
self.y = y
|
||||
|
||||
def to_partial_json(self):
|
||||
return {self.x: self.y}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
pass # pragma: no cover
|
||||
|
||||
self.basic1 = Basic('foo1')
|
||||
self.basic2 = Basic('foo2')
|
||||
self.seq = Sequence(self.basic1, self.basic2)
|
||||
self.mapping = Mapping(self.basic1, self.basic2)
|
||||
self.nested = Basic([[self.basic1]])
|
||||
self.tuple = Basic(('foo',))
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
self.Basic = Basic
|
||||
self.Sequence = Sequence
|
||||
self.Mapping = Mapping
|
||||
|
||||
def test_to_json_sequence(self):
|
||||
self.assertEqual(self.seq.to_json(), ['foo1', 'foo2'])
|
||||
|
||||
def test_to_json_mapping(self):
|
||||
self.assertEqual(self.mapping.to_json(), {'foo1': 'foo2'})
|
||||
|
||||
def test_to_json_other(self):
|
||||
mock_value = object()
|
||||
self.assertTrue(self.Basic(mock_value).to_json() is mock_value)
|
||||
|
||||
def test_to_json_nested(self):
|
||||
self.assertEqual(self.nested.to_json(), [['foo1']])
|
||||
|
||||
def test_to_json(self):
|
||||
self.assertEqual(self.tuple.to_json(), (('foo', )))
|
||||
|
||||
def test_from_json_not_implemented(self):
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
self.assertRaises(TypeError, JSONDeSerializable.from_json, 'xxx')
|
||||
|
||||
def test_json_loads(self):
|
||||
seq = self.Sequence.json_loads('["foo1", "foo2"]')
|
||||
self.assertTrue(isinstance(seq, self.Sequence))
|
||||
self.assertTrue(isinstance(seq.x, self.Basic))
|
||||
self.assertTrue(isinstance(seq.y, self.Basic))
|
||||
self.assertEqual(seq.x.v, 'foo1')
|
||||
self.assertEqual(seq.y.v, 'foo2')
|
||||
|
||||
def test_json_dumps(self):
|
||||
self.assertEqual('["foo1", "foo2"]', self.seq.json_dumps())
|
||||
|
||||
def test_json_dumps_pretty(self):
|
||||
self.assertEqual(self.seq.json_dumps_pretty(),
|
||||
'[\n "foo1",\n "foo2"\n]')
|
||||
|
||||
def test_json_dump_default(self):
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
|
||||
self.assertEqual(
|
||||
'foo1', JSONDeSerializable.json_dump_default(self.basic1))
|
||||
|
||||
jobj = JSONDeSerializable.json_dump_default(self.seq)
|
||||
self.assertEqual(len(jobj), 2)
|
||||
self.assertTrue(jobj[0] is self.basic1)
|
||||
self.assertTrue(jobj[1] is self.basic2)
|
||||
|
||||
def test_json_dump_default_type_error(self):
|
||||
from acme.jose.interfaces import JSONDeSerializable
|
||||
self.assertRaises(
|
||||
TypeError, JSONDeSerializable.json_dump_default, object())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,485 +0,0 @@
|
||||
"""JSON (de)serialization framework.
|
||||
|
||||
The framework presented here is somewhat based on `Go's "json" package`_
|
||||
(especially the ``omitempty`` functionality).
|
||||
|
||||
.. _`Go's "json" package`: http://golang.org/pkg/encoding/json/
|
||||
|
||||
"""
|
||||
import abc
|
||||
import binascii
|
||||
import logging
|
||||
|
||||
import OpenSSL
|
||||
import six
|
||||
|
||||
from acme.jose import b64
|
||||
from acme.jose import errors
|
||||
from acme.jose import interfaces
|
||||
from acme.jose import util
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Field(object):
|
||||
"""JSON object field.
|
||||
|
||||
:class:`Field` is meant to be used together with
|
||||
:class:`JSONObjectWithFields`.
|
||||
|
||||
``encoder`` (``decoder``) is a callable that accepts a single
|
||||
parameter, i.e. a value to be encoded (decoded), and returns the
|
||||
serialized (deserialized) value. In case of errors it should raise
|
||||
:class:`~acme.jose.errors.SerializationError`
|
||||
(:class:`~acme.jose.errors.DeserializationError`).
|
||||
|
||||
Note, that ``decoder`` should perform partial serialization only.
|
||||
|
||||
:ivar str json_name: Name of the field when encoded to JSON.
|
||||
:ivar default: Default value (used when not present in JSON object).
|
||||
:ivar bool omitempty: If ``True`` and the field value is empty, then
|
||||
it will not be included in the serialized JSON object, and
|
||||
``default`` will be used for deserialization. Otherwise, if ``False``,
|
||||
field is considered as required, value will always be included in the
|
||||
serialized JSON objected, and it must also be present when
|
||||
deserializing.
|
||||
|
||||
"""
|
||||
__slots__ = ('json_name', 'default', 'omitempty', 'fdec', 'fenc')
|
||||
|
||||
def __init__(self, json_name, default=None, omitempty=False,
|
||||
decoder=None, encoder=None):
|
||||
# pylint: disable=too-many-arguments
|
||||
self.json_name = json_name
|
||||
self.default = default
|
||||
self.omitempty = omitempty
|
||||
|
||||
self.fdec = self.default_decoder if decoder is None else decoder
|
||||
self.fenc = self.default_encoder if encoder is None else encoder
|
||||
|
||||
@classmethod
|
||||
def _empty(cls, value):
|
||||
"""Is the provided value considered "empty" for this field?
|
||||
|
||||
This is useful for subclasses that might want to override the
|
||||
definition of being empty, e.g. for some more exotic data types.
|
||||
|
||||
"""
|
||||
return not isinstance(value, bool) and not value
|
||||
|
||||
def omit(self, value):
|
||||
"""Omit the value in output?"""
|
||||
return self._empty(value) and self.omitempty
|
||||
|
||||
def _update_params(self, **kwargs):
|
||||
current = dict(json_name=self.json_name, default=self.default,
|
||||
omitempty=self.omitempty,
|
||||
decoder=self.fdec, encoder=self.fenc)
|
||||
current.update(kwargs)
|
||||
return type(self)(**current) # pylint: disable=star-args
|
||||
|
||||
def decoder(self, fdec):
|
||||
"""Descriptor to change the decoder on JSON object field."""
|
||||
return self._update_params(decoder=fdec)
|
||||
|
||||
def encoder(self, fenc):
|
||||
"""Descriptor to change the encoder on JSON object field."""
|
||||
return self._update_params(encoder=fenc)
|
||||
|
||||
def decode(self, value):
|
||||
"""Decode a value, optionally with context JSON object."""
|
||||
return self.fdec(value)
|
||||
|
||||
def encode(self, value):
|
||||
"""Encode a value, optionally with context JSON object."""
|
||||
return self.fenc(value)
|
||||
|
||||
@classmethod
|
||||
def default_decoder(cls, value):
|
||||
"""Default decoder.
|
||||
|
||||
Recursively deserialize into immutable types (
|
||||
:class:`acme.jose.util.frozendict` instead of
|
||||
:func:`dict`, :func:`tuple` instead of :func:`list`).
|
||||
|
||||
"""
|
||||
# bases cases for different types returned by json.loads
|
||||
if isinstance(value, list):
|
||||
return tuple(cls.default_decoder(subvalue) for subvalue in value)
|
||||
elif isinstance(value, dict):
|
||||
return util.frozendict(
|
||||
dict((cls.default_decoder(key), cls.default_decoder(value))
|
||||
for key, value in six.iteritems(value)))
|
||||
else: # integer or string
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def default_encoder(cls, value):
|
||||
"""Default (passthrough) encoder."""
|
||||
# field.to_partial_json() is no good as encoder has to do partial
|
||||
# serialization only
|
||||
return value
|
||||
|
||||
|
||||
class JSONObjectWithFieldsMeta(abc.ABCMeta):
|
||||
"""Metaclass for :class:`JSONObjectWithFields` and its subclasses.
|
||||
|
||||
It makes sure that, for any class ``cls`` with ``__metaclass__``
|
||||
set to ``JSONObjectWithFieldsMeta``:
|
||||
|
||||
1. All fields (attributes of type :class:`Field`) in the class
|
||||
definition are moved to the ``cls._fields`` dictionary, where
|
||||
keys are field attribute names and values are fields themselves.
|
||||
|
||||
2. ``cls.__slots__`` is extended by all field attribute names
|
||||
(i.e. not :attr:`Field.json_name`). Original ``cls.__slots__``
|
||||
are stored in ``cls._orig_slots``.
|
||||
|
||||
In a consequence, for a field attribute name ``some_field``,
|
||||
``cls.some_field`` will be a slot descriptor and not an instance
|
||||
of :class:`Field`. For example::
|
||||
|
||||
some_field = Field('someField', default=())
|
||||
|
||||
class Foo(object):
|
||||
__metaclass__ = JSONObjectWithFieldsMeta
|
||||
__slots__ = ('baz',)
|
||||
some_field = some_field
|
||||
|
||||
assert Foo.__slots__ == ('some_field', 'baz')
|
||||
assert Foo._orig_slots == ()
|
||||
assert Foo.some_field is not Field
|
||||
|
||||
assert Foo._fields.keys() == ['some_field']
|
||||
assert Foo._fields['some_field'] is some_field
|
||||
|
||||
As an implementation note, this metaclass inherits from
|
||||
:class:`abc.ABCMeta` (and not the usual :class:`type`) to mitigate
|
||||
the metaclass conflict (:class:`ImmutableMap` and
|
||||
:class:`JSONDeSerializable`, parents of :class:`JSONObjectWithFields`,
|
||||
use :class:`abc.ABCMeta` as its metaclass).
|
||||
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, dikt):
|
||||
fields = {}
|
||||
|
||||
for base in bases:
|
||||
fields.update(getattr(base, '_fields', {}))
|
||||
# Do not reorder, this class might override fields from base classes!
|
||||
for key, value in tuple(six.iteritems(dikt)):
|
||||
# not six.iterkeys() (in-place edit!)
|
||||
if isinstance(value, Field):
|
||||
fields[key] = dikt.pop(key)
|
||||
|
||||
dikt['_orig_slots'] = dikt.get('__slots__', ())
|
||||
dikt['__slots__'] = tuple(
|
||||
list(dikt['_orig_slots']) + list(six.iterkeys(fields)))
|
||||
dikt['_fields'] = fields
|
||||
|
||||
return abc.ABCMeta.__new__(mcs, name, bases, dikt)
|
||||
|
||||
|
||||
@six.add_metaclass(JSONObjectWithFieldsMeta)
|
||||
class JSONObjectWithFields(util.ImmutableMap, interfaces.JSONDeSerializable):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""JSON object with fields.
|
||||
|
||||
Example::
|
||||
|
||||
class Foo(JSONObjectWithFields):
|
||||
bar = Field('Bar')
|
||||
empty = Field('Empty', omitempty=True)
|
||||
|
||||
@bar.encoder
|
||||
def bar(value):
|
||||
return value + 'bar'
|
||||
|
||||
@bar.decoder
|
||||
def bar(value):
|
||||
if not value.endswith('bar'):
|
||||
raise errors.DeserializationError('No bar suffix!')
|
||||
return value[:-3]
|
||||
|
||||
assert Foo(bar='baz').to_partial_json() == {'Bar': 'bazbar'}
|
||||
assert Foo.from_json({'Bar': 'bazbar'}) == Foo(bar='baz')
|
||||
assert (Foo.from_json({'Bar': 'bazbar', 'Empty': '!'})
|
||||
== Foo(bar='baz', empty='!'))
|
||||
assert Foo(bar='baz').bar == 'baz'
|
||||
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def _defaults(cls):
|
||||
"""Get default fields values."""
|
||||
return dict([(slot, field.default) for slot, field
|
||||
in six.iteritems(cls._fields)])
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# pylint: disable=star-args
|
||||
super(JSONObjectWithFields, self).__init__(
|
||||
**(dict(self._defaults(), **kwargs)))
|
||||
|
||||
def encode(self, name):
|
||||
"""Encode a single field.
|
||||
|
||||
:param str name: Name of the field to be encoded.
|
||||
|
||||
:raises errors.SerializationError: if field cannot be serialized
|
||||
:raises errors.Error: if field could not be found
|
||||
|
||||
"""
|
||||
try:
|
||||
field = self._fields[name]
|
||||
except KeyError:
|
||||
raise errors.Error("Field not found: {0}".format(name))
|
||||
|
||||
return field.encode(getattr(self, name))
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
"""Serialize fields to JSON."""
|
||||
jobj = {}
|
||||
omitted = set()
|
||||
for slot, field in six.iteritems(self._fields):
|
||||
value = getattr(self, slot)
|
||||
|
||||
if field.omit(value):
|
||||
omitted.add((slot, value))
|
||||
else:
|
||||
try:
|
||||
jobj[field.json_name] = field.encode(value)
|
||||
except errors.SerializationError as error:
|
||||
raise errors.SerializationError(
|
||||
'Could not encode {0} ({1}): {2}'.format(
|
||||
slot, value, error))
|
||||
return jobj
|
||||
|
||||
def to_partial_json(self):
|
||||
return self.fields_to_partial_json()
|
||||
|
||||
@classmethod
|
||||
def _check_required(cls, jobj):
|
||||
missing = set()
|
||||
for _, field in six.iteritems(cls._fields):
|
||||
if not field.omitempty and field.json_name not in jobj:
|
||||
missing.add(field.json_name)
|
||||
|
||||
if missing:
|
||||
raise errors.DeserializationError(
|
||||
'The following fields are required: {0}'.format(
|
||||
','.join(missing)))
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
"""Deserialize fields from JSON."""
|
||||
cls._check_required(jobj)
|
||||
fields = {}
|
||||
for slot, field in six.iteritems(cls._fields):
|
||||
if field.json_name not in jobj and field.omitempty:
|
||||
fields[slot] = field.default
|
||||
else:
|
||||
value = jobj[field.json_name]
|
||||
try:
|
||||
fields[slot] = field.decode(value)
|
||||
except errors.DeserializationError as error:
|
||||
raise errors.DeserializationError(
|
||||
'Could not decode {0!r} ({1!r}): {2}'.format(
|
||||
slot, value, error))
|
||||
return fields
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return cls(**cls.fields_from_json(jobj))
|
||||
|
||||
|
||||
def encode_b64jose(data):
|
||||
"""Encode JOSE Base-64 field.
|
||||
|
||||
:param bytes data:
|
||||
:rtype: `unicode`
|
||||
|
||||
"""
|
||||
# b64encode produces ASCII characters only
|
||||
return b64.b64encode(data).decode('ascii')
|
||||
|
||||
|
||||
def decode_b64jose(data, size=None, minimum=False):
|
||||
"""Decode JOSE Base-64 field.
|
||||
|
||||
:param unicode data:
|
||||
:param int size: Required length (after decoding).
|
||||
:param bool minimum: If ``True``, then `size` will be treated as
|
||||
minimum required length, as opposed to exact equality.
|
||||
|
||||
:rtype: bytes
|
||||
|
||||
"""
|
||||
error_cls = TypeError if six.PY2 else binascii.Error
|
||||
try:
|
||||
decoded = b64.b64decode(data.encode())
|
||||
except error_cls as error:
|
||||
raise errors.DeserializationError(error)
|
||||
|
||||
if size is not None and ((not minimum and len(decoded) != size) or
|
||||
(minimum and len(decoded) < size)):
|
||||
raise errors.DeserializationError(
|
||||
"Expected at least or exactly {0} bytes".format(size))
|
||||
|
||||
return decoded
|
||||
|
||||
|
||||
def encode_hex16(value):
|
||||
"""Hexlify.
|
||||
|
||||
:param bytes value:
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
return binascii.hexlify(value).decode()
|
||||
|
||||
|
||||
def decode_hex16(value, size=None, minimum=False):
|
||||
"""Decode hexlified field.
|
||||
|
||||
:param unicode value:
|
||||
:param int size: Required length (after decoding).
|
||||
:param bool minimum: If ``True``, then `size` will be treated as
|
||||
minimum required length, as opposed to exact equality.
|
||||
|
||||
:rtype: bytes
|
||||
|
||||
"""
|
||||
value = value.encode()
|
||||
if size is not None and ((not minimum and len(value) != size * 2) or
|
||||
(minimum and len(value) < size * 2)):
|
||||
raise errors.DeserializationError()
|
||||
error_cls = TypeError if six.PY2 else binascii.Error
|
||||
try:
|
||||
return binascii.unhexlify(value)
|
||||
except error_cls as error:
|
||||
raise errors.DeserializationError(error)
|
||||
|
||||
|
||||
def encode_cert(cert):
|
||||
"""Encode certificate as JOSE Base-64 DER.
|
||||
|
||||
:type cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
return encode_b64jose(OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped))
|
||||
|
||||
|
||||
def decode_cert(b64der):
|
||||
"""Decode JOSE Base-64 DER-encoded certificate.
|
||||
|
||||
:param unicode b64der:
|
||||
:rtype: `OpenSSL.crypto.X509` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
try:
|
||||
return util.ComparableX509(OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der)))
|
||||
except OpenSSL.crypto.Error as error:
|
||||
raise errors.DeserializationError(error)
|
||||
|
||||
|
||||
def encode_csr(csr):
|
||||
"""Encode CSR as JOSE Base-64 DER.
|
||||
|
||||
:type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
return encode_b64jose(OpenSSL.crypto.dump_certificate_request(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, csr.wrapped))
|
||||
|
||||
|
||||
def decode_csr(b64der):
|
||||
"""Decode JOSE Base-64 DER-encoded CSR.
|
||||
|
||||
:param unicode b64der:
|
||||
:rtype: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
try:
|
||||
return util.ComparableX509(OpenSSL.crypto.load_certificate_request(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, decode_b64jose(b64der)))
|
||||
except OpenSSL.crypto.Error as error:
|
||||
raise errors.DeserializationError(error)
|
||||
|
||||
|
||||
class TypedJSONObjectWithFields(JSONObjectWithFields):
|
||||
"""JSON object with type."""
|
||||
|
||||
typ = NotImplemented
|
||||
"""Type of the object. Subclasses must override."""
|
||||
|
||||
type_field_name = "type"
|
||||
"""Field name used to distinguish different object types.
|
||||
|
||||
Subclasses will probably have to override this.
|
||||
|
||||
"""
|
||||
|
||||
TYPES = NotImplemented
|
||||
"""Types registered for JSON deserialization"""
|
||||
|
||||
@classmethod
|
||||
def register(cls, type_cls, typ=None):
|
||||
"""Register class for JSON deserialization."""
|
||||
typ = type_cls.typ if typ is None else typ
|
||||
cls.TYPES[typ] = type_cls
|
||||
return type_cls
|
||||
|
||||
@classmethod
|
||||
def get_type_cls(cls, jobj):
|
||||
"""Get the registered class for ``jobj``."""
|
||||
if cls in six.itervalues(cls.TYPES):
|
||||
if cls.type_field_name not in jobj:
|
||||
raise errors.DeserializationError(
|
||||
"Missing type field ({0})".format(cls.type_field_name))
|
||||
# cls is already registered type_cls, force to use it
|
||||
# so that, e.g Revocation.from_json(jobj) fails if
|
||||
# jobj["type"] != "revocation".
|
||||
return cls
|
||||
|
||||
if not isinstance(jobj, dict):
|
||||
raise errors.DeserializationError(
|
||||
"{0} is not a dictionary object".format(jobj))
|
||||
try:
|
||||
typ = jobj[cls.type_field_name]
|
||||
except KeyError:
|
||||
raise errors.DeserializationError("missing type field")
|
||||
|
||||
try:
|
||||
return cls.TYPES[typ]
|
||||
except KeyError:
|
||||
raise errors.UnrecognizedTypeError(typ, jobj)
|
||||
|
||||
def to_partial_json(self):
|
||||
"""Get JSON serializable object.
|
||||
|
||||
:returns: Serializable JSON object representing ACME typed object.
|
||||
:meth:`validate` will almost certainly not work, due to reasons
|
||||
explained in :class:`acme.interfaces.IJSONSerializable`.
|
||||
:rtype: dict
|
||||
|
||||
"""
|
||||
jobj = self.fields_to_partial_json()
|
||||
jobj[self.type_field_name] = self.typ
|
||||
return jobj
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
"""Deserialize ACME object from valid JSON object.
|
||||
|
||||
:raises acme.errors.UnrecognizedTypeError: if type
|
||||
of the ACME object has not been registered.
|
||||
|
||||
"""
|
||||
# make sure subclasses don't cause infinite recursive from_json calls
|
||||
type_cls = cls.get_type_cls(jobj)
|
||||
return type_cls(**type_cls.fields_from_json(jobj))
|
||||
@@ -1,381 +0,0 @@
|
||||
"""Tests for acme.jose.json_util."""
|
||||
import itertools
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import six
|
||||
|
||||
from acme import test_util
|
||||
|
||||
from acme.jose import errors
|
||||
from acme.jose import interfaces
|
||||
from acme.jose import util
|
||||
|
||||
|
||||
CERT = test_util.load_comparable_cert('cert.pem')
|
||||
CSR = test_util.load_comparable_csr('csr.pem')
|
||||
|
||||
|
||||
class FieldTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.json_util.Field."""
|
||||
|
||||
def test_no_omit_boolean(self):
|
||||
from acme.jose.json_util import Field
|
||||
for default, omitempty, value in itertools.product(
|
||||
[True, False], [True, False], [True, False]):
|
||||
self.assertFalse(
|
||||
Field("foo", default=default, omitempty=omitempty).omit(value))
|
||||
|
||||
def test_descriptors(self):
|
||||
mock_value = mock.MagicMock()
|
||||
|
||||
# pylint: disable=missing-docstring
|
||||
|
||||
def decoder(unused_value):
|
||||
return 'd'
|
||||
|
||||
def encoder(unused_value):
|
||||
return 'e'
|
||||
|
||||
from acme.jose.json_util import Field
|
||||
field = Field('foo')
|
||||
|
||||
field = field.encoder(encoder)
|
||||
self.assertEqual('e', field.encode(mock_value))
|
||||
|
||||
field = field.decoder(decoder)
|
||||
self.assertEqual('e', field.encode(mock_value))
|
||||
self.assertEqual('d', field.decode(mock_value))
|
||||
|
||||
def test_default_encoder_is_partial(self):
|
||||
class MockField(interfaces.JSONDeSerializable):
|
||||
# pylint: disable=missing-docstring
|
||||
def to_partial_json(self):
|
||||
return 'foo' # pragma: no cover
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
pass # pragma: no cover
|
||||
mock_field = MockField()
|
||||
|
||||
from acme.jose.json_util import Field
|
||||
self.assertTrue(Field.default_encoder(mock_field) is mock_field)
|
||||
# in particular...
|
||||
self.assertNotEqual('foo', Field.default_encoder(mock_field))
|
||||
|
||||
def test_default_encoder_passthrough(self):
|
||||
mock_value = mock.MagicMock()
|
||||
from acme.jose.json_util import Field
|
||||
self.assertTrue(Field.default_encoder(mock_value) is mock_value)
|
||||
|
||||
def test_default_decoder_list_to_tuple(self):
|
||||
from acme.jose.json_util import Field
|
||||
self.assertEqual((1, 2, 3), Field.default_decoder([1, 2, 3]))
|
||||
|
||||
def test_default_decoder_dict_to_frozendict(self):
|
||||
from acme.jose.json_util import Field
|
||||
obj = Field.default_decoder({'x': 2})
|
||||
self.assertTrue(isinstance(obj, util.frozendict))
|
||||
self.assertEqual(obj, util.frozendict(x=2))
|
||||
|
||||
def test_default_decoder_passthrough(self):
|
||||
mock_value = mock.MagicMock()
|
||||
from acme.jose.json_util import Field
|
||||
self.assertTrue(Field.default_decoder(mock_value) is mock_value)
|
||||
|
||||
|
||||
class JSONObjectWithFieldsMetaTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.json_util.JSONObjectWithFieldsMeta."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.json_util import Field
|
||||
from acme.jose.json_util import JSONObjectWithFieldsMeta
|
||||
self.field = Field('Baz')
|
||||
self.field2 = Field('Baz2')
|
||||
# pylint: disable=invalid-name,missing-docstring,too-few-public-methods
|
||||
# pylint: disable=blacklisted-name
|
||||
|
||||
@six.add_metaclass(JSONObjectWithFieldsMeta)
|
||||
class A(object):
|
||||
__slots__ = ('bar',)
|
||||
baz = self.field
|
||||
|
||||
class B(A):
|
||||
pass
|
||||
|
||||
class C(A):
|
||||
baz = self.field2
|
||||
|
||||
self.a_cls = A
|
||||
self.b_cls = B
|
||||
self.c_cls = C
|
||||
|
||||
def test_fields(self):
|
||||
# pylint: disable=protected-access,no-member
|
||||
self.assertEqual({'baz': self.field}, self.a_cls._fields)
|
||||
self.assertEqual({'baz': self.field}, self.b_cls._fields)
|
||||
|
||||
def test_fields_inheritance(self):
|
||||
# pylint: disable=protected-access,no-member
|
||||
self.assertEqual({'baz': self.field2}, self.c_cls._fields)
|
||||
|
||||
def test_slots(self):
|
||||
self.assertEqual(('bar', 'baz'), self.a_cls.__slots__)
|
||||
self.assertEqual(('baz',), self.b_cls.__slots__)
|
||||
|
||||
def test_orig_slots(self):
|
||||
# pylint: disable=protected-access,no-member
|
||||
self.assertEqual(('bar',), self.a_cls._orig_slots)
|
||||
self.assertEqual((), self.b_cls._orig_slots)
|
||||
|
||||
|
||||
class JSONObjectWithFieldsTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.json_util.JSONObjectWithFields."""
|
||||
# pylint: disable=protected-access
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.json_util import JSONObjectWithFields
|
||||
from acme.jose.json_util import Field
|
||||
|
||||
class MockJSONObjectWithFields(JSONObjectWithFields):
|
||||
# pylint: disable=invalid-name,missing-docstring,no-self-argument
|
||||
# pylint: disable=too-few-public-methods
|
||||
x = Field('x', omitempty=True,
|
||||
encoder=(lambda x: x * 2),
|
||||
decoder=(lambda x: x / 2))
|
||||
y = Field('y')
|
||||
z = Field('Z') # on purpose uppercase
|
||||
|
||||
@y.encoder
|
||||
def y(value):
|
||||
if value == 500:
|
||||
raise errors.SerializationError()
|
||||
return value
|
||||
|
||||
@y.decoder
|
||||
def y(value):
|
||||
if value == 500:
|
||||
raise errors.DeserializationError()
|
||||
return value
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
self.MockJSONObjectWithFields = MockJSONObjectWithFields
|
||||
self.mock = MockJSONObjectWithFields(x=None, y=2, z=3)
|
||||
|
||||
def test_init_defaults(self):
|
||||
self.assertEqual(self.mock, self.MockJSONObjectWithFields(y=2, z=3))
|
||||
|
||||
def test_encode(self):
|
||||
self.assertEqual(10, self.MockJSONObjectWithFields(
|
||||
x=5, y=0, z=0).encode("x"))
|
||||
|
||||
def test_encode_wrong_field(self):
|
||||
self.assertRaises(errors.Error, self.mock.encode, 'foo')
|
||||
|
||||
def test_encode_serialization_error_passthrough(self):
|
||||
self.assertRaises(
|
||||
errors.SerializationError,
|
||||
self.MockJSONObjectWithFields(y=500, z=None).encode, "y")
|
||||
|
||||
def test_fields_to_partial_json_omits_empty(self):
|
||||
self.assertEqual(self.mock.fields_to_partial_json(), {'y': 2, 'Z': 3})
|
||||
|
||||
def test_fields_from_json_fills_default_for_empty(self):
|
||||
self.assertEqual(
|
||||
{'x': None, 'y': 2, 'z': 3},
|
||||
self.MockJSONObjectWithFields.fields_from_json({'y': 2, 'Z': 3}))
|
||||
|
||||
def test_fields_from_json_fails_on_missing(self):
|
||||
self.assertRaises(
|
||||
errors.DeserializationError,
|
||||
self.MockJSONObjectWithFields.fields_from_json, {'y': 0})
|
||||
self.assertRaises(
|
||||
errors.DeserializationError,
|
||||
self.MockJSONObjectWithFields.fields_from_json, {'Z': 0})
|
||||
self.assertRaises(
|
||||
errors.DeserializationError,
|
||||
self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'y': 0})
|
||||
self.assertRaises(
|
||||
errors.DeserializationError,
|
||||
self.MockJSONObjectWithFields.fields_from_json, {'x': 0, 'Z': 0})
|
||||
|
||||
def test_fields_to_partial_json_encoder(self):
|
||||
self.assertEqual(
|
||||
self.MockJSONObjectWithFields(x=1, y=2, z=3).to_partial_json(),
|
||||
{'x': 2, 'y': 2, 'Z': 3})
|
||||
|
||||
def test_fields_from_json_decoder(self):
|
||||
self.assertEqual(
|
||||
{'x': 2, 'y': 2, 'z': 3},
|
||||
self.MockJSONObjectWithFields.fields_from_json(
|
||||
{'x': 4, 'y': 2, 'Z': 3}))
|
||||
|
||||
def test_fields_to_partial_json_error_passthrough(self):
|
||||
self.assertRaises(
|
||||
errors.SerializationError, self.MockJSONObjectWithFields(
|
||||
x=1, y=500, z=3).to_partial_json)
|
||||
|
||||
def test_fields_from_json_error_passthrough(self):
|
||||
self.assertRaises(
|
||||
errors.DeserializationError,
|
||||
self.MockJSONObjectWithFields.from_json,
|
||||
{'x': 4, 'y': 500, 'Z': 3})
|
||||
|
||||
|
||||
class DeEncodersTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.b64_cert = (
|
||||
u'MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhM'
|
||||
u'CVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKz'
|
||||
u'ApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxF'
|
||||
u'DASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIx'
|
||||
u'ODIyMzQ0NVowdzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRI'
|
||||
u'wEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTW'
|
||||
u'ljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4YW1wbGUuY29tMFwwD'
|
||||
u'QYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR7R_drnBSQ_zfx1vQLHUbFLh1'
|
||||
u'AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c-pVE6K-EdE_twuUCAwE'
|
||||
u'AATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksllvr6zJepBH5fMnd'
|
||||
u'fk3XJp10jT6VE-14KNtjh02a56GoraAvJAT5_H67E8GvJ_ocNnB_o'
|
||||
)
|
||||
self.b64_csr = (
|
||||
u'MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2F'
|
||||
u'uMRIwEAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECw'
|
||||
u'wWVW5pdmVyc2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb'
|
||||
u'20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD_N_HW9As'
|
||||
u'dRsUuHUBBBDlHwNlRd3fp580rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3'
|
||||
u'C5QIDAQABoCkwJwYJKoZIhvcNAQkOMRowGDAWBgNVHREEDzANggtleGFtcG'
|
||||
u'xlLmNvbTANBgkqhkiG9w0BAQsFAANBAHJH_O6BtC9aGzEVCMGOZ7z9iIRHW'
|
||||
u'Szr9x_bOzn7hLwsbXPAgO1QxEwL-X-4g20Gn9XBE1N9W6HCIEut2d8wACg'
|
||||
)
|
||||
|
||||
def test_encode_b64jose(self):
|
||||
from acme.jose.json_util import encode_b64jose
|
||||
encoded = encode_b64jose(b'x')
|
||||
self.assertTrue(isinstance(encoded, six.string_types))
|
||||
self.assertEqual(u'eA', encoded)
|
||||
|
||||
def test_decode_b64jose(self):
|
||||
from acme.jose.json_util import decode_b64jose
|
||||
decoded = decode_b64jose(u'eA')
|
||||
self.assertTrue(isinstance(decoded, six.binary_type))
|
||||
self.assertEqual(b'x', decoded)
|
||||
|
||||
def test_decode_b64jose_padding_error(self):
|
||||
from acme.jose.json_util import decode_b64jose
|
||||
self.assertRaises(errors.DeserializationError, decode_b64jose, u'x')
|
||||
|
||||
def test_decode_b64jose_size(self):
|
||||
from acme.jose.json_util import decode_b64jose
|
||||
self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3))
|
||||
self.assertRaises(
|
||||
errors.DeserializationError, decode_b64jose, u'Zm9v', size=2)
|
||||
self.assertRaises(
|
||||
errors.DeserializationError, decode_b64jose, u'Zm9v', size=4)
|
||||
|
||||
def test_decode_b64jose_minimum_size(self):
|
||||
from acme.jose.json_util import decode_b64jose
|
||||
self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=3, minimum=True))
|
||||
self.assertEqual(b'foo', decode_b64jose(u'Zm9v', size=2, minimum=True))
|
||||
self.assertRaises(errors.DeserializationError, decode_b64jose,
|
||||
u'Zm9v', size=4, minimum=True)
|
||||
|
||||
def test_encode_hex16(self):
|
||||
from acme.jose.json_util import encode_hex16
|
||||
encoded = encode_hex16(b'foo')
|
||||
self.assertEqual(u'666f6f', encoded)
|
||||
self.assertTrue(isinstance(encoded, six.string_types))
|
||||
|
||||
def test_decode_hex16(self):
|
||||
from acme.jose.json_util import decode_hex16
|
||||
decoded = decode_hex16(u'666f6f')
|
||||
self.assertEqual(b'foo', decoded)
|
||||
self.assertTrue(isinstance(decoded, six.binary_type))
|
||||
|
||||
def test_decode_hex16_minimum_size(self):
|
||||
from acme.jose.json_util import decode_hex16
|
||||
self.assertEqual(b'foo', decode_hex16(u'666f6f', size=3, minimum=True))
|
||||
self.assertEqual(b'foo', decode_hex16(u'666f6f', size=2, minimum=True))
|
||||
self.assertRaises(errors.DeserializationError, decode_hex16,
|
||||
u'666f6f', size=4, minimum=True)
|
||||
|
||||
def test_decode_hex16_odd_length(self):
|
||||
from acme.jose.json_util import decode_hex16
|
||||
self.assertRaises(errors.DeserializationError, decode_hex16, u'x')
|
||||
|
||||
def test_encode_cert(self):
|
||||
from acme.jose.json_util import encode_cert
|
||||
self.assertEqual(self.b64_cert, encode_cert(CERT))
|
||||
|
||||
def test_decode_cert(self):
|
||||
from acme.jose.json_util import decode_cert
|
||||
cert = decode_cert(self.b64_cert)
|
||||
self.assertTrue(isinstance(cert, util.ComparableX509))
|
||||
self.assertEqual(cert, CERT)
|
||||
self.assertRaises(errors.DeserializationError, decode_cert, u'')
|
||||
|
||||
def test_encode_csr(self):
|
||||
from acme.jose.json_util import encode_csr
|
||||
self.assertEqual(self.b64_csr, encode_csr(CSR))
|
||||
|
||||
def test_decode_csr(self):
|
||||
from acme.jose.json_util import decode_csr
|
||||
csr = decode_csr(self.b64_csr)
|
||||
self.assertTrue(isinstance(csr, util.ComparableX509))
|
||||
self.assertEqual(csr, CSR)
|
||||
self.assertRaises(errors.DeserializationError, decode_csr, u'')
|
||||
|
||||
|
||||
class TypedJSONObjectWithFieldsTest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.json_util import TypedJSONObjectWithFields
|
||||
|
||||
# pylint: disable=missing-docstring,abstract-method
|
||||
# pylint: disable=too-few-public-methods
|
||||
|
||||
class MockParentTypedJSONObjectWithFields(TypedJSONObjectWithFields):
|
||||
TYPES = {}
|
||||
type_field_name = 'type'
|
||||
|
||||
@MockParentTypedJSONObjectWithFields.register
|
||||
class MockTypedJSONObjectWithFields(
|
||||
MockParentTypedJSONObjectWithFields):
|
||||
typ = 'test'
|
||||
__slots__ = ('foo',)
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
return {'foo': jobj['foo']}
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
return {'foo': self.foo}
|
||||
|
||||
self.parent_cls = MockParentTypedJSONObjectWithFields
|
||||
self.msg = MockTypedJSONObjectWithFields(foo='bar')
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.msg.to_partial_json(), {
|
||||
'type': 'test',
|
||||
'foo': 'bar',
|
||||
})
|
||||
|
||||
def test_from_json_non_dict_fails(self):
|
||||
for value in [[], (), 5, "asd"]: # all possible input types
|
||||
self.assertRaises(
|
||||
errors.DeserializationError, self.parent_cls.from_json, value)
|
||||
|
||||
def test_from_json_dict_no_type_fails(self):
|
||||
self.assertRaises(
|
||||
errors.DeserializationError, self.parent_cls.from_json, {})
|
||||
|
||||
def test_from_json_unknown_type_fails(self):
|
||||
self.assertRaises(errors.UnrecognizedTypeError,
|
||||
self.parent_cls.from_json, {'type': 'bar'})
|
||||
|
||||
def test_from_json_returns_obj(self):
|
||||
self.assertEqual({'foo': 'bar'}, self.parent_cls.from_json(
|
||||
{'type': 'test', 'foo': 'bar'}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,180 +0,0 @@
|
||||
"""JSON Web Algorithm.
|
||||
|
||||
https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-40
|
||||
|
||||
"""
|
||||
import abc
|
||||
import collections
|
||||
import logging
|
||||
|
||||
import cryptography.exceptions
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes # type: ignore
|
||||
from cryptography.hazmat.primitives import hmac # type: ignore
|
||||
from cryptography.hazmat.primitives.asymmetric import padding # type: ignore
|
||||
|
||||
from acme.jose import errors
|
||||
from acme.jose import interfaces
|
||||
from acme.jose import jwk
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JWA(interfaces.JSONDeSerializable): # pylint: disable=abstract-method
|
||||
# pylint: disable=too-few-public-methods
|
||||
# for some reason disable=abstract-method has to be on the line
|
||||
# above...
|
||||
"""JSON Web Algorithm."""
|
||||
|
||||
|
||||
class JWASignature(JWA, collections.Hashable): # type: ignore
|
||||
"""JSON Web Signature Algorithm."""
|
||||
SIGNATURES = {} # type: dict
|
||||
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, JWASignature):
|
||||
return NotImplemented
|
||||
return self.name == other.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.__class__, self.name))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
@classmethod
|
||||
def register(cls, signature_cls):
|
||||
"""Register class for JSON deserialization."""
|
||||
cls.SIGNATURES[signature_cls.name] = signature_cls
|
||||
return signature_cls
|
||||
|
||||
def to_partial_json(self):
|
||||
return self.name
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
return cls.SIGNATURES[jobj]
|
||||
|
||||
@abc.abstractmethod
|
||||
def sign(self, key, msg): # pragma: no cover
|
||||
"""Sign the ``msg`` using ``key``."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abc.abstractmethod
|
||||
def verify(self, key, msg, sig): # pragma: no cover
|
||||
"""Verify the ``msg` and ``sig`` using ``key``."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class _JWAHS(JWASignature):
|
||||
|
||||
kty = jwk.JWKOct
|
||||
|
||||
def __init__(self, name, hash_):
|
||||
super(_JWAHS, self).__init__(name)
|
||||
self.hash = hash_()
|
||||
|
||||
def sign(self, key, msg):
|
||||
signer = hmac.HMAC(key, self.hash, backend=default_backend())
|
||||
signer.update(msg)
|
||||
return signer.finalize()
|
||||
|
||||
def verify(self, key, msg, sig):
|
||||
verifier = hmac.HMAC(key, self.hash, backend=default_backend())
|
||||
verifier.update(msg)
|
||||
try:
|
||||
verifier.verify(sig)
|
||||
except cryptography.exceptions.InvalidSignature as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class _JWARSA(object):
|
||||
|
||||
kty = jwk.JWKRSA
|
||||
padding = NotImplemented
|
||||
hash = NotImplemented
|
||||
|
||||
def sign(self, key, msg):
|
||||
"""Sign the ``msg`` using ``key``."""
|
||||
try:
|
||||
signer = key.signer(self.padding, self.hash)
|
||||
except AttributeError as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
raise errors.Error("Public key cannot be used for signing")
|
||||
except ValueError as error: # digest too large
|
||||
logger.debug(error, exc_info=True)
|
||||
raise errors.Error(str(error))
|
||||
signer.update(msg)
|
||||
try:
|
||||
return signer.finalize()
|
||||
except ValueError as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
raise errors.Error(str(error))
|
||||
|
||||
def verify(self, key, msg, sig):
|
||||
"""Verify the ``msg` and ``sig`` using ``key``."""
|
||||
verifier = key.verifier(sig, self.padding, self.hash)
|
||||
verifier.update(msg)
|
||||
try:
|
||||
verifier.verify()
|
||||
except cryptography.exceptions.InvalidSignature as error:
|
||||
logger.debug(error, exc_info=True)
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class _JWARS(_JWARSA, JWASignature):
|
||||
|
||||
def __init__(self, name, hash_):
|
||||
super(_JWARS, self).__init__(name)
|
||||
self.padding = padding.PKCS1v15()
|
||||
self.hash = hash_()
|
||||
|
||||
|
||||
class _JWAPS(_JWARSA, JWASignature):
|
||||
|
||||
def __init__(self, name, hash_):
|
||||
super(_JWAPS, self).__init__(name)
|
||||
self.padding = padding.PSS(
|
||||
mgf=padding.MGF1(hash_()),
|
||||
salt_length=padding.PSS.MAX_LENGTH)
|
||||
self.hash = hash_()
|
||||
|
||||
|
||||
class _JWAES(JWASignature): # pylint: disable=abstract-class-not-used
|
||||
|
||||
# TODO: implement ES signatures
|
||||
|
||||
def sign(self, key, msg): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
def verify(self, key, msg, sig): # pragma: no cover
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
HS256 = JWASignature.register(_JWAHS('HS256', hashes.SHA256))
|
||||
HS384 = JWASignature.register(_JWAHS('HS384', hashes.SHA384))
|
||||
HS512 = JWASignature.register(_JWAHS('HS512', hashes.SHA512))
|
||||
|
||||
RS256 = JWASignature.register(_JWARS('RS256', hashes.SHA256))
|
||||
RS384 = JWASignature.register(_JWARS('RS384', hashes.SHA384))
|
||||
RS512 = JWASignature.register(_JWARS('RS512', hashes.SHA512))
|
||||
|
||||
PS256 = JWASignature.register(_JWAPS('PS256', hashes.SHA256))
|
||||
PS384 = JWASignature.register(_JWAPS('PS384', hashes.SHA384))
|
||||
PS512 = JWASignature.register(_JWAPS('PS512', hashes.SHA512))
|
||||
|
||||
ES256 = JWASignature.register(_JWAES('ES256'))
|
||||
ES384 = JWASignature.register(_JWAES('ES384'))
|
||||
ES512 = JWASignature.register(_JWAES('ES512'))
|
||||
@@ -1,104 +0,0 @@
|
||||
"""Tests for acme.jose.jwa."""
|
||||
import unittest
|
||||
|
||||
from acme import test_util
|
||||
|
||||
from acme.jose import errors
|
||||
|
||||
|
||||
RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem')
|
||||
RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
RSA1024_KEY = test_util.load_rsa_private_key('rsa1024_key.pem')
|
||||
|
||||
|
||||
class JWASignatureTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.jwa.JWASignature."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.jwa import JWASignature
|
||||
|
||||
class MockSig(JWASignature):
|
||||
# pylint: disable=missing-docstring,too-few-public-methods
|
||||
# pylint: disable=abstract-class-not-used
|
||||
def sign(self, key, msg):
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
|
||||
def verify(self, key, msg, sig):
|
||||
raise NotImplementedError() # pragma: no cover
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
self.Sig1 = MockSig('Sig1')
|
||||
self.Sig2 = MockSig('Sig2')
|
||||
|
||||
def test_eq(self):
|
||||
self.assertEqual(self.Sig1, self.Sig1)
|
||||
|
||||
def test_ne(self):
|
||||
self.assertNotEqual(self.Sig1, self.Sig2)
|
||||
|
||||
def test_ne_other_type(self):
|
||||
self.assertNotEqual(self.Sig1, 5)
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('Sig1', repr(self.Sig1))
|
||||
self.assertEqual('Sig2', repr(self.Sig2))
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.Sig1.to_partial_json(), 'Sig1')
|
||||
self.assertEqual(self.Sig2.to_partial_json(), 'Sig2')
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.jose.jwa import JWASignature
|
||||
from acme.jose.jwa import RS256
|
||||
self.assertTrue(JWASignature.from_json('RS256') is RS256)
|
||||
|
||||
|
||||
class JWAHSTest(unittest.TestCase): # pylint: disable=too-few-public-methods
|
||||
|
||||
def test_it(self):
|
||||
from acme.jose.jwa import HS256
|
||||
sig = (
|
||||
b"\xceR\xea\xcd\x94\xab\xcf\xfb\xe0\xacA.:\x1a'\x08i\xe2\xc4"
|
||||
b"\r\x85+\x0e\x85\xaeUZ\xd4\xb3\x97zO"
|
||||
)
|
||||
self.assertEqual(HS256.sign(b'some key', b'foo'), sig)
|
||||
self.assertTrue(HS256.verify(b'some key', b'foo', sig) is True)
|
||||
self.assertTrue(HS256.verify(b'some key', b'foo', sig + b'!') is False)
|
||||
|
||||
|
||||
class JWARSTest(unittest.TestCase):
|
||||
|
||||
def test_sign_no_private_part(self):
|
||||
from acme.jose.jwa import RS256
|
||||
self.assertRaises(
|
||||
errors.Error, RS256.sign, RSA512_KEY.public_key(), b'foo')
|
||||
|
||||
def test_sign_key_too_small(self):
|
||||
from acme.jose.jwa import RS256
|
||||
from acme.jose.jwa import PS256
|
||||
self.assertRaises(errors.Error, RS256.sign, RSA256_KEY, b'foo')
|
||||
self.assertRaises(errors.Error, PS256.sign, RSA256_KEY, b'foo')
|
||||
|
||||
def test_rs(self):
|
||||
from acme.jose.jwa import RS256
|
||||
sig = (
|
||||
b'|\xc6\xb2\xa4\xab(\x87\x99\xfa*:\xea\xf8\xa0N&}\x9f\x0f\xc0O'
|
||||
b'\xc6t\xa3\xe6\xfa\xbb"\x15Y\x80Y\xe0\x81\xb8\x88)\xba\x0c\x9c'
|
||||
b'\xa4\x99\x1e\x19&\xd8\xc7\x99S\x97\xfc\x85\x0cOV\xe6\x07\x99'
|
||||
b'\xd2\xb9.>}\xfd'
|
||||
)
|
||||
self.assertEqual(RS256.sign(RSA512_KEY, b'foo'), sig)
|
||||
self.assertTrue(RS256.verify(RSA512_KEY.public_key(), b'foo', sig))
|
||||
self.assertFalse(RS256.verify(
|
||||
RSA512_KEY.public_key(), b'foo', sig + b'!'))
|
||||
|
||||
def test_ps(self):
|
||||
from acme.jose.jwa import PS256
|
||||
sig = PS256.sign(RSA1024_KEY, b'foo')
|
||||
self.assertTrue(PS256.verify(RSA1024_KEY.public_key(), b'foo', sig))
|
||||
self.assertFalse(PS256.verify(
|
||||
RSA1024_KEY.public_key(), b'foo', sig + b'!'))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,281 +0,0 @@
|
||||
"""JSON Web Key."""
|
||||
import abc
|
||||
import binascii
|
||||
import json
|
||||
import logging
|
||||
|
||||
import cryptography.exceptions
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes # type: ignore
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec # type: ignore
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
|
||||
import six
|
||||
|
||||
from acme.jose import errors
|
||||
from acme.jose import json_util
|
||||
from acme.jose import util
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class JWK(json_util.TypedJSONObjectWithFields):
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""JSON Web Key."""
|
||||
type_field_name = 'kty'
|
||||
TYPES = {} # type: dict
|
||||
cryptography_key_types = () # type: tuple
|
||||
"""Subclasses should override."""
|
||||
|
||||
required = NotImplemented
|
||||
"""Required members of public key's representation as defined by JWK/JWA."""
|
||||
|
||||
_thumbprint_json_dumps_params = {
|
||||
# "no whitespace or line breaks before or after any syntactic
|
||||
# elements"
|
||||
'indent': None,
|
||||
'separators': (',', ':'),
|
||||
# "members ordered lexicographically by the Unicode [UNICODE]
|
||||
# code points of the member names"
|
||||
'sort_keys': True,
|
||||
}
|
||||
|
||||
def thumbprint(self, hash_function=hashes.SHA256):
|
||||
"""Compute JWK Thumbprint.
|
||||
|
||||
https://tools.ietf.org/html/rfc7638
|
||||
|
||||
:returns bytes:
|
||||
|
||||
"""
|
||||
digest = hashes.Hash(hash_function(), backend=default_backend())
|
||||
digest.update(json.dumps(
|
||||
dict((k, v) for k, v in six.iteritems(self.to_json())
|
||||
if k in self.required),
|
||||
**self._thumbprint_json_dumps_params).encode())
|
||||
return digest.finalize()
|
||||
|
||||
@abc.abstractmethod
|
||||
def public_key(self): # pragma: no cover
|
||||
"""Generate JWK with public key.
|
||||
|
||||
For symmetric cryptosystems, this would return ``self``.
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def _load_cryptography_key(cls, data, password=None, backend=None):
|
||||
backend = default_backend() if backend is None else backend
|
||||
exceptions = {}
|
||||
|
||||
# private key?
|
||||
for loader in (serialization.load_pem_private_key,
|
||||
serialization.load_der_private_key):
|
||||
try:
|
||||
return loader(data, password, backend)
|
||||
except (ValueError, TypeError,
|
||||
cryptography.exceptions.UnsupportedAlgorithm) as error:
|
||||
exceptions[loader] = error
|
||||
|
||||
# public key?
|
||||
for loader in (serialization.load_pem_public_key,
|
||||
serialization.load_der_public_key):
|
||||
try:
|
||||
return loader(data, backend)
|
||||
except (ValueError,
|
||||
cryptography.exceptions.UnsupportedAlgorithm) as error:
|
||||
exceptions[loader] = error
|
||||
|
||||
# no luck
|
||||
raise errors.Error('Unable to deserialize key: {0}'.format(exceptions))
|
||||
|
||||
@classmethod
|
||||
def load(cls, data, password=None, backend=None):
|
||||
"""Load serialized key as JWK.
|
||||
|
||||
:param str data: Public or private key serialized as PEM or DER.
|
||||
:param str password: Optional password.
|
||||
:param backend: A `.PEMSerializationBackend` and
|
||||
`.DERSerializationBackend` provider.
|
||||
|
||||
:raises errors.Error: if unable to deserialize, or unsupported
|
||||
JWK algorithm
|
||||
|
||||
:returns: JWK of an appropriate type.
|
||||
:rtype: `JWK`
|
||||
|
||||
"""
|
||||
try:
|
||||
key = cls._load_cryptography_key(data, password, backend)
|
||||
except errors.Error as error:
|
||||
logger.debug('Loading symmetric key, asymmetric failed: %s', error)
|
||||
return JWKOct(key=data)
|
||||
|
||||
if cls.typ is not NotImplemented and not isinstance(
|
||||
key, cls.cryptography_key_types):
|
||||
raise errors.Error('Unable to deserialize {0} into {1}'.format(
|
||||
key.__class__, cls.__class__))
|
||||
for jwk_cls in six.itervalues(cls.TYPES):
|
||||
if isinstance(key, jwk_cls.cryptography_key_types):
|
||||
return jwk_cls(key=key)
|
||||
raise errors.Error('Unsupported algorithm: {0}'.format(key.__class__))
|
||||
|
||||
|
||||
@JWK.register
|
||||
class JWKES(JWK): # pragma: no cover
|
||||
# pylint: disable=abstract-class-not-used
|
||||
"""ES JWK.
|
||||
|
||||
.. warning:: This is not yet implemented!
|
||||
|
||||
"""
|
||||
typ = 'ES'
|
||||
cryptography_key_types = (
|
||||
ec.EllipticCurvePublicKey, ec.EllipticCurvePrivateKey)
|
||||
required = ('crv', JWK.type_field_name, 'x', 'y')
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
raise NotImplementedError()
|
||||
|
||||
def public_key(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
@JWK.register
|
||||
class JWKOct(JWK):
|
||||
"""Symmetric JWK."""
|
||||
typ = 'oct'
|
||||
__slots__ = ('key',)
|
||||
required = ('k', JWK.type_field_name)
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
# TODO: An "alg" member SHOULD also be present to identify the
|
||||
# algorithm intended to be used with the key, unless the
|
||||
# application uses another means or convention to determine
|
||||
# the algorithm used.
|
||||
return {'k': json_util.encode_b64jose(self.key)}
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
return cls(key=json_util.decode_b64jose(jobj['k']))
|
||||
|
||||
def public_key(self):
|
||||
return self
|
||||
|
||||
|
||||
@JWK.register
|
||||
class JWKRSA(JWK):
|
||||
"""RSA JWK.
|
||||
|
||||
:ivar key: `cryptography.hazmat.primitives.rsa.RSAPrivateKey`
|
||||
or `cryptography.hazmat.primitives.rsa.RSAPublicKey` wrapped
|
||||
in `.ComparableRSAKey`
|
||||
|
||||
"""
|
||||
typ = 'RSA'
|
||||
cryptography_key_types = (rsa.RSAPublicKey, rsa.RSAPrivateKey)
|
||||
__slots__ = ('key',)
|
||||
required = ('e', JWK.type_field_name, 'n')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'key' in kwargs and not isinstance(
|
||||
kwargs['key'], util.ComparableRSAKey):
|
||||
kwargs['key'] = util.ComparableRSAKey(kwargs['key'])
|
||||
super(JWKRSA, self).__init__(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _encode_param(cls, data):
|
||||
"""Encode Base64urlUInt.
|
||||
|
||||
:type data: long
|
||||
:rtype: unicode
|
||||
|
||||
"""
|
||||
def _leading_zeros(arg):
|
||||
if len(arg) % 2:
|
||||
return '0' + arg
|
||||
return arg
|
||||
|
||||
return json_util.encode_b64jose(binascii.unhexlify(
|
||||
_leading_zeros(hex(data)[2:].rstrip('L'))))
|
||||
|
||||
@classmethod
|
||||
def _decode_param(cls, data):
|
||||
"""Decode Base64urlUInt."""
|
||||
try:
|
||||
return int(binascii.hexlify(json_util.decode_b64jose(data)), 16)
|
||||
except ValueError: # invalid literal for long() with base 16
|
||||
raise errors.DeserializationError()
|
||||
|
||||
def public_key(self):
|
||||
return type(self)(key=self.key.public_key())
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
# pylint: disable=invalid-name
|
||||
n, e = (cls._decode_param(jobj[x]) for x in ('n', 'e'))
|
||||
public_numbers = rsa.RSAPublicNumbers(e=e, n=n)
|
||||
if 'd' not in jobj: # public key
|
||||
key = public_numbers.public_key(default_backend())
|
||||
else: # private key
|
||||
d = cls._decode_param(jobj['d'])
|
||||
if ('p' in jobj or 'q' in jobj or 'dp' in jobj or
|
||||
'dq' in jobj or 'qi' in jobj or 'oth' in jobj):
|
||||
# "If the producer includes any of the other private
|
||||
# key parameters, then all of the others MUST be
|
||||
# present, with the exception of "oth", which MUST
|
||||
# only be present when more than two prime factors
|
||||
# were used."
|
||||
p, q, dp, dq, qi, = all_params = tuple(
|
||||
jobj.get(x) for x in ('p', 'q', 'dp', 'dq', 'qi'))
|
||||
if tuple(param for param in all_params if param is None):
|
||||
raise errors.Error(
|
||||
'Some private parameters are missing: {0}'.format(
|
||||
all_params))
|
||||
p, q, dp, dq, qi = tuple(
|
||||
cls._decode_param(x) for x in all_params)
|
||||
|
||||
# TODO: check for oth
|
||||
else:
|
||||
# cryptography>=0.8
|
||||
p, q = rsa.rsa_recover_prime_factors(n, e, d)
|
||||
dp = rsa.rsa_crt_dmp1(d, p)
|
||||
dq = rsa.rsa_crt_dmq1(d, q)
|
||||
qi = rsa.rsa_crt_iqmp(p, q)
|
||||
|
||||
key = rsa.RSAPrivateNumbers(
|
||||
p, q, d, dp, dq, qi, public_numbers).private_key(
|
||||
default_backend())
|
||||
|
||||
return cls(key=key)
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
# pylint: disable=protected-access
|
||||
if isinstance(self.key._wrapped, rsa.RSAPublicKey):
|
||||
numbers = self.key.public_numbers()
|
||||
params = {
|
||||
'n': numbers.n,
|
||||
'e': numbers.e,
|
||||
}
|
||||
else: # rsa.RSAPrivateKey
|
||||
private = self.key.private_numbers()
|
||||
public = self.key.public_key().public_numbers()
|
||||
params = {
|
||||
'n': public.n,
|
||||
'e': public.e,
|
||||
'd': private.d,
|
||||
'p': private.p,
|
||||
'q': private.q,
|
||||
'dp': private.dmp1,
|
||||
'dq': private.dmq1,
|
||||
'qi': private.iqmp,
|
||||
}
|
||||
return dict((key, self._encode_param(value))
|
||||
for key, value in six.iteritems(params))
|
||||
@@ -1,191 +0,0 @@
|
||||
"""Tests for acme.jose.jwk."""
|
||||
import binascii
|
||||
import unittest
|
||||
|
||||
from acme import test_util
|
||||
|
||||
from acme.jose import errors
|
||||
from acme.jose import json_util
|
||||
from acme.jose import util
|
||||
|
||||
|
||||
DSA_PEM = test_util.load_vector('dsa512_key.pem')
|
||||
RSA256_KEY = test_util.load_rsa_private_key('rsa256_key.pem')
|
||||
RSA512_KEY = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
|
||||
|
||||
class JWKTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.jwk.JWK."""
|
||||
|
||||
def test_load(self):
|
||||
from acme.jose.jwk import JWK
|
||||
self.assertRaises(errors.Error, JWK.load, DSA_PEM)
|
||||
|
||||
def test_load_subclass_wrong_type(self):
|
||||
from acme.jose.jwk import JWKRSA
|
||||
self.assertRaises(errors.Error, JWKRSA.load, DSA_PEM)
|
||||
|
||||
|
||||
class JWKTestBaseMixin(object):
|
||||
"""Mixin test for JWK subclass tests."""
|
||||
|
||||
thumbprint = NotImplemented
|
||||
|
||||
def test_thumbprint_private(self):
|
||||
self.assertEqual(self.thumbprint, self.jwk.thumbprint())
|
||||
|
||||
def test_thumbprint_public(self):
|
||||
self.assertEqual(self.thumbprint, self.jwk.public_key().thumbprint())
|
||||
|
||||
|
||||
class JWKOctTest(unittest.TestCase, JWKTestBaseMixin):
|
||||
"""Tests for acme.jose.jwk.JWKOct."""
|
||||
|
||||
thumbprint = (b"\xf3\xe7\xbe\xa8`\xd2\xdap\xe9}\x9c\xce>"
|
||||
b"\xd0\xfcI\xbe\xcd\x92'\xd4o\x0e\xf41\xea"
|
||||
b"\x8e(\x8a\xb2i\x1c")
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.jwk import JWKOct
|
||||
self.jwk = JWKOct(key=b'foo')
|
||||
self.jobj = {'kty': 'oct', 'k': json_util.encode_b64jose(b'foo')}
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jwk.to_partial_json(), self.jobj)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.jose.jwk import JWKOct
|
||||
self.assertEqual(self.jwk, JWKOct.from_json(self.jobj))
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.jose.jwk import JWKOct
|
||||
hash(JWKOct.from_json(self.jobj))
|
||||
|
||||
def test_load(self):
|
||||
from acme.jose.jwk import JWKOct
|
||||
self.assertEqual(self.jwk, JWKOct.load(b'foo'))
|
||||
|
||||
def test_public_key(self):
|
||||
self.assertTrue(self.jwk.public_key() is self.jwk)
|
||||
|
||||
|
||||
class JWKRSATest(unittest.TestCase, JWKTestBaseMixin):
|
||||
"""Tests for acme.jose.jwk.JWKRSA."""
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
|
||||
thumbprint = (b'\x83K\xdc#3\x98\xca\x98\xed\xcb\x80\x80<\x0c'
|
||||
b'\xf0\x95\xb9H\xb2*l\xbd$\xe5&|O\x91\xd4 \xb0Y')
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.jwk import JWKRSA
|
||||
self.jwk256 = JWKRSA(key=RSA256_KEY.public_key())
|
||||
self.jwk256json = {
|
||||
'kty': 'RSA',
|
||||
'e': 'AQAB',
|
||||
'n': 'm2Fylv-Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEk',
|
||||
}
|
||||
# pylint: disable=protected-access
|
||||
self.jwk256_not_comparable = JWKRSA(
|
||||
key=RSA256_KEY.public_key()._wrapped)
|
||||
self.jwk512 = JWKRSA(key=RSA512_KEY.public_key())
|
||||
self.jwk512json = {
|
||||
'kty': 'RSA',
|
||||
'e': 'AQAB',
|
||||
'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5'
|
||||
'80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q',
|
||||
}
|
||||
self.private = JWKRSA(key=RSA256_KEY)
|
||||
self.private_json_small = self.jwk256json.copy()
|
||||
self.private_json_small['d'] = (
|
||||
'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE')
|
||||
self.private_json = self.jwk256json.copy()
|
||||
self.private_json.update({
|
||||
'd': 'lPQED_EPTV0UIBfNI3KP2d9Jlrc2mrMllmf946bu-CE',
|
||||
'p': 'zUVNZn4lLLBD1R6NE8TKNQ',
|
||||
'q': 'wcfKfc7kl5jfqXArCRSURQ',
|
||||
'dp': 'CWJFq43QvT5Bm5iN8n1okQ',
|
||||
'dq': 'bHh2u7etM8LKKCF2pY2UdQ',
|
||||
'qi': 'oi45cEkbVoJjAbnQpFY87Q',
|
||||
})
|
||||
self.jwk = self.private
|
||||
|
||||
def test_init_auto_comparable(self):
|
||||
self.assertTrue(isinstance(
|
||||
self.jwk256_not_comparable.key, util.ComparableRSAKey))
|
||||
self.assertEqual(self.jwk256, self.jwk256_not_comparable)
|
||||
|
||||
def test_encode_param_zero(self):
|
||||
from acme.jose.jwk import JWKRSA
|
||||
# pylint: disable=protected-access
|
||||
# TODO: move encode/decode _param to separate class
|
||||
self.assertEqual('AA', JWKRSA._encode_param(0))
|
||||
|
||||
def test_equals(self):
|
||||
self.assertEqual(self.jwk256, self.jwk256)
|
||||
self.assertEqual(self.jwk512, self.jwk512)
|
||||
|
||||
def test_not_equals(self):
|
||||
self.assertNotEqual(self.jwk256, self.jwk512)
|
||||
self.assertNotEqual(self.jwk512, self.jwk256)
|
||||
|
||||
def test_load(self):
|
||||
from acme.jose.jwk import JWKRSA
|
||||
self.assertEqual(self.private, JWKRSA.load(
|
||||
test_util.load_vector('rsa256_key.pem')))
|
||||
|
||||
def test_public_key(self):
|
||||
self.assertEqual(self.jwk256, self.private.public_key())
|
||||
|
||||
def test_to_partial_json(self):
|
||||
self.assertEqual(self.jwk256.to_partial_json(), self.jwk256json)
|
||||
self.assertEqual(self.jwk512.to_partial_json(), self.jwk512json)
|
||||
self.assertEqual(self.private.to_partial_json(), self.private_json)
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.jose.jwk import JWK
|
||||
self.assertEqual(
|
||||
self.jwk256, JWK.from_json(self.jwk256json))
|
||||
self.assertEqual(
|
||||
self.jwk512, JWK.from_json(self.jwk512json))
|
||||
self.assertEqual(self.private, JWK.from_json(self.private_json))
|
||||
|
||||
def test_from_json_private_small(self):
|
||||
from acme.jose.jwk import JWK
|
||||
self.assertEqual(self.private, JWK.from_json(self.private_json_small))
|
||||
|
||||
def test_from_json_missing_one_additional(self):
|
||||
from acme.jose.jwk import JWK
|
||||
del self.private_json['q']
|
||||
self.assertRaises(errors.Error, JWK.from_json, self.private_json)
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.jose.jwk import JWK
|
||||
hash(JWK.from_json(self.jwk256json))
|
||||
|
||||
def test_from_json_non_schema_errors(self):
|
||||
# valid against schema, but still failing
|
||||
from acme.jose.jwk import JWK
|
||||
self.assertRaises(errors.DeserializationError, JWK.from_json,
|
||||
{'kty': 'RSA', 'e': 'AQAB', 'n': ''})
|
||||
self.assertRaises(errors.DeserializationError, JWK.from_json,
|
||||
{'kty': 'RSA', 'e': 'AQAB', 'n': '1'})
|
||||
|
||||
def test_thumbprint_go_jose(self):
|
||||
# https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk.go#L155
|
||||
# https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L331-L344
|
||||
# https://github.com/square/go-jose/blob/4ddd71883fa547d37fbf598071f04512d8bafee3/jwk_test.go#L384
|
||||
from acme.jose.jwk import JWKRSA
|
||||
key = JWKRSA.json_loads("""{
|
||||
"kty": "RSA",
|
||||
"kid": "bilbo.baggins@hobbiton.example",
|
||||
"use": "sig",
|
||||
"n": "n4EPtAOCc9AlkeQHPzHStgAbgs7bTZLwUBZdR8_KuKPEHLd4rHVTeT-O-XV2jRojdNhxJWTDvNd7nqQ0VEiZQHz_AJmSCpMaJMRBSFKrKb2wqVwGU_NsYOYL-QtiWN2lbzcEe6XC0dApr5ydQLrHqkHHig3RBordaZ6Aj-oBHqFEHYpPe7Tpe-OfVfHd1E6cS6M1FZcD1NNLYD5lFHpPI9bTwJlsde3uhGqC0ZCuEHg8lhzwOHrtIQbS0FVbb9k3-tVTU4fg_3L_vniUFAKwuCLqKnS2BYwdq_mzSnbLY7h_qixoR7jig3__kRhuaxwUkRz5iaiQkqgc5gHdrNP5zw",
|
||||
"e": "AQAB"
|
||||
}""")
|
||||
self.assertEqual(
|
||||
binascii.hexlify(key.thumbprint()),
|
||||
b"f63838e96077ad1fc01c3f8405774dedc0641f558ebb4b40dccf5f9b6d66a932")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,433 +0,0 @@
|
||||
"""JOSE Web Signature."""
|
||||
import argparse
|
||||
import base64
|
||||
import sys
|
||||
|
||||
import OpenSSL
|
||||
import six
|
||||
|
||||
from acme.jose import b64
|
||||
from acme.jose import errors
|
||||
from acme.jose import json_util
|
||||
from acme.jose import jwa
|
||||
from acme.jose import jwk
|
||||
from acme.jose import util
|
||||
|
||||
|
||||
class MediaType(object):
|
||||
"""MediaType field encoder/decoder."""
|
||||
|
||||
PREFIX = 'application/'
|
||||
"""MIME Media Type and Content Type prefix."""
|
||||
|
||||
@classmethod
|
||||
def decode(cls, value):
|
||||
"""Decoder."""
|
||||
# 4.1.10
|
||||
if '/' not in value:
|
||||
if ';' in value:
|
||||
raise errors.DeserializationError('Unexpected semi-colon')
|
||||
return cls.PREFIX + value
|
||||
return value
|
||||
|
||||
@classmethod
|
||||
def encode(cls, value):
|
||||
"""Encoder."""
|
||||
# 4.1.10
|
||||
if ';' not in value:
|
||||
assert value.startswith(cls.PREFIX)
|
||||
return value[len(cls.PREFIX):]
|
||||
return value
|
||||
|
||||
|
||||
class Header(json_util.JSONObjectWithFields):
|
||||
"""JOSE Header.
|
||||
|
||||
.. warning:: This class supports **only** Registered Header
|
||||
Parameter Names (as defined in section 4.1 of the
|
||||
protocol). If you need Public Header Parameter Names (4.2)
|
||||
or Private Header Parameter Names (4.3), you must subclass
|
||||
and override :meth:`from_json` and :meth:`to_partial_json`
|
||||
appropriately.
|
||||
|
||||
.. warning:: This class does not support any extensions through
|
||||
the "crit" (Critical) Header Parameter (4.1.11) and as a
|
||||
conforming implementation, :meth:`from_json` treats its
|
||||
occurrence as an error. Please subclass if you seek for
|
||||
a different behaviour.
|
||||
|
||||
:ivar x5tS256: "x5t#S256"
|
||||
:ivar str typ: MIME Media Type, inc. :const:`MediaType.PREFIX`.
|
||||
:ivar str cty: Content-Type, inc. :const:`MediaType.PREFIX`.
|
||||
|
||||
"""
|
||||
alg = json_util.Field(
|
||||
'alg', decoder=jwa.JWASignature.from_json, omitempty=True)
|
||||
jku = json_util.Field('jku', omitempty=True)
|
||||
jwk = json_util.Field('jwk', decoder=jwk.JWK.from_json, omitempty=True)
|
||||
kid = json_util.Field('kid', omitempty=True)
|
||||
x5u = json_util.Field('x5u', omitempty=True)
|
||||
x5c = json_util.Field('x5c', omitempty=True, default=())
|
||||
x5t = json_util.Field(
|
||||
'x5t', decoder=json_util.decode_b64jose, omitempty=True)
|
||||
x5tS256 = json_util.Field(
|
||||
'x5t#S256', decoder=json_util.decode_b64jose, omitempty=True)
|
||||
typ = json_util.Field('typ', encoder=MediaType.encode,
|
||||
decoder=MediaType.decode, omitempty=True)
|
||||
cty = json_util.Field('cty', encoder=MediaType.encode,
|
||||
decoder=MediaType.decode, omitempty=True)
|
||||
crit = json_util.Field('crit', omitempty=True, default=())
|
||||
|
||||
def not_omitted(self):
|
||||
"""Fields that would not be omitted in the JSON object."""
|
||||
return dict((name, getattr(self, name))
|
||||
for name, field in six.iteritems(self._fields)
|
||||
if not field.omit(getattr(self, name)))
|
||||
|
||||
def __add__(self, other):
|
||||
if not isinstance(other, type(self)):
|
||||
raise TypeError('Header cannot be added to: {0}'.format(
|
||||
type(other)))
|
||||
|
||||
not_omitted_self = self.not_omitted()
|
||||
not_omitted_other = other.not_omitted()
|
||||
|
||||
if set(not_omitted_self).intersection(not_omitted_other):
|
||||
raise TypeError('Addition of overlapping headers not defined')
|
||||
|
||||
not_omitted_self.update(not_omitted_other)
|
||||
return type(self)(**not_omitted_self) # pylint: disable=star-args
|
||||
|
||||
def find_key(self):
|
||||
"""Find key based on header.
|
||||
|
||||
.. todo:: Supports only "jwk" header parameter lookup.
|
||||
|
||||
:returns: (Public) key found in the header.
|
||||
:rtype: .JWK
|
||||
|
||||
:raises acme.jose.errors.Error: if key could not be found
|
||||
|
||||
"""
|
||||
if self.jwk is None:
|
||||
raise errors.Error('No key found')
|
||||
return self.jwk
|
||||
|
||||
@crit.decoder
|
||||
def crit(unused_value):
|
||||
# pylint: disable=missing-docstring,no-self-argument,no-self-use
|
||||
raise errors.DeserializationError(
|
||||
'"crit" is not supported, please subclass')
|
||||
|
||||
# x5c does NOT use JOSE Base64 (4.1.6)
|
||||
|
||||
@x5c.encoder # type: ignore
|
||||
def x5c(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return [base64.b64encode(OpenSSL.crypto.dump_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1, cert.wrapped)) for cert in value]
|
||||
|
||||
@x5c.decoder # type: ignore
|
||||
def x5c(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
try:
|
||||
return tuple(util.ComparableX509(OpenSSL.crypto.load_certificate(
|
||||
OpenSSL.crypto.FILETYPE_ASN1,
|
||||
base64.b64decode(cert))) for cert in value)
|
||||
except OpenSSL.crypto.Error as error:
|
||||
raise errors.DeserializationError(error)
|
||||
|
||||
|
||||
class Signature(json_util.JSONObjectWithFields):
|
||||
"""JWS Signature.
|
||||
|
||||
:ivar combined: Combined Header (protected and unprotected,
|
||||
:class:`Header`).
|
||||
:ivar unicode protected: JWS protected header (Jose Base-64 decoded).
|
||||
:ivar header: JWS Unprotected Header (:class:`Header`).
|
||||
:ivar str signature: The signature.
|
||||
|
||||
"""
|
||||
header_cls = Header
|
||||
|
||||
__slots__ = ('combined',)
|
||||
protected = json_util.Field('protected', omitempty=True, default='')
|
||||
header = json_util.Field(
|
||||
'header', omitempty=True, default=header_cls(),
|
||||
decoder=header_cls.from_json)
|
||||
signature = json_util.Field(
|
||||
'signature', decoder=json_util.decode_b64jose,
|
||||
encoder=json_util.encode_b64jose)
|
||||
|
||||
@protected.encoder # type: ignore
|
||||
def protected(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
# wrong type guess (Signature, not bytes) | pylint: disable=no-member
|
||||
return json_util.encode_b64jose(value.encode('utf-8'))
|
||||
|
||||
@protected.decoder # type: ignore
|
||||
def protected(value): # pylint: disable=missing-docstring,no-self-argument
|
||||
return json_util.decode_b64jose(value).decode('utf-8')
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if 'combined' not in kwargs:
|
||||
kwargs = self._with_combined(kwargs)
|
||||
super(Signature, self).__init__(**kwargs)
|
||||
assert self.combined.alg is not None
|
||||
|
||||
@classmethod
|
||||
def _with_combined(cls, kwargs):
|
||||
assert 'combined' not in kwargs
|
||||
header = kwargs.get('header', cls._fields['header'].default)
|
||||
protected = kwargs.get('protected', cls._fields['protected'].default)
|
||||
|
||||
if protected:
|
||||
combined = header + cls.header_cls.json_loads(protected)
|
||||
else:
|
||||
combined = header
|
||||
|
||||
kwargs['combined'] = combined
|
||||
return kwargs
|
||||
|
||||
@classmethod
|
||||
def _msg(cls, protected, payload):
|
||||
return (b64.b64encode(protected.encode('utf-8')) + b'.' +
|
||||
b64.b64encode(payload))
|
||||
|
||||
def verify(self, payload, key=None):
|
||||
"""Verify.
|
||||
|
||||
:param JWK key: Key used for verification.
|
||||
|
||||
"""
|
||||
key = self.combined.find_key() if key is None else key
|
||||
return self.combined.alg.verify(
|
||||
key=key.key, sig=self.signature,
|
||||
msg=self._msg(self.protected, payload))
|
||||
|
||||
@classmethod
|
||||
def sign(cls, payload, key, alg, include_jwk=True,
|
||||
protect=frozenset(), **kwargs):
|
||||
"""Sign.
|
||||
|
||||
:param JWK key: Key for signature.
|
||||
|
||||
"""
|
||||
assert isinstance(key, alg.kty)
|
||||
|
||||
header_params = kwargs
|
||||
header_params['alg'] = alg
|
||||
if include_jwk:
|
||||
header_params['jwk'] = key.public_key()
|
||||
|
||||
assert set(header_params).issubset(cls.header_cls._fields)
|
||||
assert protect.issubset(cls.header_cls._fields)
|
||||
|
||||
protected_params = {}
|
||||
for header in protect:
|
||||
if header in header_params:
|
||||
protected_params[header] = header_params.pop(header)
|
||||
if protected_params:
|
||||
# pylint: disable=star-args
|
||||
protected = cls.header_cls(**protected_params).json_dumps()
|
||||
else:
|
||||
protected = ''
|
||||
|
||||
header = cls.header_cls(**header_params) # pylint: disable=star-args
|
||||
signature = alg.sign(key.key, cls._msg(protected, payload))
|
||||
|
||||
return cls(protected=protected, header=header, signature=signature)
|
||||
|
||||
def fields_to_partial_json(self):
|
||||
fields = super(Signature, self).fields_to_partial_json()
|
||||
if not fields['header'].not_omitted():
|
||||
del fields['header']
|
||||
return fields
|
||||
|
||||
@classmethod
|
||||
def fields_from_json(cls, jobj):
|
||||
fields = super(Signature, cls).fields_from_json(jobj)
|
||||
fields_with_combined = cls._with_combined(fields)
|
||||
if 'alg' not in fields_with_combined['combined'].not_omitted():
|
||||
raise errors.DeserializationError('alg not present')
|
||||
return fields_with_combined
|
||||
|
||||
|
||||
class JWS(json_util.JSONObjectWithFields):
|
||||
"""JSON Web Signature.
|
||||
|
||||
:ivar str payload: JWS Payload.
|
||||
:ivar str signature: JWS Signatures.
|
||||
|
||||
"""
|
||||
__slots__ = ('payload', 'signatures')
|
||||
|
||||
signature_cls = Signature
|
||||
|
||||
def verify(self, key=None):
|
||||
"""Verify."""
|
||||
return all(sig.verify(self.payload, key) for sig in self.signatures)
|
||||
|
||||
@classmethod
|
||||
def sign(cls, payload, **kwargs):
|
||||
"""Sign."""
|
||||
return cls(payload=payload, signatures=(
|
||||
cls.signature_cls.sign(payload=payload, **kwargs),))
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
"""Get a singleton signature.
|
||||
|
||||
:rtype: `signature_cls`
|
||||
|
||||
"""
|
||||
assert len(self.signatures) == 1
|
||||
return self.signatures[0]
|
||||
|
||||
def to_compact(self):
|
||||
"""Compact serialization.
|
||||
|
||||
:rtype: bytes
|
||||
|
||||
"""
|
||||
assert len(self.signatures) == 1
|
||||
|
||||
assert 'alg' not in self.signature.header.not_omitted()
|
||||
# ... it must be in protected
|
||||
|
||||
return (
|
||||
b64.b64encode(self.signature.protected.encode('utf-8')) +
|
||||
b'.' +
|
||||
b64.b64encode(self.payload) +
|
||||
b'.' +
|
||||
b64.b64encode(self.signature.signature))
|
||||
|
||||
@classmethod
|
||||
def from_compact(cls, compact):
|
||||
"""Compact deserialization.
|
||||
|
||||
:param bytes compact:
|
||||
|
||||
"""
|
||||
try:
|
||||
protected, payload, signature = compact.split(b'.')
|
||||
except ValueError:
|
||||
raise errors.DeserializationError(
|
||||
'Compact JWS serialization should comprise of exactly'
|
||||
' 3 dot-separated components')
|
||||
|
||||
sig = cls.signature_cls(
|
||||
protected=b64.b64decode(protected).decode('utf-8'),
|
||||
signature=b64.b64decode(signature))
|
||||
return cls(payload=b64.b64decode(payload), signatures=(sig,))
|
||||
|
||||
def to_partial_json(self, flat=True): # pylint: disable=arguments-differ
|
||||
assert self.signatures
|
||||
payload = json_util.encode_b64jose(self.payload)
|
||||
|
||||
if flat and len(self.signatures) == 1:
|
||||
ret = self.signatures[0].to_partial_json()
|
||||
ret['payload'] = payload
|
||||
return ret
|
||||
else:
|
||||
return {
|
||||
'payload': payload,
|
||||
'signatures': self.signatures,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_json(cls, jobj):
|
||||
if 'signature' in jobj and 'signatures' in jobj:
|
||||
raise errors.DeserializationError('Flat mixed with non-flat')
|
||||
elif 'signature' in jobj: # flat
|
||||
return cls(payload=json_util.decode_b64jose(jobj.pop('payload')),
|
||||
signatures=(cls.signature_cls.from_json(jobj),))
|
||||
else:
|
||||
return cls(payload=json_util.decode_b64jose(jobj['payload']),
|
||||
signatures=tuple(cls.signature_cls.from_json(sig)
|
||||
for sig in jobj['signatures']))
|
||||
|
||||
|
||||
class CLI(object):
|
||||
"""JWS CLI."""
|
||||
|
||||
@classmethod
|
||||
def sign(cls, args):
|
||||
"""Sign."""
|
||||
key = args.alg.kty.load(args.key.read())
|
||||
args.key.close()
|
||||
if args.protect is None:
|
||||
args.protect = []
|
||||
if args.compact:
|
||||
args.protect.append('alg')
|
||||
|
||||
sig = JWS.sign(payload=sys.stdin.read().encode(), key=key, alg=args.alg,
|
||||
protect=set(args.protect))
|
||||
|
||||
if args.compact:
|
||||
six.print_(sig.to_compact().decode('utf-8'))
|
||||
else: # JSON
|
||||
six.print_(sig.json_dumps_pretty())
|
||||
|
||||
@classmethod
|
||||
def verify(cls, args):
|
||||
"""Verify."""
|
||||
if args.compact:
|
||||
sig = JWS.from_compact(sys.stdin.read().encode())
|
||||
else: # JSON
|
||||
try:
|
||||
sig = JWS.json_loads(sys.stdin.read())
|
||||
except errors.Error as error:
|
||||
six.print_(error)
|
||||
return -1
|
||||
|
||||
if args.key is not None:
|
||||
assert args.kty is not None
|
||||
key = args.kty.load(args.key.read()).public_key()
|
||||
args.key.close()
|
||||
else:
|
||||
key = None
|
||||
|
||||
sys.stdout.write(sig.payload)
|
||||
return not sig.verify(key=key)
|
||||
|
||||
@classmethod
|
||||
def _alg_type(cls, arg):
|
||||
return jwa.JWASignature.from_json(arg)
|
||||
|
||||
@classmethod
|
||||
def _header_type(cls, arg):
|
||||
assert arg in Signature.header_cls._fields
|
||||
return arg
|
||||
|
||||
@classmethod
|
||||
def _kty_type(cls, arg):
|
||||
assert arg in jwk.JWK.TYPES
|
||||
return jwk.JWK.TYPES[arg]
|
||||
|
||||
@classmethod
|
||||
def run(cls, args=sys.argv[1:]):
|
||||
"""Parse arguments and sign/verify."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--compact', action='store_true')
|
||||
|
||||
subparsers = parser.add_subparsers()
|
||||
parser_sign = subparsers.add_parser('sign')
|
||||
parser_sign.set_defaults(func=cls.sign)
|
||||
parser_sign.add_argument(
|
||||
'-k', '--key', type=argparse.FileType('rb'), required=True)
|
||||
parser_sign.add_argument(
|
||||
'-a', '--alg', type=cls._alg_type, default=jwa.RS256)
|
||||
parser_sign.add_argument(
|
||||
'-p', '--protect', action='append', type=cls._header_type)
|
||||
|
||||
parser_verify = subparsers.add_parser('verify')
|
||||
parser_verify.set_defaults(func=cls.verify)
|
||||
parser_verify.add_argument(
|
||||
'-k', '--key', type=argparse.FileType('rb'), required=False)
|
||||
parser_verify.add_argument(
|
||||
'--kty', type=cls._kty_type, required=False)
|
||||
|
||||
parsed = parser.parse_args(args)
|
||||
return parsed.func(parsed)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(CLI.run()) # pragma: no cover
|
||||
@@ -1,239 +0,0 @@
|
||||
"""Tests for acme.jose.jws."""
|
||||
import base64
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
import OpenSSL
|
||||
|
||||
from acme import test_util
|
||||
|
||||
from acme.jose import errors
|
||||
from acme.jose import json_util
|
||||
from acme.jose import jwa
|
||||
from acme.jose import jwk
|
||||
|
||||
|
||||
CERT = test_util.load_comparable_cert('cert.pem')
|
||||
KEY = jwk.JWKRSA.load(test_util.load_vector('rsa512_key.pem'))
|
||||
|
||||
|
||||
class MediaTypeTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.jws.MediaType."""
|
||||
|
||||
def test_decode(self):
|
||||
from acme.jose.jws import MediaType
|
||||
self.assertEqual('application/app', MediaType.decode('application/app'))
|
||||
self.assertEqual('application/app', MediaType.decode('app'))
|
||||
self.assertRaises(
|
||||
errors.DeserializationError, MediaType.decode, 'app;foo')
|
||||
|
||||
def test_encode(self):
|
||||
from acme.jose.jws import MediaType
|
||||
self.assertEqual('app', MediaType.encode('application/app'))
|
||||
self.assertEqual('application/app;foo',
|
||||
MediaType.encode('application/app;foo'))
|
||||
|
||||
|
||||
class HeaderTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.jws.Header."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.jws import Header
|
||||
self.header1 = Header(jwk='foo')
|
||||
self.header2 = Header(jwk='bar')
|
||||
self.crit = Header(crit=('a', 'b'))
|
||||
self.empty = Header()
|
||||
|
||||
def test_add_non_empty(self):
|
||||
from acme.jose.jws import Header
|
||||
self.assertEqual(Header(jwk='foo', crit=('a', 'b')),
|
||||
self.header1 + self.crit)
|
||||
|
||||
def test_add_empty(self):
|
||||
self.assertEqual(self.header1, self.header1 + self.empty)
|
||||
self.assertEqual(self.header1, self.empty + self.header1)
|
||||
|
||||
def test_add_overlapping_error(self):
|
||||
self.assertRaises(TypeError, self.header1.__add__, self.header2)
|
||||
|
||||
def test_add_wrong_type_error(self):
|
||||
self.assertRaises(TypeError, self.header1.__add__, 'xxx')
|
||||
|
||||
def test_crit_decode_always_errors(self):
|
||||
from acme.jose.jws import Header
|
||||
self.assertRaises(errors.DeserializationError, Header.from_json,
|
||||
{'crit': ['a', 'b']})
|
||||
|
||||
def test_x5c_decoding(self):
|
||||
from acme.jose.jws import Header
|
||||
header = Header(x5c=(CERT, CERT))
|
||||
jobj = header.to_partial_json()
|
||||
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' + cert_asn1)
|
||||
self.assertRaises(errors.DeserializationError, Header.from_json, jobj)
|
||||
|
||||
def test_find_key(self):
|
||||
self.assertEqual('foo', self.header1.find_key())
|
||||
self.assertEqual('bar', self.header2.find_key())
|
||||
self.assertRaises(errors.Error, self.crit.find_key)
|
||||
|
||||
|
||||
class SignatureTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.jws.Signature."""
|
||||
|
||||
def test_from_json(self):
|
||||
from acme.jose.jws import Header
|
||||
from acme.jose.jws import Signature
|
||||
self.assertEqual(
|
||||
Signature(signature=b'foo', header=Header(alg=jwa.RS256)),
|
||||
Signature.from_json(
|
||||
{'signature': 'Zm9v', 'header': {'alg': 'RS256'}}))
|
||||
|
||||
def test_from_json_no_alg_error(self):
|
||||
from acme.jose.jws import Signature
|
||||
self.assertRaises(errors.DeserializationError,
|
||||
Signature.from_json, {'signature': 'foo'})
|
||||
|
||||
|
||||
class JWSTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.jws.JWS."""
|
||||
|
||||
def setUp(self):
|
||||
self.privkey = KEY
|
||||
self.pubkey = self.privkey.public_key()
|
||||
|
||||
from acme.jose.jws import JWS
|
||||
self.unprotected = JWS.sign(
|
||||
payload=b'foo', key=self.privkey, alg=jwa.RS256)
|
||||
self.protected = JWS.sign(
|
||||
payload=b'foo', key=self.privkey, alg=jwa.RS256,
|
||||
protect=frozenset(['jwk', 'alg']))
|
||||
self.mixed = JWS.sign(
|
||||
payload=b'foo', key=self.privkey, alg=jwa.RS256,
|
||||
protect=frozenset(['alg']))
|
||||
|
||||
def test_pubkey_jwk(self):
|
||||
self.assertEqual(self.unprotected.signature.combined.jwk, self.pubkey)
|
||||
self.assertEqual(self.protected.signature.combined.jwk, self.pubkey)
|
||||
self.assertEqual(self.mixed.signature.combined.jwk, self.pubkey)
|
||||
|
||||
def test_sign_unprotected(self):
|
||||
self.assertTrue(self.unprotected.verify())
|
||||
|
||||
def test_sign_protected(self):
|
||||
self.assertTrue(self.protected.verify())
|
||||
|
||||
def test_sign_mixed(self):
|
||||
self.assertTrue(self.mixed.verify())
|
||||
|
||||
def test_compact_lost_unprotected(self):
|
||||
compact = self.mixed.to_compact()
|
||||
self.assertEqual(
|
||||
b'eyJhbGciOiAiUlMyNTYifQ.Zm9v.OHdxFVj73l5LpxbFp1AmYX4yJM0Pyb'
|
||||
b'_893n1zQjpim_eLS5J1F61lkvrCrCDErTEJnBGOGesJ72M7b6Ve1cAJA',
|
||||
compact)
|
||||
|
||||
from acme.jose.jws import JWS
|
||||
mixed = JWS.from_compact(compact)
|
||||
|
||||
self.assertNotEqual(self.mixed, mixed)
|
||||
self.assertEqual(
|
||||
set(['alg']), set(mixed.signature.combined.not_omitted()))
|
||||
|
||||
def test_from_compact_missing_components(self):
|
||||
from acme.jose.jws import JWS
|
||||
self.assertRaises(errors.DeserializationError, JWS.from_compact, b'.')
|
||||
|
||||
def test_json_omitempty(self):
|
||||
protected_jobj = self.protected.to_partial_json(flat=True)
|
||||
unprotected_jobj = self.unprotected.to_partial_json(flat=True)
|
||||
|
||||
self.assertTrue('protected' not in unprotected_jobj)
|
||||
self.assertTrue('header' not in protected_jobj)
|
||||
|
||||
unprotected_jobj['header'] = unprotected_jobj['header'].to_json()
|
||||
|
||||
from acme.jose.jws import JWS
|
||||
self.assertEqual(JWS.from_json(protected_jobj), self.protected)
|
||||
self.assertEqual(JWS.from_json(unprotected_jobj), self.unprotected)
|
||||
|
||||
def test_json_flat(self):
|
||||
jobj_to = {
|
||||
'signature': json_util.encode_b64jose(
|
||||
self.mixed.signature.signature),
|
||||
'payload': json_util.encode_b64jose(b'foo'),
|
||||
'header': self.mixed.signature.header,
|
||||
'protected': json_util.encode_b64jose(
|
||||
self.mixed.signature.protected.encode('utf-8')),
|
||||
}
|
||||
jobj_from = jobj_to.copy()
|
||||
jobj_from['header'] = jobj_from['header'].to_json()
|
||||
|
||||
self.assertEqual(self.mixed.to_partial_json(flat=True), jobj_to)
|
||||
from acme.jose.jws import JWS
|
||||
self.assertEqual(self.mixed, JWS.from_json(jobj_from))
|
||||
|
||||
def test_json_not_flat(self):
|
||||
jobj_to = {
|
||||
'signatures': (self.mixed.signature,),
|
||||
'payload': json_util.encode_b64jose(b'foo'),
|
||||
}
|
||||
jobj_from = jobj_to.copy()
|
||||
jobj_from['signatures'] = [jobj_to['signatures'][0].to_json()]
|
||||
|
||||
self.assertEqual(self.mixed.to_partial_json(flat=False), jobj_to)
|
||||
from acme.jose.jws import JWS
|
||||
self.assertEqual(self.mixed, JWS.from_json(jobj_from))
|
||||
|
||||
def test_from_json_mixed_flat(self):
|
||||
from acme.jose.jws import JWS
|
||||
self.assertRaises(errors.DeserializationError, JWS.from_json,
|
||||
{'signatures': (), 'signature': 'foo'})
|
||||
|
||||
def test_from_json_hashable(self):
|
||||
from acme.jose.jws import JWS
|
||||
hash(JWS.from_json(self.mixed.to_json()))
|
||||
|
||||
|
||||
class CLITest(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.key_path = test_util.vector_path('rsa512_key.pem')
|
||||
|
||||
def test_unverified(self):
|
||||
from acme.jose.jws import CLI
|
||||
with mock.patch('sys.stdin') as sin:
|
||||
sin.read.return_value = '{"payload": "foo", "signature": "xxx"}'
|
||||
with mock.patch('sys.stdout'):
|
||||
self.assertEqual(-1, CLI.run(['verify']))
|
||||
|
||||
def test_json(self):
|
||||
from acme.jose.jws import CLI
|
||||
|
||||
with mock.patch('sys.stdin') as sin:
|
||||
sin.read.return_value = 'foo'
|
||||
with mock.patch('sys.stdout') as sout:
|
||||
CLI.run(['sign', '-k', self.key_path, '-a', 'RS256',
|
||||
'-p', 'jwk'])
|
||||
sin.read.return_value = sout.write.mock_calls[0][1][0]
|
||||
self.assertEqual(0, CLI.run(['verify']))
|
||||
|
||||
def test_compact(self):
|
||||
from acme.jose.jws import CLI
|
||||
|
||||
with mock.patch('sys.stdin') as sin:
|
||||
sin.read.return_value = 'foo'
|
||||
with mock.patch('sys.stdout') as sout:
|
||||
CLI.run(['--compact', 'sign', '-k', self.key_path])
|
||||
sin.read.return_value = sout.write.mock_calls[0][1][0]
|
||||
self.assertEqual(0, CLI.run([
|
||||
'--compact', 'verify', '--kty', 'RSA',
|
||||
'-k', self.key_path]))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,226 +0,0 @@
|
||||
"""JOSE utilities."""
|
||||
import collections
|
||||
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
import OpenSSL
|
||||
import six
|
||||
|
||||
|
||||
class abstractclassmethod(classmethod):
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
"""Descriptor for an abstract classmethod.
|
||||
|
||||
It augments the :mod:`abc` framework with an abstract
|
||||
classmethod. This is implemented as :class:`abc.abstractclassmethod`
|
||||
in the standard Python library starting with version 3.2.
|
||||
|
||||
This particular implementation, allegedly based on Python 3.3 source
|
||||
code, is stolen from
|
||||
http://stackoverflow.com/questions/11217878/python-2-7-combine-abc-abstractmethod-and-classmethod.
|
||||
|
||||
"""
|
||||
__isabstractmethod__ = True
|
||||
|
||||
def __init__(self, target):
|
||||
target.__isabstractmethod__ = True
|
||||
super(abstractclassmethod, self).__init__(target)
|
||||
|
||||
|
||||
class ComparableX509(object): # pylint: disable=too-few-public-methods
|
||||
"""Wrapper for OpenSSL.crypto.X509** objects that supports __eq__.
|
||||
|
||||
: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
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self.wrapped, name)
|
||||
|
||||
def _dump(self, filetype=OpenSSL.crypto.FILETYPE_ASN1):
|
||||
"""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)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return NotImplemented
|
||||
# pylint: disable=protected-access
|
||||
return self._dump() == other._dump()
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.__class__, self._dump()))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0}({1!r})>'.format(self.__class__.__name__, self.wrapped)
|
||||
|
||||
|
||||
class ComparableKey(object): # pylint: disable=too-few-public-methods
|
||||
"""Comparable wrapper for `cryptography` keys.
|
||||
|
||||
See https://github.com/pyca/cryptography/issues/2122.
|
||||
|
||||
"""
|
||||
__hash__ = NotImplemented
|
||||
|
||||
def __init__(self, wrapped):
|
||||
self._wrapped = wrapped
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._wrapped, name)
|
||||
|
||||
def __eq__(self, other):
|
||||
# pylint: disable=protected-access
|
||||
if (not isinstance(other, self.__class__) or
|
||||
self._wrapped.__class__ is not other._wrapped.__class__):
|
||||
return NotImplemented
|
||||
elif hasattr(self._wrapped, 'private_numbers'):
|
||||
return self.private_numbers() == other.private_numbers()
|
||||
elif hasattr(self._wrapped, 'public_numbers'):
|
||||
return self.public_numbers() == other.public_numbers()
|
||||
else:
|
||||
return NotImplemented
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0}({1!r})>'.format(self.__class__.__name__, self._wrapped)
|
||||
|
||||
def public_key(self):
|
||||
"""Get wrapped public key."""
|
||||
return self.__class__(self._wrapped.public_key())
|
||||
|
||||
|
||||
class ComparableRSAKey(ComparableKey): # pylint: disable=too-few-public-methods
|
||||
"""Wrapper for `cryptography` RSA keys.
|
||||
|
||||
Wraps around:
|
||||
- `cryptography.hazmat.primitives.asymmetric.RSAPrivateKey`
|
||||
- `cryptography.hazmat.primitives.asymmetric.RSAPublicKey`
|
||||
|
||||
"""
|
||||
|
||||
def __hash__(self):
|
||||
# public_numbers() hasn't got stable hash!
|
||||
# https://github.com/pyca/cryptography/issues/2143
|
||||
if isinstance(self._wrapped, rsa.RSAPrivateKeyWithSerialization):
|
||||
priv = self.private_numbers()
|
||||
pub = priv.public_numbers
|
||||
return hash((self.__class__, priv.p, priv.q, priv.dmp1,
|
||||
priv.dmq1, priv.iqmp, pub.n, pub.e))
|
||||
elif isinstance(self._wrapped, rsa.RSAPublicKeyWithSerialization):
|
||||
pub = self.public_numbers()
|
||||
return hash((self.__class__, pub.n, pub.e))
|
||||
|
||||
|
||||
class ImmutableMap(collections.Mapping, collections.Hashable): # type: ignore
|
||||
# pylint: disable=too-few-public-methods
|
||||
"""Immutable key to value mapping with attribute access."""
|
||||
|
||||
__slots__ = ()
|
||||
"""Must be overridden in subclasses."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if set(kwargs) != set(self.__slots__):
|
||||
raise TypeError(
|
||||
'__init__() takes exactly the following arguments: {0} '
|
||||
'({1} given)'.format(', '.join(self.__slots__),
|
||||
', '.join(kwargs) if kwargs else 'none'))
|
||||
for slot in self.__slots__:
|
||||
object.__setattr__(self, slot, kwargs.pop(slot))
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Return updated map."""
|
||||
items = dict(self)
|
||||
items.update(kwargs)
|
||||
return type(self)(**items) # pylint: disable=star-args
|
||||
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return getattr(self, key)
|
||||
except AttributeError:
|
||||
raise KeyError(key)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.__slots__)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.__slots__)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(tuple(getattr(self, slot) for slot in self.__slots__))
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
raise AttributeError("can't set attribute")
|
||||
|
||||
def __repr__(self):
|
||||
return '{0}({1})'.format(self.__class__.__name__, ', '.join(
|
||||
'{0}={1!r}'.format(key, value)
|
||||
for key, value in six.iteritems(self)))
|
||||
|
||||
|
||||
class frozendict(collections.Mapping, collections.Hashable): # type: ignore
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
"""Frozen dictionary."""
|
||||
__slots__ = ('_items', '_keys')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs and not args:
|
||||
items = dict(kwargs)
|
||||
elif len(args) == 1 and isinstance(args[0], collections.Mapping):
|
||||
items = args[0]
|
||||
else:
|
||||
raise TypeError()
|
||||
# TODO: support generators/iterators
|
||||
|
||||
object.__setattr__(self, '_items', items)
|
||||
object.__setattr__(self, '_keys', tuple(sorted(six.iterkeys(items))))
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._items[key]
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._keys)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._items)
|
||||
|
||||
def _sorted_items(self):
|
||||
return tuple((key, self[key]) for key in self._keys)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._sorted_items())
|
||||
|
||||
def __getattr__(self, name):
|
||||
try:
|
||||
return self._items[name]
|
||||
except KeyError:
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
raise AttributeError("can't set attribute")
|
||||
|
||||
def __repr__(self):
|
||||
return 'frozendict({0})'.format(', '.join('{0}={1!r}'.format(
|
||||
key, value) for key, value in self._sorted_items()))
|
||||
@@ -1,199 +0,0 @@
|
||||
"""Tests for acme.jose.util."""
|
||||
import functools
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
from acme import test_util
|
||||
|
||||
|
||||
class ComparableX509Test(unittest.TestCase):
|
||||
"""Tests for acme.jose.util.ComparableX509."""
|
||||
|
||||
def setUp(self):
|
||||
# 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_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)
|
||||
self.assertEqual(self.cert1, self.cert2)
|
||||
|
||||
def test_ne(self):
|
||||
self.assertNotEqual(self.req1, self.req_other)
|
||||
self.assertNotEqual(self.cert1, self.cert_other)
|
||||
|
||||
def test_ne_wrong_types(self):
|
||||
self.assertNotEqual(self.req1, 5)
|
||||
self.assertNotEqual(self.cert1, 5)
|
||||
|
||||
def test_hash(self):
|
||||
self.assertEqual(hash(self.req1), hash(self.req2))
|
||||
self.assertNotEqual(hash(self.req1), hash(self.req_other))
|
||||
|
||||
self.assertEqual(hash(self.cert1), hash(self.cert2))
|
||||
self.assertNotEqual(hash(self.cert1), hash(self.cert_other))
|
||||
|
||||
def test_repr(self):
|
||||
for x509 in self.req1, self.cert1:
|
||||
self.assertEqual(repr(x509),
|
||||
'<ComparableX509({0!r})>'.format(x509.wrapped))
|
||||
|
||||
|
||||
class ComparableRSAKeyTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.util.ComparableRSAKey."""
|
||||
|
||||
def setUp(self):
|
||||
# test_utl.load_rsa_private_key return ComparableRSAKey
|
||||
self.key = test_util.load_rsa_private_key('rsa256_key.pem')
|
||||
self.key_same = test_util.load_rsa_private_key('rsa256_key.pem')
|
||||
self.key2 = test_util.load_rsa_private_key('rsa512_key.pem')
|
||||
|
||||
def test_getattr_proxy(self):
|
||||
self.assertEqual(256, self.key.key_size)
|
||||
|
||||
def test_eq(self):
|
||||
self.assertEqual(self.key, self.key_same)
|
||||
|
||||
def test_ne(self):
|
||||
self.assertNotEqual(self.key, self.key2)
|
||||
|
||||
def test_ne_different_types(self):
|
||||
self.assertNotEqual(self.key, 5)
|
||||
|
||||
def test_ne_not_wrapped(self):
|
||||
# pylint: disable=protected-access
|
||||
self.assertNotEqual(self.key, self.key_same._wrapped)
|
||||
|
||||
def test_ne_no_serialization(self):
|
||||
from acme.jose.util import ComparableRSAKey
|
||||
self.assertNotEqual(ComparableRSAKey(5), ComparableRSAKey(5))
|
||||
|
||||
def test_hash(self):
|
||||
self.assertTrue(isinstance(hash(self.key), int))
|
||||
self.assertEqual(hash(self.key), hash(self.key_same))
|
||||
self.assertNotEqual(hash(self.key), hash(self.key2))
|
||||
|
||||
def test_repr(self):
|
||||
self.assertTrue(repr(self.key).startswith(
|
||||
'<ComparableRSAKey(<cryptography.hazmat.'))
|
||||
|
||||
def test_public_key(self):
|
||||
from acme.jose.util import ComparableRSAKey
|
||||
self.assertTrue(isinstance(self.key.public_key(), ComparableRSAKey))
|
||||
|
||||
|
||||
class ImmutableMapTest(unittest.TestCase):
|
||||
"""Tests for acme.jose.util.ImmutableMap."""
|
||||
|
||||
def setUp(self):
|
||||
# pylint: disable=invalid-name,too-few-public-methods
|
||||
# pylint: disable=missing-docstring
|
||||
from acme.jose.util import ImmutableMap
|
||||
|
||||
class A(ImmutableMap):
|
||||
__slots__ = ('x', 'y')
|
||||
|
||||
class B(ImmutableMap):
|
||||
__slots__ = ('x', 'y')
|
||||
|
||||
self.A = A
|
||||
self.B = B
|
||||
|
||||
self.a1 = self.A(x=1, y=2)
|
||||
self.a1_swap = self.A(y=2, x=1)
|
||||
self.a2 = self.A(x=3, y=4)
|
||||
self.b = self.B(x=1, y=2)
|
||||
|
||||
def test_update(self):
|
||||
self.assertEqual(self.A(x=2, y=2), self.a1.update(x=2))
|
||||
self.assertEqual(self.a2, self.a1.update(x=3, y=4))
|
||||
|
||||
def test_get_missing_item_raises_key_error(self):
|
||||
self.assertRaises(KeyError, self.a1.__getitem__, 'z')
|
||||
|
||||
def test_order_of_args_does_not_matter(self):
|
||||
self.assertEqual(self.a1, self.a1_swap)
|
||||
|
||||
def test_type_error_on_missing(self):
|
||||
self.assertRaises(TypeError, self.A, x=1)
|
||||
self.assertRaises(TypeError, self.A, y=2)
|
||||
|
||||
def test_type_error_on_unrecognized(self):
|
||||
self.assertRaises(TypeError, self.A, x=1, z=2)
|
||||
self.assertRaises(TypeError, self.A, x=1, y=2, z=3)
|
||||
|
||||
def test_get_attr(self):
|
||||
self.assertEqual(1, self.a1.x)
|
||||
self.assertEqual(2, self.a1.y)
|
||||
self.assertEqual(1, self.a1_swap.x)
|
||||
self.assertEqual(2, self.a1_swap.y)
|
||||
|
||||
def test_set_attr_raises_attribute_error(self):
|
||||
self.assertRaises(
|
||||
AttributeError, functools.partial(self.a1.__setattr__, 'x'), 10)
|
||||
|
||||
def test_equal(self):
|
||||
self.assertEqual(self.a1, self.a1)
|
||||
self.assertEqual(self.a2, self.a2)
|
||||
self.assertNotEqual(self.a1, self.a2)
|
||||
|
||||
def test_hash(self):
|
||||
self.assertEqual(hash((1, 2)), hash(self.a1))
|
||||
|
||||
def test_unhashable(self):
|
||||
self.assertRaises(TypeError, self.A(x=1, y={}).__hash__)
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('A(x=1, y=2)', repr(self.a1))
|
||||
self.assertEqual('A(x=1, y=2)', repr(self.a1_swap))
|
||||
self.assertEqual('B(x=1, y=2)', repr(self.b))
|
||||
self.assertEqual("B(x='foo', y='bar')", repr(self.B(x='foo', y='bar')))
|
||||
|
||||
|
||||
class frozendictTest(unittest.TestCase): # pylint: disable=invalid-name
|
||||
"""Tests for acme.jose.util.frozendict."""
|
||||
|
||||
def setUp(self):
|
||||
from acme.jose.util import frozendict
|
||||
self.fdict = frozendict(x=1, y='2')
|
||||
|
||||
def test_init_dict(self):
|
||||
from acme.jose.util import frozendict
|
||||
self.assertEqual(self.fdict, frozendict({'x': 1, 'y': '2'}))
|
||||
|
||||
def test_init_other_raises_type_error(self):
|
||||
from acme.jose.util import frozendict
|
||||
# specifically fail for generators...
|
||||
self.assertRaises(TypeError, frozendict, six.iteritems({'a': 'b'}))
|
||||
|
||||
def test_len(self):
|
||||
self.assertEqual(2, len(self.fdict))
|
||||
|
||||
def test_hash(self):
|
||||
self.assertTrue(isinstance(hash(self.fdict), int))
|
||||
|
||||
def test_getattr_proxy(self):
|
||||
self.assertEqual(1, self.fdict.x)
|
||||
self.assertEqual('2', self.fdict.y)
|
||||
|
||||
def test_getattr_raises_attribute_error(self):
|
||||
self.assertRaises(AttributeError, self.fdict.__getattr__, 'z')
|
||||
|
||||
def test_setattr_immutable(self):
|
||||
self.assertRaises(AttributeError, self.fdict.__setattr__, 'z', 3)
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual("frozendict(x=1, y='2')", repr(self.fdict))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -1,10 +1,10 @@
|
||||
"""ACME-specific JWS.
|
||||
|
||||
The JWS implementation in acme.jose only implements the base JOSE standard. In
|
||||
The JWS implementation in josepy only implements the base JOSE standard. In
|
||||
order to support the new header fields defined in ACME, this module defines some
|
||||
ACME-specific classes that layer on top of acme.jose.
|
||||
ACME-specific classes that layer on top of josepy.
|
||||
"""
|
||||
from acme import jose
|
||||
import josepy as jose
|
||||
|
||||
|
||||
class Header(jose.Header):
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Tests for acme.jws."""
|
||||
import unittest
|
||||
|
||||
from acme import jose
|
||||
import josepy as jose
|
||||
|
||||
from acme import test_util
|
||||
|
||||
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
import collections
|
||||
import six
|
||||
|
||||
import josepy as jose
|
||||
|
||||
from acme import challenges
|
||||
from acme import errors
|
||||
from acme import fields
|
||||
from acme import jose
|
||||
from acme import util
|
||||
|
||||
OLD_ERROR_PREFIX = "urn:acme:error:"
|
||||
@@ -238,7 +239,7 @@ class ResourceBody(jose.JSONObjectWithFields):
|
||||
class Registration(ResourceBody):
|
||||
"""Registration Resource Body.
|
||||
|
||||
:ivar acme.jose.jwk.JWK key: Public key.
|
||||
:ivar josepy.jwk.JWK key: Public key.
|
||||
:ivar tuple contact: Contact information following ACME spec,
|
||||
`tuple` of `unicode`.
|
||||
:ivar unicode agreement:
|
||||
@@ -446,7 +447,7 @@ class AuthorizationResource(ResourceWithURI):
|
||||
class CertificateRequest(jose.JSONObjectWithFields):
|
||||
"""ACME new-cert request.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 csr:
|
||||
:ivar josepy.util.ComparableX509 csr:
|
||||
`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`
|
||||
|
||||
"""
|
||||
@@ -458,7 +459,7 @@ class CertificateRequest(jose.JSONObjectWithFields):
|
||||
class CertificateResource(ResourceWithURI):
|
||||
"""Certificate Resource.
|
||||
|
||||
:ivar acme.jose.util.ComparableX509 body:
|
||||
:ivar josepy.util.ComparableX509 body:
|
||||
`OpenSSL.crypto.X509` wrapped in `.ComparableX509`
|
||||
:ivar unicode cert_chain_uri: URI found in the 'up' ``Link`` header
|
||||
:ivar tuple authzrs: `tuple` of `AuthorizationResource`.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Tests for acme.messages."""
|
||||
import unittest
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
from acme import test_util
|
||||
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import unittest
|
||||
from six.moves import http_client # pylint: disable=import-error
|
||||
from six.moves import socketserver # type: ignore # pylint: disable=import-error
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
import requests
|
||||
|
||||
from acme import challenges
|
||||
from acme import crypto_util
|
||||
from acme import errors
|
||||
from acme import jose
|
||||
from acme import test_util
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,9 @@ import unittest
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
|
||||
from acme import jose
|
||||
|
||||
|
||||
def vector_path(*names):
|
||||
"""Path to a test vector."""
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
JOSE
|
||||
----
|
||||
|
||||
.. automodule:: acme.jose
|
||||
:members:
|
||||
The ``acme.jose`` module was moved to its own package "josepy_".
|
||||
Please refer to its documentation there.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
|
||||
jose/*
|
||||
.. _josepy: https://josepy.readthedocs.io/
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
JOSE Base64
|
||||
-----------
|
||||
|
||||
.. automodule:: acme.jose.b64
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
Errors
|
||||
------
|
||||
|
||||
.. automodule:: acme.jose.errors
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
Interfaces
|
||||
----------
|
||||
|
||||
.. automodule:: acme.jose.interfaces
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
JSON utilities
|
||||
--------------
|
||||
|
||||
.. automodule:: acme.jose.json_util
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
JSON Web Algorithms
|
||||
-------------------
|
||||
|
||||
.. automodule:: acme.jose.jwa
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
JSON Web Key
|
||||
------------
|
||||
|
||||
.. automodule:: acme.jose.jwk
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
JSON Web Signature
|
||||
------------------
|
||||
|
||||
.. automodule:: acme.jose.jws
|
||||
:members:
|
||||
@@ -1,5 +0,0 @@
|
||||
Utilities
|
||||
---------
|
||||
|
||||
.. automodule:: acme.jose.util
|
||||
:members:
|
||||
@@ -308,4 +308,5 @@ texinfo_documents = [
|
||||
|
||||
intersphinx_mapping = {
|
||||
'python': ('https://docs.python.org/', None),
|
||||
'josepy': ('https://josepy.readthedocs.io/en/latest/', None),
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@ import pkg_resources
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
|
||||
from acme import client
|
||||
from acme import messages
|
||||
from acme import jose
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
@@ -4,13 +4,15 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.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',
|
||||
# formerly known as acme.jose:
|
||||
'josepy>=1.0.0',
|
||||
# Connection.set_tlsext_host_name (>=0.13)
|
||||
'mock',
|
||||
'PyOpenSSL>=0.13',
|
||||
@@ -74,10 +76,5 @@ setup(
|
||||
'dev': dev_extras,
|
||||
'docs': docs_extras,
|
||||
},
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'jws = acme.jose.jws:CLI.run',
|
||||
],
|
||||
},
|
||||
test_suite='acme',
|
||||
)
|
||||
|
||||
@@ -93,4 +93,8 @@ def parse_define_file(filepath, varname):
|
||||
if v == "-D" and len(a_opts) >= i+2:
|
||||
var_parts = a_opts[i+1].partition("=")
|
||||
return_vars[var_parts[0]] = var_parts[2]
|
||||
elif len(v) > 2 and v.startswith("-D"):
|
||||
# Found var with no whitespace separator
|
||||
var_parts = v[2:].partition("=")
|
||||
return_vars[var_parts[0]] = var_parts[2]
|
||||
return return_vars
|
||||
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
|
||||
SSLHonorCipherOrder on
|
||||
|
||||
SSLOptions +StrictRequire
|
||||
|
||||
@@ -24,9 +24,10 @@ from certbot_apache import apache_util
|
||||
from certbot_apache import augeas_configurator
|
||||
from certbot_apache import constants
|
||||
from certbot_apache import display_ops
|
||||
from certbot_apache import tls_sni_01
|
||||
from certbot_apache import http_01
|
||||
from certbot_apache import obj
|
||||
from certbot_apache import parser
|
||||
from certbot_apache import tls_sni_01
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -163,6 +164,9 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"ensure-http-header": self._set_http_header,
|
||||
"staple-ocsp": self._enable_ocsp_stapling}
|
||||
|
||||
# This will be set during the perform function
|
||||
self.http_doer = None
|
||||
|
||||
@property
|
||||
def mod_ssl_conf(self):
|
||||
"""Full absolute path to SSL configuration file."""
|
||||
@@ -736,31 +740,43 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
|
||||
"""
|
||||
|
||||
# If nonstandard port, add service definition for matching
|
||||
if port != "443":
|
||||
self.prepare_https_modules(temp)
|
||||
self.ensure_listen(port, https=True)
|
||||
|
||||
def ensure_listen(self, port, https=False):
|
||||
"""Make sure that Apache is listening on the port. Checks if the
|
||||
Listen statement for the port already exists, and adds it to the
|
||||
configuration if necessary.
|
||||
|
||||
:param str port: Port number to check and add Listen for if not in
|
||||
place already
|
||||
:param bool https: If the port will be used for HTTPS
|
||||
|
||||
"""
|
||||
|
||||
# If HTTPS requested for nonstandard port, add service definition
|
||||
if https and port != "443":
|
||||
port_service = "%s %s" % (port, "https")
|
||||
else:
|
||||
port_service = port
|
||||
|
||||
self.prepare_https_modules(temp)
|
||||
# Check for Listen <port>
|
||||
# Note: This could be made to also look for ip:443 combo
|
||||
listens = [self.parser.get_arg(x).split()[0] for
|
||||
x in self.parser.find_dir("Listen")]
|
||||
|
||||
# In case no Listens are set (which really is a broken apache config)
|
||||
if not listens:
|
||||
listens = ["80"]
|
||||
|
||||
# Listen already in place
|
||||
if self._has_port_already(listens, port):
|
||||
return
|
||||
|
||||
listen_dirs = set(listens)
|
||||
|
||||
if not listens:
|
||||
listen_dirs.add(port_service)
|
||||
|
||||
for listen in listens:
|
||||
# For any listen statement, check if the machine also listens on
|
||||
# Port 443. If not, add such a listen statement.
|
||||
# the given port. If not, add such a listen statement.
|
||||
if len(listen.split(":")) == 1:
|
||||
# Its listening to all interfaces
|
||||
if port not in listen_dirs and port_service not in listen_dirs:
|
||||
@@ -772,11 +788,39 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
if "%s:%s" % (ip, port_service) not in listen_dirs and (
|
||||
"%s:%s" % (ip, port_service) not in listen_dirs):
|
||||
listen_dirs.add("%s:%s" % (ip, port_service))
|
||||
self._add_listens(listen_dirs, listens, port)
|
||||
if https:
|
||||
self._add_listens_https(listen_dirs, listens, port)
|
||||
else:
|
||||
self._add_listens_http(listen_dirs, listens, port)
|
||||
|
||||
def _add_listens(self, listens, listens_orig, port):
|
||||
"""Helper method for prepare_server_https to figure out which new
|
||||
listen statements need adding
|
||||
def _add_listens_http(self, listens, listens_orig, port):
|
||||
"""Helper method for ensure_listen to figure out which new
|
||||
listen statements need adding for listening HTTP on port
|
||||
|
||||
:param set listens: Set of all needed Listen statements
|
||||
:param list listens_orig: List of existing listen statements
|
||||
:param string port: Port number we're adding
|
||||
"""
|
||||
|
||||
new_listens = listens.difference(listens_orig)
|
||||
|
||||
if port in new_listens:
|
||||
# We have wildcard, skip the rest
|
||||
self.parser.add_dir(parser.get_aug_path(self.parser.loc["listen"]),
|
||||
"Listen", port)
|
||||
self.save_notes += "Added Listen %s directive to %s\n" % (
|
||||
port, self.parser.loc["listen"])
|
||||
else:
|
||||
for listen in new_listens:
|
||||
self.parser.add_dir(parser.get_aug_path(
|
||||
self.parser.loc["listen"]), "Listen", listen.split(" "))
|
||||
self.save_notes += ("Added Listen %s directive to "
|
||||
"%s\n") % (listen,
|
||||
self.parser.loc["listen"])
|
||||
|
||||
def _add_listens_https(self, listens, listens_orig, port):
|
||||
"""Helper method for ensure_listen to figure out which new
|
||||
listen statements need adding for listening HTTPS on port
|
||||
|
||||
:param set listens: Set of all needed Listen statements
|
||||
:param list listens_orig: List of existing listen statements
|
||||
@@ -1855,7 +1899,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
###########################################################################
|
||||
def get_chall_pref(self, unused_domain): # pylint: disable=no-self-use
|
||||
"""Return list of challenge preferences."""
|
||||
return [challenges.TLSSNI01]
|
||||
return [challenges.TLSSNI01, challenges.HTTP01]
|
||||
|
||||
def perform(self, achalls):
|
||||
"""Perform the configuration related challenge.
|
||||
@@ -1867,16 +1911,21 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
"""
|
||||
self._chall_out.update(achalls)
|
||||
responses = [None] * len(achalls)
|
||||
chall_doer = tls_sni_01.ApacheTlsSni01(self)
|
||||
self.http_doer = http_01.ApacheHttp01(self)
|
||||
sni_doer = tls_sni_01.ApacheTlsSni01(self)
|
||||
|
||||
for i, achall in enumerate(achalls):
|
||||
# Currently also have chall_doer hold associated index of the
|
||||
# challenge. This helps to put all of the responses back together
|
||||
# when they are all complete.
|
||||
chall_doer.add_chall(achall, i)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
self.http_doer.add_chall(achall, i)
|
||||
else: # tls-sni-01
|
||||
sni_doer.add_chall(achall, i)
|
||||
|
||||
sni_response = chall_doer.perform()
|
||||
if sni_response:
|
||||
http_response = self.http_doer.perform()
|
||||
sni_response = sni_doer.perform()
|
||||
if http_response or sni_response:
|
||||
# Must reload in order to activate the challenges.
|
||||
# Handled here because we may be able to load up other challenge
|
||||
# types
|
||||
@@ -1886,14 +1935,18 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
# of identifying when the new configuration is being used.
|
||||
time.sleep(3)
|
||||
|
||||
# Go through all of the challenges and assign them to the proper
|
||||
# place in the responses return value. All responses must be in the
|
||||
# same order as the original challenges.
|
||||
for i, resp in enumerate(sni_response):
|
||||
responses[chall_doer.indices[i]] = resp
|
||||
self._update_responses(responses, http_response, self.http_doer)
|
||||
self._update_responses(responses, sni_response, sni_doer)
|
||||
|
||||
return responses
|
||||
|
||||
def _update_responses(self, responses, chall_response, chall_doer):
|
||||
# Go through all of the challenges and assign them to the proper
|
||||
# place in the responses return value. All responses must be in the
|
||||
# same order as the original challenges.
|
||||
for i, resp in enumerate(chall_response):
|
||||
responses[chall_doer.indices[i]] = resp
|
||||
|
||||
def cleanup(self, achalls):
|
||||
"""Revert all challenges."""
|
||||
self._chall_out.difference_update(achalls)
|
||||
@@ -1903,6 +1956,7 @@ class ApacheConfigurator(augeas_configurator.AugeasConfigurator):
|
||||
self.revert_challenge_config()
|
||||
self.restart()
|
||||
self.parser.reset_modules()
|
||||
self.http_doer.cleanup()
|
||||
|
||||
def install_ssl_options_conf(self, options_ssl, options_ssl_digest):
|
||||
"""Copy Certbot's SSL options file into the system's config dir if required."""
|
||||
|
||||
@@ -16,6 +16,8 @@ ALL_SSL_OPTIONS_HASHES = [
|
||||
'4066b90268c03c9ba0201068eaa39abbc02acf9558bb45a788b630eb85dadf27',
|
||||
'f175e2e7c673bd88d0aff8220735f385f916142c44aa83b09f1df88dd4767a88',
|
||||
'cfdd7c18d2025836ea3307399f509cfb1ebf2612c87dd600a65da2a8e2f2797b',
|
||||
'80720bd171ccdc2e6b917ded340defae66919e4624962396b992b7218a561791',
|
||||
'c0c022ea6b8a51ecc8f1003d0a04af6c3f2bc1c3ce506b3c2dfc1f11ef931082',
|
||||
]
|
||||
"""SHA256 hashes of the contents of previous versions of all versions of MOD_SSL_CONF_SRC"""
|
||||
|
||||
|
||||
112
certbot-apache/certbot_apache/http_01.py
Normal file
112
certbot-apache/certbot_apache/http_01.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""A class that performs HTTP-01 challenges for Apache"""
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from certbot.plugins import common
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ApacheHttp01(common.TLSSNI01):
|
||||
"""Class that performs HTPP-01 challenges within the Apache configurator."""
|
||||
|
||||
CONFIG_TEMPLATE24 = """\
|
||||
Alias /.well-known/acme-challenge {0}
|
||||
|
||||
<Directory {0} >
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
"""
|
||||
|
||||
CONFIG_TEMPLATE22 = """\
|
||||
Alias /.well-known/acme-challenge {0}
|
||||
|
||||
<Directory {0} >
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ApacheHttp01, self).__init__(*args, **kwargs)
|
||||
self.challenge_conf = os.path.join(
|
||||
self.configurator.conf("challenge-location"),
|
||||
"le_http_01_challenge.conf")
|
||||
self.challenge_dir = None
|
||||
|
||||
def perform(self):
|
||||
"""Perform all HTTP-01 challenges."""
|
||||
if not self.achalls:
|
||||
return []
|
||||
# Save any changes to the configuration as a precaution
|
||||
# About to make temporary changes to the config
|
||||
self.configurator.save("Changes before challenge setup", True)
|
||||
|
||||
self.configurator.ensure_listen(str(
|
||||
self.configurator.config.http01_port))
|
||||
self.prepare_http01_modules()
|
||||
|
||||
responses = self._set_up_challenges()
|
||||
self._mod_config()
|
||||
# Save reversible changes
|
||||
self.configurator.save("HTTP Challenge", True)
|
||||
|
||||
return responses
|
||||
|
||||
def cleanup(self):
|
||||
"""Cleanup the challenge directory."""
|
||||
if self.challenge_dir:
|
||||
shutil.rmtree(self.challenge_dir, ignore_errors=True)
|
||||
self.challenge_dir = None
|
||||
|
||||
def prepare_http01_modules(self):
|
||||
"""Make sure that we have the needed modules available for http01"""
|
||||
|
||||
if self.configurator.conf("handle-modules"):
|
||||
needed_modules = ["alias"]
|
||||
if self.configurator.version < (2, 4):
|
||||
needed_modules.append("authz_host")
|
||||
else:
|
||||
needed_modules.append("authz_core")
|
||||
for mod in needed_modules:
|
||||
if mod + "_module" not in self.configurator.parser.modules:
|
||||
self.configurator.enable_mod(mod, temp=True)
|
||||
|
||||
def _mod_config(self):
|
||||
self.configurator.parser.add_include(
|
||||
self.configurator.parser.loc["default"], self.challenge_conf)
|
||||
self.configurator.reverter.register_file_creation(
|
||||
True, self.challenge_conf)
|
||||
|
||||
if self.configurator.version < (2, 4):
|
||||
config_template = self.CONFIG_TEMPLATE22
|
||||
else:
|
||||
config_template = self.CONFIG_TEMPLATE24
|
||||
config_text = config_template.format(self.challenge_dir)
|
||||
|
||||
logger.debug("writing a config file with text:\n %s", config_text)
|
||||
with open(self.challenge_conf, "w") as new_conf:
|
||||
new_conf.write(config_text)
|
||||
|
||||
def _set_up_challenges(self):
|
||||
self.challenge_dir = tempfile.mkdtemp()
|
||||
os.chmod(self.challenge_dir, 0o755)
|
||||
|
||||
responses = []
|
||||
for achall in self.achalls:
|
||||
responses.append(self._set_up_challenge(achall))
|
||||
|
||||
return responses
|
||||
|
||||
def _set_up_challenge(self, achall):
|
||||
response, validation = achall.response_and_validation()
|
||||
|
||||
name = os.path.join(self.challenge_dir, achall.chall.encode("token"))
|
||||
with open(name, 'wb') as f:
|
||||
f.write(validation.encode())
|
||||
os.chmod(name, 0o644)
|
||||
|
||||
return response
|
||||
@@ -8,7 +8,7 @@ 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
|
||||
SSLCipherSuite ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
|
||||
SSLHonorCipherOrder on
|
||||
SSLCompression off
|
||||
|
||||
|
||||
@@ -140,5 +140,5 @@ class DebianConfigurator(configurator.ApacheConfigurator):
|
||||
"a2dismod are configured correctly for certbot.")
|
||||
|
||||
self.reverter.register_undo_command(
|
||||
temp, [self.conf("dismod"), mod_name])
|
||||
temp, [self.conf("dismod"), "-f", mod_name])
|
||||
util.run_script([self.conf("enmod"), mod_name])
|
||||
|
||||
@@ -49,6 +49,7 @@ class GentooParser(parser.ApacheParser):
|
||||
def update_runtime_variables(self):
|
||||
""" Override for update_runtime_variables for custom parsing """
|
||||
self.parse_sysconfig_var()
|
||||
self.update_modules()
|
||||
|
||||
def parse_sysconfig_var(self):
|
||||
""" Parses Apache CLI options from Gentoo configuration file """
|
||||
@@ -56,3 +57,10 @@ class GentooParser(parser.ApacheParser):
|
||||
"APACHE2_OPTS")
|
||||
for k in defines.keys():
|
||||
self.variables[k] = defines[k]
|
||||
|
||||
def update_modules(self):
|
||||
"""Get loaded modules from httpd process, and add them to DOM"""
|
||||
mod_cmd = [self.configurator.constant("apache_cmd"), "modules"]
|
||||
matches = self.parse_from_subprocess(mod_cmd, r"(.*)_module")
|
||||
for mod in matches:
|
||||
self.add_mod(mod.strip())
|
||||
|
||||
@@ -118,6 +118,8 @@ class MultipleVhostsTestCentOS(util.ApacheTest):
|
||||
self.assertTrue("mock_define_too" in self.config.parser.variables.keys())
|
||||
self.assertTrue("mock_value" in self.config.parser.variables.keys())
|
||||
self.assertEqual("TRUE", self.config.parser.variables["mock_value"])
|
||||
self.assertTrue("MOCK_NOSEP" in self.config.parser.variables.keys())
|
||||
self.assertEqual("NOSEP_VAL", self.config.parser.variables["NOSEP_TWO"])
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -424,6 +424,43 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.assertTrue(self.config.parser.find_dir(
|
||||
"NameVirtualHost", "*:80"))
|
||||
|
||||
def test_add_listen_80(self):
|
||||
mock_find = mock.Mock()
|
||||
mock_add_dir = mock.Mock()
|
||||
mock_find.return_value = []
|
||||
self.config.parser.find_dir = mock_find
|
||||
self.config.parser.add_dir = mock_add_dir
|
||||
self.config.ensure_listen("80")
|
||||
self.assertTrue(mock_add_dir.called)
|
||||
self.assertTrue(mock_find.called)
|
||||
self.assertEqual(mock_add_dir.call_args[0][1], "Listen")
|
||||
self.assertEqual(mock_add_dir.call_args[0][2], "80")
|
||||
|
||||
def test_add_listen_80_named(self):
|
||||
mock_find = mock.Mock()
|
||||
mock_find.return_value = ["test1", "test2", "test3"]
|
||||
mock_get = mock.Mock()
|
||||
mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
|
||||
mock_add_dir = mock.Mock()
|
||||
|
||||
self.config.parser.find_dir = mock_find
|
||||
self.config.parser.get_arg = mock_get
|
||||
self.config.parser.add_dir = mock_add_dir
|
||||
|
||||
self.config.ensure_listen("80")
|
||||
self.assertEqual(mock_add_dir.call_count, 0)
|
||||
|
||||
# Reset return lists and inputs
|
||||
mock_add_dir.reset_mock()
|
||||
mock_get.side_effect = ["1.2.3.4:80", "[::1]:80", "1.1.1.1:443"]
|
||||
|
||||
# Test
|
||||
self.config.ensure_listen("8080")
|
||||
self.assertEqual(mock_add_dir.call_count, 3)
|
||||
self.assertTrue(mock_add_dir.called)
|
||||
self.assertEqual(mock_add_dir.call_args[0][1], "Listen")
|
||||
self.assertEqual(mock_add_dir.call_args[0][2], ['1.2.3.4:8080'])
|
||||
|
||||
def test_prepare_server_https(self):
|
||||
mock_enable = mock.Mock()
|
||||
self.config.enable_mod = mock_enable
|
||||
@@ -435,7 +472,6 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
# This will test the Add listen
|
||||
self.config.parser.find_dir = mock_find
|
||||
self.config.parser.add_dir_to_ifmodssl = mock_add_dir
|
||||
|
||||
self.config.prepare_server_https("443")
|
||||
# Changing the order these modules are enabled breaks the reverter
|
||||
self.assertEqual(mock_enable.call_args_list[0][0][0], "socache_shmcb")
|
||||
@@ -676,23 +712,33 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
self.config._add_name_vhost_if_necessary(self.vh_truth[0])
|
||||
self.assertEqual(self.config.add_name_vhost.call_count, 2)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.http_01.ApacheHttp01.perform")
|
||||
@mock.patch("certbot_apache.configurator.tls_sni_01.ApacheTlsSni01.perform")
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
def test_perform(self, mock_restart, mock_perform):
|
||||
def test_perform(self, mock_restart, mock_tls_perform, mock_http_perform):
|
||||
# Only tests functionality specific to configurator.perform
|
||||
# Note: As more challenges are offered this will have to be expanded
|
||||
account_key, achall1, achall2 = self.get_achalls()
|
||||
account_key, achalls = self.get_key_and_achalls()
|
||||
|
||||
expected = [
|
||||
achall1.response(account_key),
|
||||
achall2.response(account_key),
|
||||
]
|
||||
all_expected = []
|
||||
http_expected = []
|
||||
tls_expected = []
|
||||
for achall in achalls:
|
||||
response = achall.response(account_key)
|
||||
if isinstance(achall.chall, challenges.HTTP01):
|
||||
http_expected.append(response)
|
||||
else:
|
||||
tls_expected.append(response)
|
||||
all_expected.append(response)
|
||||
|
||||
mock_perform.return_value = expected
|
||||
responses = self.config.perform([achall1, achall2])
|
||||
mock_http_perform.return_value = http_expected
|
||||
mock_tls_perform.return_value = tls_expected
|
||||
|
||||
self.assertEqual(mock_perform.call_count, 1)
|
||||
self.assertEqual(responses, expected)
|
||||
responses = self.config.perform(achalls)
|
||||
|
||||
self.assertEqual(mock_http_perform.call_count, 1)
|
||||
self.assertEqual(mock_tls_perform.call_count, 1)
|
||||
self.assertEqual(responses, all_expected)
|
||||
|
||||
self.assertEqual(mock_restart.call_count, 1)
|
||||
|
||||
@@ -700,30 +746,38 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
||||
def test_cleanup(self, mock_cfg, mock_restart):
|
||||
mock_cfg.return_value = ""
|
||||
_, achall1, achall2 = self.get_achalls()
|
||||
_, achalls = self.get_key_and_achalls()
|
||||
self.config.http_doer = mock.MagicMock()
|
||||
|
||||
self.config._chall_out.add(achall1) # pylint: disable=protected-access
|
||||
self.config._chall_out.add(achall2) # pylint: disable=protected-access
|
||||
for achall in achalls:
|
||||
self.config._chall_out.add(achall) # pylint: disable=protected-access
|
||||
|
||||
self.config.cleanup([achall1])
|
||||
self.assertFalse(mock_restart.called)
|
||||
|
||||
self.config.cleanup([achall2])
|
||||
self.assertTrue(mock_restart.called)
|
||||
for i, achall in enumerate(achalls):
|
||||
self.config.cleanup([achall])
|
||||
if i == len(achalls) - 1:
|
||||
self.assertTrue(mock_restart.called)
|
||||
self.assertTrue(self.config.http_doer.cleanup.called)
|
||||
else:
|
||||
self.assertFalse(mock_restart.called)
|
||||
self.assertFalse(self.config.http_doer.cleanup.called)
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.restart")
|
||||
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
||||
def test_cleanup_no_errors(self, mock_cfg, mock_restart):
|
||||
mock_cfg.return_value = ""
|
||||
_, achall1, achall2 = self.get_achalls()
|
||||
_, achalls = self.get_key_and_achalls()
|
||||
self.config.http_doer = mock.MagicMock()
|
||||
|
||||
self.config._chall_out.add(achall1) # pylint: disable=protected-access
|
||||
for achall in achalls:
|
||||
self.config._chall_out.add(achall) # pylint: disable=protected-access
|
||||
|
||||
self.config.cleanup([achall2])
|
||||
self.config.cleanup([achalls[-1]])
|
||||
self.assertFalse(mock_restart.called)
|
||||
self.assertFalse(self.config.http_doer.cleanup.called)
|
||||
|
||||
self.config.cleanup([achall1, achall2])
|
||||
self.config.cleanup(achalls)
|
||||
self.assertTrue(mock_restart.called)
|
||||
self.assertTrue(self.config.http_doer.cleanup.called)
|
||||
|
||||
@mock.patch("certbot.util.run_script")
|
||||
def test_get_version(self, mock_script):
|
||||
@@ -1151,7 +1205,7 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
not_rewriterule = "NotRewriteRule ^ ..."
|
||||
self.assertFalse(self.config._sift_rewrite_rule(not_rewriterule))
|
||||
|
||||
def get_achalls(self):
|
||||
def get_key_and_achalls(self):
|
||||
"""Return testing achallenges."""
|
||||
account_key = self.rsa512jwk
|
||||
achall1 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
@@ -1166,8 +1220,12 @@ class MultipleVhostsTest(util.ApacheTest):
|
||||
token=b"uqnaPzxtrndteOqtrXb0Asl5gOJfWAnnx6QJyvcmlDU"),
|
||||
"pending"),
|
||||
domain="certbot.demo", account_key=account_key)
|
||||
achall3 = achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(token=(b'x' * 16)), "pending"),
|
||||
domain="example.org", account_key=account_key)
|
||||
|
||||
return account_key, achall1, achall2
|
||||
return account_key, (achall1, achall2, achall3)
|
||||
|
||||
def test_make_addrs_sni_ready(self):
|
||||
self.config.version = (2, 2)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import os
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
from certbot_apache import override_gentoo
|
||||
from certbot_apache import obj
|
||||
from certbot_apache.tests import util
|
||||
@@ -46,9 +48,10 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
|
||||
config_root=config_root,
|
||||
vhost_root=vhost_root)
|
||||
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
|
||||
os_info="gentoo")
|
||||
with mock.patch("certbot_apache.override_gentoo.GentooParser.update_runtime_variables"):
|
||||
self.config = util.get_apache_configurator(
|
||||
self.config_path, self.vhost_path, self.config_dir, self.work_dir,
|
||||
os_info="gentoo")
|
||||
self.vh_truth = get_vh_truth(
|
||||
self.temp_dir, "gentoo_apache/apache")
|
||||
|
||||
@@ -78,9 +81,47 @@ class MultipleVhostsTestGentoo(util.ApacheTest):
|
||||
self.config.parser.apacheconfig_filep = os.path.realpath(
|
||||
os.path.join(self.config.parser.root, "../conf.d/apache2"))
|
||||
self.config.parser.variables = {}
|
||||
self.config.parser.update_runtime_variables()
|
||||
with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"):
|
||||
self.config.parser.update_runtime_variables()
|
||||
for define in defines:
|
||||
self.assertTrue(define in self.config.parser.variables.keys())
|
||||
|
||||
@mock.patch("certbot_apache.parser.ApacheParser.parse_from_subprocess")
|
||||
def test_no_binary_configdump(self, mock_subprocess):
|
||||
"""Make sure we don't call binary dumps other than modules from Apache
|
||||
as this is not supported in Gentoo currently"""
|
||||
|
||||
with mock.patch("certbot_apache.override_gentoo.GentooParser.update_modules"):
|
||||
self.config.parser.update_runtime_variables()
|
||||
self.config.parser.reset_modules()
|
||||
self.assertFalse(mock_subprocess.called)
|
||||
|
||||
self.config.parser.update_runtime_variables()
|
||||
self.config.parser.reset_modules()
|
||||
self.assertTrue(mock_subprocess.called)
|
||||
|
||||
@mock.patch("certbot_apache.parser.ApacheParser._get_runtime_cfg")
|
||||
def test_opportunistic_httpd_runtime_parsing(self, mock_get):
|
||||
mod_val = (
|
||||
'Loaded Modules:\n'
|
||||
' mock_module (static)\n'
|
||||
' another_module (static)\n'
|
||||
)
|
||||
def mock_get_cfg(command):
|
||||
"""Mock httpd process stdout"""
|
||||
if command == ['apache2ctl', 'modules']:
|
||||
return mod_val
|
||||
mock_get.side_effect = mock_get_cfg
|
||||
self.config.parser.modules = set()
|
||||
|
||||
with mock.patch("certbot.util.get_os_info") as mock_osi:
|
||||
# Make sure we have the have the CentOS httpd constants
|
||||
mock_osi.return_value = ("gentoo", "123")
|
||||
self.config.parser.update_runtime_variables()
|
||||
|
||||
self.assertEquals(mock_get.call_count, 1)
|
||||
self.assertEquals(len(self.config.parser.modules), 4)
|
||||
self.assertTrue("mod_another.c" in self.config.parser.modules)
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
152
certbot-apache/certbot_apache/tests/http_01_test.py
Normal file
152
certbot-apache/certbot_apache/tests/http_01_test.py
Normal file
@@ -0,0 +1,152 @@
|
||||
"""Test for certbot_apache.http_01."""
|
||||
import mock
|
||||
import os
|
||||
import unittest
|
||||
|
||||
from acme import challenges
|
||||
|
||||
from certbot import achallenges
|
||||
|
||||
from certbot.tests import acme_util
|
||||
|
||||
from certbot_apache.tests import util
|
||||
|
||||
|
||||
NUM_ACHALLS = 3
|
||||
|
||||
|
||||
class ApacheHttp01TestMeta(type):
|
||||
"""Generates parmeterized tests for testing perform."""
|
||||
def __new__(mcs, name, bases, class_dict):
|
||||
|
||||
def _gen_test(num_achalls, minor_version):
|
||||
def _test(self):
|
||||
achalls = self.achalls[:num_achalls]
|
||||
self.config.version = (2, minor_version)
|
||||
self.common_perform_test(achalls)
|
||||
return _test
|
||||
|
||||
for i in range(1, NUM_ACHALLS + 1):
|
||||
for j in (2, 4):
|
||||
test_name = "test_perform_{0}_{1}".format(i, j)
|
||||
class_dict[test_name] = _gen_test(i, j)
|
||||
return type.__new__(mcs, name, bases, class_dict)
|
||||
|
||||
|
||||
class ApacheHttp01Test(util.ApacheTest):
|
||||
"""Test for certbot_apache.http_01.ApacheHttp01."""
|
||||
|
||||
__metaclass__ = ApacheHttp01TestMeta
|
||||
|
||||
def setUp(self, *args, **kwargs):
|
||||
super(ApacheHttp01Test, self).setUp(*args, **kwargs)
|
||||
|
||||
self.account_key = self.rsa512jwk
|
||||
self.achalls = []
|
||||
for i in range(NUM_ACHALLS):
|
||||
self.achalls.append(
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.HTTP01(token=((chr(ord('a') + i) * 16))),
|
||||
"pending"),
|
||||
domain="example{0}.com".format(i),
|
||||
account_key=self.account_key))
|
||||
|
||||
modules = ["alias", "authz_core", "authz_host"]
|
||||
for mod in modules:
|
||||
self.config.parser.modules.add("mod_{0}.c".format(mod))
|
||||
self.config.parser.modules.add(mod + "_module")
|
||||
|
||||
from certbot_apache.http_01 import ApacheHttp01
|
||||
self.http = ApacheHttp01(self.config)
|
||||
|
||||
def test_empty_perform(self):
|
||||
self.assertFalse(self.http.perform())
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||
def test_enable_modules_22(self, mock_enmod):
|
||||
self.config.version = (2, 2)
|
||||
self.config.parser.modules.remove("authz_host_module")
|
||||
self.config.parser.modules.remove("mod_authz_host.c")
|
||||
|
||||
enmod_calls = self.common_enable_modules_test(mock_enmod)
|
||||
self.assertEqual(enmod_calls[0][0][0], "authz_host")
|
||||
|
||||
@mock.patch("certbot_apache.configurator.ApacheConfigurator.enable_mod")
|
||||
def test_enable_modules_24(self, mock_enmod):
|
||||
self.config.parser.modules.remove("authz_core_module")
|
||||
self.config.parser.modules.remove("mod_authz_core.c")
|
||||
|
||||
enmod_calls = self.common_enable_modules_test(mock_enmod)
|
||||
self.assertEqual(enmod_calls[0][0][0], "authz_core")
|
||||
|
||||
def common_enable_modules_test(self, mock_enmod):
|
||||
"""Tests enabling mod_alias and other modules."""
|
||||
self.config.parser.modules.remove("alias_module")
|
||||
self.config.parser.modules.remove("mod_alias.c")
|
||||
|
||||
self.http.prepare_http01_modules()
|
||||
|
||||
self.assertTrue(mock_enmod.called)
|
||||
calls = mock_enmod.call_args_list
|
||||
other_calls = []
|
||||
for call in calls:
|
||||
if "alias" != call[0][0]:
|
||||
other_calls.append(call)
|
||||
|
||||
# If these lists are equal, we never enabled mod_alias
|
||||
self.assertNotEqual(calls, other_calls)
|
||||
return other_calls
|
||||
|
||||
def common_perform_test(self, achalls):
|
||||
"""Tests perform with the given achalls."""
|
||||
for achall in achalls:
|
||||
self.http.add_chall(achall)
|
||||
|
||||
expected_response = [
|
||||
achall.response(self.account_key) for achall in achalls]
|
||||
self.assertEqual(self.http.perform(), expected_response)
|
||||
|
||||
self.assertTrue(os.path.isdir(self.http.challenge_dir))
|
||||
self._has_min_permissions(self.http.challenge_dir, 0o755)
|
||||
self._test_challenge_conf()
|
||||
|
||||
for achall in achalls:
|
||||
self._test_challenge_file(achall)
|
||||
|
||||
challenge_dir = self.http.challenge_dir
|
||||
self.http.cleanup()
|
||||
self.assertFalse(os.path.exists(challenge_dir))
|
||||
|
||||
def _test_challenge_conf(self):
|
||||
self.assertEqual(
|
||||
len(self.config.parser.find_dir(
|
||||
"Include", self.http.challenge_conf)), 1)
|
||||
|
||||
with open(self.http.challenge_conf) as f:
|
||||
conf_contents = f.read()
|
||||
|
||||
alias_fmt = "Alias /.well-known/acme-challenge {0}"
|
||||
alias = alias_fmt.format(self.http.challenge_dir)
|
||||
self.assertTrue(alias in conf_contents)
|
||||
if self.config.version < (2, 4):
|
||||
self.assertTrue("Allow from all" in conf_contents)
|
||||
else:
|
||||
self.assertTrue("Require all granted" in conf_contents)
|
||||
|
||||
def _test_challenge_file(self, achall):
|
||||
name = os.path.join(self.http.challenge_dir, achall.chall.encode("token"))
|
||||
validation = achall.validation(self.account_key)
|
||||
|
||||
self._has_min_permissions(name, 0o644)
|
||||
with open(name, 'rb') as f:
|
||||
self.assertEqual(f.read(), validation.encode())
|
||||
|
||||
def _has_min_permissions(self, path, min_mode):
|
||||
"""Tests the given file has at least the permissions in mode."""
|
||||
st_mode = os.stat(path).st_mode
|
||||
self.assertEqual(st_mode, st_mode | min_mode)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
@@ -14,7 +14,7 @@
|
||||
# To pass additional options (for instance, -D definitions) to the
|
||||
# httpd binary at startup, set OPTIONS here.
|
||||
#
|
||||
OPTIONS="-D mock_define -D mock_define_too -D mock_value=TRUE"
|
||||
OPTIONS="-D mock_define -D mock_define_too -D mock_value=TRUE -DMOCK_NOSEP -DNOSEP_TWO=NOSEP_VAL"
|
||||
|
||||
#
|
||||
# This setting ensures the httpd process is started in the "C" locale
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Test for certbot_apache.tls_sni_01."""
|
||||
import unittest
|
||||
import shutil
|
||||
import unittest
|
||||
|
||||
import mock
|
||||
|
||||
@@ -16,8 +16,8 @@ from six.moves import xrange # pylint: disable=redefined-builtin, import-error
|
||||
class TlsSniPerformTest(util.ApacheTest):
|
||||
"""Test the ApacheTlsSni01 challenge."""
|
||||
|
||||
auth_key = common_test.TLSSNI01Test.auth_key
|
||||
achalls = common_test.TLSSNI01Test.achalls
|
||||
auth_key = common_test.AUTH_KEY
|
||||
achalls = common_test.ACHALLS
|
||||
|
||||
def setUp(self): # pylint: disable=arguments-differ
|
||||
super(TlsSniPerformTest, self).setUp()
|
||||
|
||||
@@ -5,11 +5,10 @@ import sys
|
||||
import unittest
|
||||
|
||||
import augeas
|
||||
import josepy as jose
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from acme import jose
|
||||
|
||||
from certbot.display import util as display_util
|
||||
|
||||
from certbot.plugins import common
|
||||
@@ -104,6 +103,7 @@ def get_apache_configurator( # pylint: disable=too-many-arguments, too-many-loc
|
||||
apache_challenge_location=config_path,
|
||||
backup_dir=backups,
|
||||
config_dir=config_dir,
|
||||
http01_port=80,
|
||||
temp_checkpoint_dir=os.path.join(work_dir, "temp_checkpoints"),
|
||||
in_progress_dir=os.path.join(backups, "IN_PROGRESS"),
|
||||
work_dir=work_dir)
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
33
certbot-auto
33
certbot-auto
@@ -31,7 +31,7 @@ if [ -z "$VENV_PATH" ]; then
|
||||
fi
|
||||
VENV_BIN="$VENV_PATH/bin"
|
||||
BOOTSTRAP_VERSION_PATH="$VENV_PATH/certbot-auto-bootstrap-version.txt"
|
||||
LE_AUTO_VERSION="0.19.0"
|
||||
LE_AUTO_VERSION="0.20.0"
|
||||
BASENAME=$(basename $0)
|
||||
USAGE="Usage: $BASENAME [OPTIONS]
|
||||
A self-updating wrapper script for the Certbot ACME client. When run, updates
|
||||
@@ -1062,9 +1062,10 @@ zope.interface==4.1.3 \
|
||||
--hash=sha256:928138365245a0e8869a5999fbcc2a45475a0a6ed52a494d60dbdc540335fedd \
|
||||
--hash=sha256:0d841ba1bb840eea0e6489dc5ecafa6125554971f53b5acb87764441e61bceba \
|
||||
--hash=sha256:b09c8c1d47b3531c400e0195697f1414a63221de6ef478598a4f1460f7d9a392
|
||||
mock==2.0.0 \
|
||||
--hash=sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1 \
|
||||
--hash=sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba
|
||||
# Using an older version of mock here prevents regressions of #5276.
|
||||
mock==1.3.0 \
|
||||
--hash=sha256:3f573a18be94de886d1191f27c168427ef693e8dcfcecf95b170577b2eb69cbb \
|
||||
--hash=sha256:1e247dbecc6ce057299eb7ee019ad68314bb93152e81d9a6110d35f4d5eca0f6
|
||||
|
||||
# Contains the requirements for the letsencrypt package.
|
||||
#
|
||||
@@ -1077,18 +1078,18 @@ letsencrypt==0.7.0 \
|
||||
--hash=sha256:105a5fb107e45bcd0722eb89696986dcf5f08a86a321d6aef25a0c7c63375ade \
|
||||
--hash=sha256:c36e532c486a7e92155ee09da54b436a3c420813ec1c590b98f635d924720de9
|
||||
|
||||
certbot==0.19.0 \
|
||||
--hash=sha256:3207ee5319bfc37e855c25a43148275fcfb37869eefde9087405012049734a20 \
|
||||
--hash=sha256:a7230791dff5d085738119fc22d88ad9d8a35d0b6a3d67806fe33990c7c79d53
|
||||
acme==0.19.0 \
|
||||
--hash=sha256:c612eafe234d722d97bb5d3dbc49e5522f44be29611f7577954eb893e5c2d6de \
|
||||
--hash=sha256:1fa23d64d494aaf001e6fe857c461fcfff10f75a1c2c35ec831447f641e1e822
|
||||
certbot-apache==0.19.0 \
|
||||
--hash=sha256:fadb28b33bfabc85cdb962b5b149bef58b98f0606b78581db7895fe38323f37c \
|
||||
--hash=sha256:70306ca2d5be7f542af68d46883c0ae39527cf202f17ef92cd256fb0bc3f1619
|
||||
certbot-nginx==0.19.0 \
|
||||
--hash=sha256:4909cb3db49919fb35590793cac28e1c0b6dbd29cbedf887b9106e5fcef5362c \
|
||||
--hash=sha256:cb5a224a3f277092555c25096d1678fc735306fd3a43447649ebe524c7ca79e1
|
||||
certbot==0.20.0 \
|
||||
--hash=sha256:c6b6bd288700898d1eb31a65b605e3a5fc10f1e3213ce468207d76a2decb9d35 \
|
||||
--hash=sha256:cabf505b64fb400c4239dcdbaeb882079477eb6a8442268596a8791b9e34de88
|
||||
acme==0.20.0 \
|
||||
--hash=sha256:8b0cee192c0d76d6f4045bdb14b3cfd29d9720e0dad2046794a2a555f1eaccb7 \
|
||||
--hash=sha256:45121aed6c8cc2f31896ac1083068dfdeb613f3edeff9576dc0d10632ea5a3d5
|
||||
certbot-apache==0.20.0 \
|
||||
--hash=sha256:f7e4dbc154d2e9d1461118b6dd3dbd16f6892da468f060eeaa162aff673347e2 \
|
||||
--hash=sha256:0ba499706451ffbccb172bcf93d6ef4c6cc8599157077a4fa6dfbe5a83c7921f
|
||||
certbot-nginx==0.20.0 \
|
||||
--hash=sha256:b6e372e8740b20dd9bd63837646157ac97b3c9a65affd3954571b8e872ae9ecf \
|
||||
--hash=sha256:6379fdf20d9a7651fe30bb8d4b828cbea178cc263d7af5a380fc4508d793b9ae
|
||||
|
||||
UNLIKELY_EOF
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -6,7 +6,8 @@ import re
|
||||
import shutil
|
||||
import tarfile
|
||||
|
||||
from acme import jose
|
||||
import josepy as jose
|
||||
|
||||
from acme import test_util
|
||||
from certbot import constants
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'certbot',
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -3,7 +3,7 @@ import sys
|
||||
from distutils.core import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
install_requires = [
|
||||
'acme=={0}'.format(version),
|
||||
|
||||
@@ -23,43 +23,24 @@ from certbot import util
|
||||
from certbot.plugins import common
|
||||
|
||||
from certbot_nginx import constants
|
||||
from certbot_nginx import tls_sni_01
|
||||
from certbot_nginx import nginxparser
|
||||
from certbot_nginx import parser
|
||||
from certbot_nginx import tls_sni_01
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REDIRECT_BLOCK = [[
|
||||
['\n ', 'if', ' ', '($scheme', ' ', '!=', ' ', '"https")'],
|
||||
[['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],
|
||||
'\n ']
|
||||
], ['\n']]
|
||||
|
||||
TEST_REDIRECT_BLOCK = [
|
||||
[
|
||||
['if', '($scheme', '!=', '"https")'],
|
||||
[
|
||||
['return', '301', 'https://$host$request_uri']
|
||||
]
|
||||
],
|
||||
['#', ' managed by Certbot']
|
||||
REDIRECT_BLOCK = [
|
||||
['\n ', 'return', ' ', '301', ' ', 'https://$host$request_uri'],
|
||||
['\n']
|
||||
]
|
||||
|
||||
REDIRECT_COMMENT_BLOCK = [
|
||||
['\n ', '#', ' Redirect non-https traffic to https'],
|
||||
['\n ', '#', ' if ($scheme != "https") {'],
|
||||
['\n ', '#', " return 301 https://$host$request_uri;"],
|
||||
['\n ', '#', " } # managed by Certbot"],
|
||||
['\n ', '#', ' return 301 https://$host$request_uri;'],
|
||||
['\n']
|
||||
]
|
||||
|
||||
TEST_REDIRECT_COMMENT_BLOCK = [
|
||||
['#', ' Redirect non-https traffic to https'],
|
||||
['#', ' if ($scheme != "https") {'],
|
||||
['#', " return 301 https://$host$request_uri;"],
|
||||
['#', " } # managed by Certbot"],
|
||||
]
|
||||
|
||||
@zope.interface.implementer(interfaces.IAuthenticator, interfaces.IInstaller)
|
||||
@zope.interface.provider(interfaces.IPluginFactory)
|
||||
class NginxConfigurator(common.Installer):
|
||||
@@ -117,6 +98,9 @@ class NginxConfigurator(common.Installer):
|
||||
# Files to save
|
||||
self.save_notes = ""
|
||||
|
||||
# For creating new vhosts if no names match
|
||||
self.new_vhost = None
|
||||
|
||||
# Add number of outstanding challenges
|
||||
self._chall_out = 0
|
||||
|
||||
@@ -191,14 +175,14 @@ class NginxConfigurator(common.Installer):
|
||||
"The nginx plugin currently requires --fullchain-path to "
|
||||
"install a cert.")
|
||||
|
||||
vhost = self.choose_vhost(domain)
|
||||
cert_directives = [['\n', 'ssl_certificate', ' ', fullchain_path],
|
||||
['\n', 'ssl_certificate_key', ' ', key_path]]
|
||||
vhost = self.choose_vhost(domain, create_if_no_match=True)
|
||||
cert_directives = [['\n ', 'ssl_certificate', ' ', fullchain_path],
|
||||
['\n ', 'ssl_certificate_key', ' ', key_path]]
|
||||
|
||||
self.parser.add_server_directives(vhost,
|
||||
cert_directives, replace=True)
|
||||
logger.info("Deployed Certificate to VirtualHost %s for %s",
|
||||
vhost.filep, vhost.names)
|
||||
vhost.filep, ", ".join(vhost.names))
|
||||
|
||||
self.save_notes += ("Changed vhost at %s with addresses of %s\n" %
|
||||
(vhost.filep,
|
||||
@@ -209,7 +193,7 @@ class NginxConfigurator(common.Installer):
|
||||
#######################
|
||||
# Vhost parsing methods
|
||||
#######################
|
||||
def choose_vhost(self, target_name):
|
||||
def choose_vhost(self, target_name, create_if_no_match=False):
|
||||
"""Chooses a virtual host based on the given domain name.
|
||||
|
||||
.. note:: This makes the vhost SSL-enabled if it isn't already. Follows
|
||||
@@ -223,6 +207,8 @@ class NginxConfigurator(common.Installer):
|
||||
hostname. Currently we just ignore this.
|
||||
|
||||
:param str target_name: domain name
|
||||
:param bool create_if_no_match: If we should create a new vhost from default
|
||||
when there is no match found
|
||||
|
||||
:returns: ssl vhost associated with name
|
||||
:rtype: :class:`~certbot_nginx.obj.VirtualHost`
|
||||
@@ -233,20 +219,81 @@ class NginxConfigurator(common.Installer):
|
||||
matches = self._get_ranked_matches(target_name)
|
||||
vhost = self._select_best_name_match(matches)
|
||||
if not vhost:
|
||||
# No matches. Raise a misconfiguration error.
|
||||
raise errors.MisconfigurationError(
|
||||
("Cannot find a VirtualHost matching domain %s. "
|
||||
"In order for Certbot to correctly perform the challenge "
|
||||
"please add a corresponding server_name directive to your "
|
||||
"nginx configuration: "
|
||||
"https://nginx.org/en/docs/http/server_names.html") % (target_name))
|
||||
else:
|
||||
# Note: if we are enhancing with ocsp, vhost should already be ssl.
|
||||
if not vhost.ssl:
|
||||
self._make_server_ssl(vhost)
|
||||
if create_if_no_match:
|
||||
vhost = self._vhost_from_duplicated_default(target_name)
|
||||
else:
|
||||
# No matches. Raise a misconfiguration error.
|
||||
raise errors.MisconfigurationError(
|
||||
("Cannot find a VirtualHost matching domain %s. "
|
||||
"In order for Certbot to correctly perform the challenge "
|
||||
"please add a corresponding server_name directive to your "
|
||||
"nginx configuration: "
|
||||
"https://nginx.org/en/docs/http/server_names.html") % (target_name))
|
||||
# Note: if we are enhancing with ocsp, vhost should already be ssl.
|
||||
if not vhost.ssl:
|
||||
self._make_server_ssl(vhost)
|
||||
|
||||
return vhost
|
||||
|
||||
def ipv6_info(self, port):
|
||||
"""Returns tuple of booleans (ipv6_active, ipv6only_present)
|
||||
ipv6_active is true if any server block listens ipv6 address in any port
|
||||
|
||||
ipv6only_present is true if ipv6only=on option exists in any server
|
||||
block ipv6 listen directive for the specified port.
|
||||
|
||||
:param str port: Port to check ipv6only=on directive for
|
||||
|
||||
:returns: Tuple containing information if IPv6 is enabled in the global
|
||||
configuration, and existence of ipv6only directive for specified port
|
||||
:rtype: tuple of type (bool, bool)
|
||||
"""
|
||||
vhosts = self.parser.get_vhosts()
|
||||
ipv6_active = False
|
||||
ipv6only_present = False
|
||||
for vh in vhosts:
|
||||
for addr in vh.addrs:
|
||||
if addr.ipv6:
|
||||
ipv6_active = True
|
||||
if addr.ipv6only and addr.get_port() == port:
|
||||
ipv6only_present = True
|
||||
return (ipv6_active, ipv6only_present)
|
||||
|
||||
def _vhost_from_duplicated_default(self, domain):
|
||||
if self.new_vhost is None:
|
||||
default_vhost = self._get_default_vhost()
|
||||
self.new_vhost = self.parser.duplicate_vhost(default_vhost, delete_default=True)
|
||||
self.new_vhost.names = set()
|
||||
|
||||
self._add_server_name_to_vhost(self.new_vhost, domain)
|
||||
return self.new_vhost
|
||||
|
||||
def _add_server_name_to_vhost(self, vhost, domain):
|
||||
vhost.names.add(domain)
|
||||
name_block = [['\n ', 'server_name']]
|
||||
for name in vhost.names:
|
||||
name_block[0].append(' ')
|
||||
name_block[0].append(name)
|
||||
self.parser.add_server_directives(vhost, name_block, replace=True)
|
||||
|
||||
def _get_default_vhost(self):
|
||||
vhost_list = self.parser.get_vhosts()
|
||||
# if one has default_server set, return that one
|
||||
default_vhosts = []
|
||||
for vhost in vhost_list:
|
||||
for addr in vhost.addrs:
|
||||
if addr.default:
|
||||
default_vhosts.append(vhost)
|
||||
break
|
||||
|
||||
if len(default_vhosts) == 1:
|
||||
return default_vhosts[0]
|
||||
|
||||
# TODO: present a list of vhosts for user to choose from
|
||||
|
||||
raise errors.MisconfigurationError("Could not automatically find a matching server"
|
||||
" block. Set the `server_name` directive to use the Nginx installer.")
|
||||
|
||||
def _get_ranked_matches(self, target_name):
|
||||
"""Returns a ranked list of vhosts that match target_name.
|
||||
The ranking gives preference to SSL vhosts.
|
||||
@@ -405,9 +452,12 @@ class NginxConfigurator(common.Installer):
|
||||
all_names.add(host)
|
||||
elif not common.private_ips_regex.match(host):
|
||||
# If it isn't a private IP, do a reverse DNS lookup
|
||||
# TODO: IPv6 support
|
||||
try:
|
||||
socket.inet_aton(host)
|
||||
if addr.ipv6:
|
||||
host = addr.get_ipv6_exploded()
|
||||
socket.inet_pton(socket.AF_INET6, host)
|
||||
else:
|
||||
socket.inet_pton(socket.AF_INET, host)
|
||||
all_names.add(socket.gethostbyaddr(host)[0])
|
||||
except (socket.error, socket.herror, socket.timeout):
|
||||
continue
|
||||
@@ -433,26 +483,47 @@ class NginxConfigurator(common.Installer):
|
||||
def _make_server_ssl(self, vhost):
|
||||
"""Make a server SSL.
|
||||
|
||||
Make a server SSL based on server_name and filename by adding a
|
||||
``listen IConfig.tls_sni_01_port ssl`` directive to the server block.
|
||||
|
||||
.. todo:: Maybe this should create a new block instead of modifying
|
||||
the existing one?
|
||||
Make a server SSL by adding new listen and SSL directives.
|
||||
|
||||
:param vhost: The vhost to add SSL to.
|
||||
:type vhost: :class:`~certbot_nginx.obj.VirtualHost`
|
||||
|
||||
"""
|
||||
ipv6info = self.ipv6_info(self.config.tls_sni_01_port)
|
||||
ipv6_block = ['']
|
||||
ipv4_block = ['']
|
||||
|
||||
# If the vhost was implicitly listening on the default Nginx port,
|
||||
# have it continue to do so.
|
||||
if len(vhost.addrs) == 0:
|
||||
listen_block = [['\n ', 'listen', ' ', self.DEFAULT_LISTEN_PORT]]
|
||||
self.parser.add_server_directives(vhost, listen_block, replace=False)
|
||||
|
||||
if vhost.ipv6_enabled():
|
||||
ipv6_block = ['\n ',
|
||||
'listen',
|
||||
' ',
|
||||
'[::]:{0}'.format(self.config.tls_sni_01_port),
|
||||
' ',
|
||||
'ssl']
|
||||
if not ipv6info[1]:
|
||||
# ipv6only=on is absent in global config
|
||||
ipv6_block.append(' ')
|
||||
ipv6_block.append('ipv6only=on')
|
||||
|
||||
if vhost.ipv4_enabled():
|
||||
ipv4_block = ['\n ',
|
||||
'listen',
|
||||
' ',
|
||||
'{0}'.format(self.config.tls_sni_01_port),
|
||||
' ',
|
||||
'ssl']
|
||||
|
||||
snakeoil_cert, snakeoil_key = self._get_snakeoil_paths()
|
||||
|
||||
ssl_block = ([
|
||||
['\n ', 'listen', ' ', '{0} ssl'.format(self.config.tls_sni_01_port)],
|
||||
ipv6_block,
|
||||
ipv4_block,
|
||||
['\n ', 'ssl_certificate', ' ', snakeoil_cert],
|
||||
['\n ', 'ssl_certificate_key', ' ', snakeoil_key],
|
||||
['\n ', 'include', ' ', self.mod_ssl_conf],
|
||||
@@ -490,10 +561,12 @@ class NginxConfigurator(common.Installer):
|
||||
raise
|
||||
|
||||
def _has_certbot_redirect(self, vhost):
|
||||
return vhost.contains_list(TEST_REDIRECT_BLOCK)
|
||||
test_redirect_block = _test_block_from_block(REDIRECT_BLOCK)
|
||||
return vhost.contains_list(test_redirect_block)
|
||||
|
||||
def _has_certbot_redirect_comment(self, vhost):
|
||||
return vhost.contains_list(TEST_REDIRECT_COMMENT_BLOCK)
|
||||
test_redirect_comment_block = _test_block_from_block(REDIRECT_COMMENT_BLOCK)
|
||||
return vhost.contains_list(test_redirect_comment_block)
|
||||
|
||||
def _add_redirect_block(self, vhost, active=True):
|
||||
"""Add redirect directive to vhost
|
||||
@@ -509,7 +582,8 @@ class NginxConfigurator(common.Installer):
|
||||
def _enable_redirect(self, domain, unused_options):
|
||||
"""Redirect all equivalent HTTP traffic to ssl_vhost.
|
||||
|
||||
Add rewrite directive to non https traffic
|
||||
If the vhost is listening plaintextishly, separate out the
|
||||
relevant directives into a new server block and add a rewrite directive.
|
||||
|
||||
.. note:: This function saves the configuration
|
||||
|
||||
@@ -522,26 +596,46 @@ class NginxConfigurator(common.Installer):
|
||||
vhost = None
|
||||
# If there are blocks listening plaintextishly on self.DEFAULT_LISTEN_PORT,
|
||||
# choose the most name-matching one.
|
||||
|
||||
vhost = self.choose_redirect_vhost(domain, port)
|
||||
|
||||
if vhost is None:
|
||||
logger.info("No matching insecure server blocks listening on port %s found.",
|
||||
self.DEFAULT_LISTEN_PORT)
|
||||
return
|
||||
|
||||
if vhost.ssl:
|
||||
new_vhost = self.parser.duplicate_vhost(vhost,
|
||||
only_directives=['listen', 'server_name'])
|
||||
|
||||
def _ssl_match_func(directive):
|
||||
return 'ssl' in directive
|
||||
|
||||
def _no_ssl_match_func(directive):
|
||||
return 'ssl' not in directive
|
||||
|
||||
# remove all ssl addresses from the new block
|
||||
self.parser.remove_server_directives(new_vhost, 'listen', match_func=_ssl_match_func)
|
||||
|
||||
# remove all non-ssl addresses from the existing block
|
||||
self.parser.remove_server_directives(vhost, 'listen', match_func=_no_ssl_match_func)
|
||||
|
||||
vhost = new_vhost
|
||||
|
||||
if self._has_certbot_redirect(vhost):
|
||||
logger.info("Traffic on port %s already redirecting to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
elif vhost.has_redirect():
|
||||
if not self._has_certbot_redirect_comment(vhost):
|
||||
self._add_redirect_block(vhost, active=False)
|
||||
logger.info("The appropriate server block is already redirecting "
|
||||
"traffic. To enable redirect anyway, uncomment the "
|
||||
"redirect lines in %s.", vhost.filep)
|
||||
else:
|
||||
if self._has_certbot_redirect(vhost):
|
||||
logger.info("Traffic on port %s already redirecting to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
elif vhost.has_redirect():
|
||||
if not self._has_certbot_redirect_comment(vhost):
|
||||
self._add_redirect_block(vhost, active=False)
|
||||
logger.info("The appropriate server block is already redirecting "
|
||||
"traffic. To enable redirect anyway, uncomment the "
|
||||
"redirect lines in %s.", vhost.filep)
|
||||
else:
|
||||
# Redirect plaintextish host to https
|
||||
self._add_redirect_block(vhost, active=True)
|
||||
logger.info("Redirecting all traffic on port %s to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
# Redirect plaintextish host to https
|
||||
self._add_redirect_block(vhost, active=True)
|
||||
logger.info("Redirecting all traffic on port %s to ssl in %s",
|
||||
self.DEFAULT_LISTEN_PORT, vhost.filep)
|
||||
|
||||
def _enable_ocsp_stapling(self, domain, chain_path):
|
||||
"""Include OCSP response in TLS handshake
|
||||
@@ -715,6 +809,7 @@ class NginxConfigurator(common.Installer):
|
||||
|
||||
"""
|
||||
super(NginxConfigurator, self).recovery_routine()
|
||||
self.new_vhost = None
|
||||
self.parser.load()
|
||||
|
||||
def revert_challenge_config(self):
|
||||
@@ -724,6 +819,7 @@ class NginxConfigurator(common.Installer):
|
||||
|
||||
"""
|
||||
self.revert_temporary_config()
|
||||
self.new_vhost = None
|
||||
self.parser.load()
|
||||
|
||||
def rollback_checkpoints(self, rollback=1):
|
||||
@@ -736,6 +832,7 @@ class NginxConfigurator(common.Installer):
|
||||
|
||||
"""
|
||||
super(NginxConfigurator, self).rollback_checkpoints(rollback)
|
||||
self.new_vhost = None
|
||||
self.parser.load()
|
||||
|
||||
###########################################################################
|
||||
@@ -788,6 +885,11 @@ class NginxConfigurator(common.Installer):
|
||||
self.restart()
|
||||
|
||||
|
||||
def _test_block_from_block(block):
|
||||
test_block = nginxparser.UnspacedList(block)
|
||||
parser.comment_directive(test_block, 0)
|
||||
return test_block[:-1]
|
||||
|
||||
def nginx_restart(nginx_ctl, nginx_conf):
|
||||
"""Restarts the Nginx Server.
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ from pyparsing import (
|
||||
Literal, White, Forward, Group, Optional, OneOrMore, QuotedString, Regex, ZeroOrMore, Combine)
|
||||
from pyparsing import stringEnd
|
||||
from pyparsing import restOfLine
|
||||
import six
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -71,7 +72,7 @@ class RawNginxDumper(object):
|
||||
"""Iterates the dumped nginx content."""
|
||||
blocks = blocks or self.blocks
|
||||
for b0 in blocks:
|
||||
if isinstance(b0, str):
|
||||
if isinstance(b0, six.string_types):
|
||||
yield b0
|
||||
continue
|
||||
item = copy.deepcopy(b0)
|
||||
@@ -88,7 +89,7 @@ class RawNginxDumper(object):
|
||||
yield '}'
|
||||
else: # not a block - list of strings
|
||||
semicolon = ";"
|
||||
if isinstance(item[0], str) and item[0].strip() == '#': # comment
|
||||
if isinstance(item[0], six.string_types) and item[0].strip() == '#': # comment
|
||||
semicolon = ""
|
||||
yield "".join(item) + semicolon
|
||||
|
||||
@@ -145,7 +146,7 @@ def dump(blocks, _file):
|
||||
return _file.write(dumps(blocks))
|
||||
|
||||
|
||||
spacey = lambda x: (isinstance(x, str) and x.isspace()) or x == ''
|
||||
spacey = lambda x: (isinstance(x, six.string_types) and x.isspace()) or x == ''
|
||||
|
||||
class UnspacedList(list):
|
||||
"""Wrap a list [of lists], making any whitespace entries magically invisible"""
|
||||
@@ -189,13 +190,15 @@ class UnspacedList(list):
|
||||
item, spaced_item = self._coerce(x)
|
||||
slicepos = self._spaced_position(i) if i < len(self) else len(self.spaced)
|
||||
self.spaced.insert(slicepos, spaced_item)
|
||||
list.insert(self, i, item)
|
||||
if not spacey(item):
|
||||
list.insert(self, i, item)
|
||||
self.dirty = True
|
||||
|
||||
def append(self, x):
|
||||
item, spaced_item = self._coerce(x)
|
||||
self.spaced.append(spaced_item)
|
||||
list.append(self, item)
|
||||
if not spacey(item):
|
||||
list.append(self, item)
|
||||
self.dirty = True
|
||||
|
||||
def extend(self, x):
|
||||
@@ -226,7 +229,8 @@ class UnspacedList(list):
|
||||
raise NotImplementedError("Slice operations on UnspacedLists not yet implemented")
|
||||
item, spaced_item = self._coerce(value)
|
||||
self.spaced.__setitem__(self._spaced_position(i), spaced_item)
|
||||
list.__setitem__(self, i, item)
|
||||
if not spacey(item):
|
||||
list.__setitem__(self, i, item)
|
||||
self.dirty = True
|
||||
|
||||
def __delitem__(self, i):
|
||||
@@ -235,8 +239,8 @@ class UnspacedList(list):
|
||||
self.dirty = True
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
l = UnspacedList(self[:])
|
||||
l.spaced = copy.deepcopy(self.spaced, memo=memo)
|
||||
new_spaced = copy.deepcopy(self.spaced, memo=memo)
|
||||
l = UnspacedList(new_spaced)
|
||||
l.dirty = self.dirty
|
||||
return l
|
||||
|
||||
|
||||
@@ -34,10 +34,13 @@ class Addr(common.Addr):
|
||||
UNSPECIFIED_IPV4_ADDRESSES = ('', '*', '0.0.0.0')
|
||||
CANONICAL_UNSPECIFIED_ADDRESS = UNSPECIFIED_IPV4_ADDRESSES[0]
|
||||
|
||||
def __init__(self, host, port, ssl, default):
|
||||
def __init__(self, host, port, ssl, default, ipv6, ipv6only):
|
||||
# pylint: disable=too-many-arguments
|
||||
super(Addr, self).__init__((host, port))
|
||||
self.ssl = ssl
|
||||
self.default = default
|
||||
self.ipv6 = ipv6
|
||||
self.ipv6only = ipv6only
|
||||
self.unspecified_address = host in self.UNSPECIFIED_IPV4_ADDRESSES
|
||||
|
||||
@classmethod
|
||||
@@ -46,6 +49,8 @@ class Addr(common.Addr):
|
||||
parts = str_addr.split(' ')
|
||||
ssl = False
|
||||
default = False
|
||||
ipv6 = False
|
||||
ipv6only = False
|
||||
host = ''
|
||||
port = ''
|
||||
|
||||
@@ -56,15 +61,25 @@ class Addr(common.Addr):
|
||||
if addr.startswith('unix:'):
|
||||
return None
|
||||
|
||||
tup = addr.partition(':')
|
||||
if re.match(r'^\d+$', tup[0]):
|
||||
# This is a bare port, not a hostname. E.g. listen 80
|
||||
host = ''
|
||||
port = tup[0]
|
||||
# IPv6 check
|
||||
ipv6_match = re.match(r'\[.*\]', addr)
|
||||
if ipv6_match:
|
||||
ipv6 = True
|
||||
# IPv6 handling
|
||||
host = ipv6_match.group()
|
||||
# The rest of the addr string will be the port, if any
|
||||
port = addr[ipv6_match.end()+1:]
|
||||
else:
|
||||
# This is a host-port tuple. E.g. listen 127.0.0.1:*
|
||||
host = tup[0]
|
||||
port = tup[2]
|
||||
# IPv4 handling
|
||||
tup = addr.partition(':')
|
||||
if re.match(r'^\d+$', tup[0]):
|
||||
# This is a bare port, not a hostname. E.g. listen 80
|
||||
host = ''
|
||||
port = tup[0]
|
||||
else:
|
||||
# This is a host-port tuple. E.g. listen 127.0.0.1:*
|
||||
host = tup[0]
|
||||
port = tup[2]
|
||||
|
||||
# The rest of the parts are options; we only care about ssl and default
|
||||
while len(parts) > 0:
|
||||
@@ -73,8 +88,10 @@ class Addr(common.Addr):
|
||||
ssl = True
|
||||
elif nextpart == 'default_server':
|
||||
default = True
|
||||
elif nextpart == "ipv6only=on":
|
||||
ipv6only = True
|
||||
|
||||
return cls(host, port, ssl, default)
|
||||
return cls(host, port, ssl, default, ipv6, ipv6only)
|
||||
|
||||
def to_string(self, include_default=True):
|
||||
"""Return string representation of Addr"""
|
||||
@@ -114,8 +131,6 @@ class Addr(common.Addr):
|
||||
self.tup[1]), self.ipv6) == \
|
||||
common.Addr((other.CANONICAL_UNSPECIFIED_ADDRESS,
|
||||
other.tup[1]), other.ipv6)
|
||||
# Nginx plugin currently doesn't support IPv6 but this will
|
||||
# future-proof it
|
||||
return super(Addr, self).__eq__(other)
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -190,15 +205,31 @@ class VirtualHost(object): # pylint: disable=too-few-public-methods
|
||||
def contains_list(self, test):
|
||||
"""Determine if raw server block contains test list at top level
|
||||
"""
|
||||
for i in six.moves.range(0, len(self.raw) - len(test)):
|
||||
for i in six.moves.range(0, len(self.raw) - len(test) + 1):
|
||||
if self.raw[i:i + len(test)] == test:
|
||||
return True
|
||||
return False
|
||||
|
||||
def ipv6_enabled(self):
|
||||
"""Return true if one or more of the listen directives in vhost supports
|
||||
IPv6"""
|
||||
for a in self.addrs:
|
||||
if a.ipv6:
|
||||
return True
|
||||
|
||||
def ipv4_enabled(self):
|
||||
"""Return true if one or more of the listen directives in vhost are IPv4
|
||||
only"""
|
||||
if self.addrs is None or len(self.addrs) == 0:
|
||||
return True
|
||||
for a in self.addrs:
|
||||
if not a.ipv6:
|
||||
return True
|
||||
|
||||
def _find_directive(directives, directive_name):
|
||||
"""Find a directive of type directive_name in directives
|
||||
"""
|
||||
if not directives or isinstance(directives, str) or len(directives) == 0:
|
||||
if not directives or isinstance(directives, six.string_types) or len(directives) == 0:
|
||||
return None
|
||||
|
||||
if directives[0] == directive_name:
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"""NginxParser is a member object of the NginxConfigurator class."""
|
||||
import copy
|
||||
import functools
|
||||
import glob
|
||||
import logging
|
||||
import os
|
||||
import pyparsing
|
||||
import re
|
||||
|
||||
import six
|
||||
|
||||
from certbot import errors
|
||||
|
||||
from certbot_nginx import obj
|
||||
@@ -292,6 +295,30 @@ class NginxParser(object):
|
||||
:param bool replace: Whether to only replace existing directives
|
||||
|
||||
"""
|
||||
self._modify_server_directives(vhost,
|
||||
functools.partial(_add_directives, directives, replace))
|
||||
|
||||
def remove_server_directives(self, vhost, directive_name, match_func=None):
|
||||
"""Remove all directives of type directive_name.
|
||||
|
||||
:param :class:`~certbot_nginx.obj.VirtualHost` vhost: The vhost
|
||||
to remove directives from
|
||||
:param string directive_name: The directive type to remove
|
||||
:param callable match_func: Function of the directive that returns true for directives
|
||||
to be deleted.
|
||||
"""
|
||||
self._modify_server_directives(vhost,
|
||||
functools.partial(_remove_directives, directive_name, match_func))
|
||||
|
||||
def _update_vhost_based_on_new_directives(self, vhost, directives_list):
|
||||
new_server = self._get_included_directives(directives_list)
|
||||
parsed_server = self.parse_server(new_server)
|
||||
vhost.addrs = parsed_server['addrs']
|
||||
vhost.ssl = parsed_server['ssl']
|
||||
vhost.names = parsed_server['names']
|
||||
vhost.raw = new_server
|
||||
|
||||
def _modify_server_directives(self, vhost, block_func):
|
||||
filename = vhost.filep
|
||||
try:
|
||||
result = self.parsed[filename]
|
||||
@@ -300,18 +327,54 @@ class NginxParser(object):
|
||||
if not isinstance(result, list) or len(result) != 2:
|
||||
raise errors.MisconfigurationError("Not a server block.")
|
||||
result = result[1]
|
||||
_add_directives(result, directives, replace)
|
||||
block_func(result)
|
||||
|
||||
# update vhost based on new directives
|
||||
new_server = self._get_included_directives(result)
|
||||
parsed_server = self.parse_server(new_server)
|
||||
vhost.addrs = parsed_server['addrs']
|
||||
vhost.ssl = parsed_server['ssl']
|
||||
vhost.names = parsed_server['names']
|
||||
vhost.raw = new_server
|
||||
self._update_vhost_based_on_new_directives(vhost, result)
|
||||
except errors.MisconfigurationError as err:
|
||||
raise errors.MisconfigurationError("Problem in %s: %s" % (filename, str(err)))
|
||||
|
||||
def duplicate_vhost(self, vhost_template, delete_default=False, only_directives=None):
|
||||
"""Duplicate the vhost in the configuration files.
|
||||
|
||||
:param :class:`~certbot_nginx.obj.VirtualHost` vhost_template: The vhost
|
||||
whose information we copy
|
||||
:param bool delete_default: If we should remove default_server
|
||||
from listen directives in the block.
|
||||
:param list only_directives: If it exists, only duplicate the named directives. Only
|
||||
looks at first level of depth; does not expand includes.
|
||||
|
||||
:returns: A vhost object for the newly created vhost
|
||||
:rtype: :class:`~certbot_nginx.obj.VirtualHost`
|
||||
"""
|
||||
# TODO: https://github.com/certbot/certbot/issues/5185
|
||||
# put it in the same file as the template, at the same level
|
||||
new_vhost = copy.deepcopy(vhost_template)
|
||||
|
||||
enclosing_block = self.parsed[vhost_template.filep]
|
||||
for index in vhost_template.path[:-1]:
|
||||
enclosing_block = enclosing_block[index]
|
||||
raw_in_parsed = copy.deepcopy(enclosing_block[vhost_template.path[-1]])
|
||||
|
||||
if only_directives is not None:
|
||||
new_directives = nginxparser.UnspacedList([])
|
||||
for directive in raw_in_parsed[1]:
|
||||
if len(directive) > 0 and directive[0] in only_directives:
|
||||
new_directives.append(directive)
|
||||
raw_in_parsed[1] = new_directives
|
||||
|
||||
self._update_vhost_based_on_new_directives(new_vhost, new_directives)
|
||||
|
||||
enclosing_block.append(raw_in_parsed)
|
||||
new_vhost.path[-1] = len(enclosing_block) - 1
|
||||
if delete_default:
|
||||
for addr in new_vhost.addrs:
|
||||
addr.default = False
|
||||
for directive in enclosing_block[new_vhost.path[-1]][1]:
|
||||
if (len(directive) > 0 and directive[0] == 'listen'
|
||||
and 'default_server' in directive):
|
||||
del directive[directive.index('default_server')]
|
||||
return new_vhost
|
||||
|
||||
def _parse_ssl_options(ssl_options):
|
||||
if ssl_options is not None:
|
||||
try:
|
||||
@@ -444,7 +507,7 @@ def _is_include_directive(entry):
|
||||
"""
|
||||
return (isinstance(entry, list) and
|
||||
len(entry) == 2 and entry[0] == 'include' and
|
||||
isinstance(entry[1], str))
|
||||
isinstance(entry[1], six.string_types))
|
||||
|
||||
def _is_ssl_on_directive(entry):
|
||||
"""Checks if an nginx parsed entry is an 'ssl on' directive.
|
||||
@@ -458,7 +521,7 @@ def _is_ssl_on_directive(entry):
|
||||
len(entry) == 2 and entry[0] == 'ssl' and
|
||||
entry[1] == 'on')
|
||||
|
||||
def _add_directives(block, directives, replace):
|
||||
def _add_directives(directives, replace, block):
|
||||
"""Adds or replaces directives in a config block.
|
||||
|
||||
When replace=False, it's an error to try and add a directive that already
|
||||
@@ -470,8 +533,9 @@ def _add_directives(block, directives, replace):
|
||||
|
||||
..todo :: Find directives that are in included files.
|
||||
|
||||
:param list block: The block to replace in
|
||||
:param list directives: The new directives.
|
||||
:param bool replace: Described above.
|
||||
:param list block: The block to replace in
|
||||
|
||||
"""
|
||||
for directive in directives:
|
||||
@@ -485,8 +549,12 @@ REPEATABLE_DIRECTIVES = set(['server_name', 'listen', INCLUDE])
|
||||
COMMENT = ' managed by Certbot'
|
||||
COMMENT_BLOCK = [' ', '#', COMMENT]
|
||||
|
||||
def _comment_directive(block, location):
|
||||
"""Add a comment to the end of the line at location."""
|
||||
def comment_directive(block, location):
|
||||
"""Add a ``#managed by Certbot`` comment to the end of the line at location.
|
||||
|
||||
:param list block: The block containing the directive to be commented
|
||||
:param int location: The location within ``block`` of the directive to be commented
|
||||
"""
|
||||
next_entry = block[location + 1] if location + 1 < len(block) else None
|
||||
if isinstance(next_entry, list) and next_entry:
|
||||
if len(next_entry) >= 2 and next_entry[-2] == "#" and COMMENT in next_entry[-1]:
|
||||
@@ -523,6 +591,12 @@ def _comment_out_directive(block, location, include_location):
|
||||
|
||||
block[location] = new_dir[0] # set the now-single-line-comment directive back in place
|
||||
|
||||
def _find_location(block, directive_name, match_func=None):
|
||||
"""Finds the index of the first instance of directive_name in block.
|
||||
If no line exists, use None."""
|
||||
return next((index for index, line in enumerate(block) \
|
||||
if line and line[0] == directive_name and (match_func is None or match_func(line))), None)
|
||||
|
||||
def _add_directive(block, directive, replace):
|
||||
"""Adds or replaces a single directive in a config block.
|
||||
|
||||
@@ -538,19 +612,12 @@ def _add_directive(block, directive, replace):
|
||||
block.append(directive)
|
||||
return
|
||||
|
||||
def find_location(direc):
|
||||
""" Find the index of a config line where the name of the directive matches
|
||||
the name of the directive we want to add. If no line exists, use None.
|
||||
"""
|
||||
return next((index for index, line in enumerate(block) \
|
||||
if line and line[0] == direc[0]), None)
|
||||
|
||||
location = find_location(directive)
|
||||
location = _find_location(block, directive[0])
|
||||
|
||||
if replace:
|
||||
if location is not None:
|
||||
block[location] = directive
|
||||
_comment_directive(block, location)
|
||||
comment_directive(block, location)
|
||||
return
|
||||
# Append directive. Fail if the name is not a repeatable directive name,
|
||||
# and there is already a copy of that directive with a different value
|
||||
@@ -561,7 +628,8 @@ def _add_directive(block, directive, replace):
|
||||
directive_name = directive[0]
|
||||
def can_append(loc, dir_name):
|
||||
""" Can we append this directive to the block? """
|
||||
return loc is None or (isinstance(dir_name, str) and dir_name in REPEATABLE_DIRECTIVES)
|
||||
return loc is None or (isinstance(dir_name, six.string_types)
|
||||
and dir_name in REPEATABLE_DIRECTIVES)
|
||||
|
||||
err_fmt = 'tried to insert directive "{0}" but found conflicting "{1}".'
|
||||
|
||||
@@ -573,7 +641,7 @@ def _add_directive(block, directive, replace):
|
||||
included_directives = _parse_ssl_options(directive[1])
|
||||
|
||||
for included_directive in included_directives:
|
||||
included_dir_loc = find_location(included_directive)
|
||||
included_dir_loc = _find_location(block, included_directive[0])
|
||||
included_dir_name = included_directive[0]
|
||||
if not is_whitespace_or_comment(included_directive) \
|
||||
and not can_append(included_dir_loc, included_dir_name):
|
||||
@@ -585,10 +653,19 @@ def _add_directive(block, directive, replace):
|
||||
|
||||
if can_append(location, directive_name):
|
||||
block.append(directive)
|
||||
_comment_directive(block, len(block) - 1)
|
||||
comment_directive(block, len(block) - 1)
|
||||
elif block[location] != directive:
|
||||
raise errors.MisconfigurationError(err_fmt.format(directive, block[location]))
|
||||
|
||||
def _remove_directives(directive_name, match_func, block):
|
||||
"""Removes directives of name directive_name from a config block if match_func matches.
|
||||
"""
|
||||
while True:
|
||||
location = _find_location(block, directive_name, match_func=match_func)
|
||||
if location is None:
|
||||
return
|
||||
del block[location]
|
||||
|
||||
def _apply_global_addr_ssl(addr_to_ssl, parsed_server):
|
||||
"""Apply global sslishness information to the parsed server block
|
||||
"""
|
||||
|
||||
@@ -45,7 +45,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
|
||||
def test_prepare(self):
|
||||
self.assertEqual((1, 6, 2), self.config.version)
|
||||
self.assertEqual(8, len(self.config.parser.parsed))
|
||||
self.assertEqual(10, len(self.config.parser.parsed))
|
||||
|
||||
@mock.patch("certbot_nginx.configurator.util.exe_exists")
|
||||
@mock.patch("certbot_nginx.configurator.subprocess.Popen")
|
||||
@@ -89,7 +89,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
self.assertEqual(names, set(
|
||||
["155.225.50.69.nephoscale.net", "www.example.org", "another.alias",
|
||||
"migration.com", "summer.com", "geese.com", "sslon.com",
|
||||
"globalssl.com", "globalsslsetssl.com"]))
|
||||
"globalssl.com", "globalsslsetssl.com", "ipv6.com", "ipv6ssl.com"]))
|
||||
|
||||
def test_supported_enhancements(self):
|
||||
self.assertEqual(['redirect', 'staple-ocsp'],
|
||||
@@ -131,6 +131,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
server_conf = set(['somename', 'another.alias', 'alias'])
|
||||
example_conf = set(['.example.com', 'example.*'])
|
||||
foo_conf = set(['*.www.foo.com', '*.www.example.com'])
|
||||
ipv6_conf = set(['ipv6.com'])
|
||||
|
||||
results = {'localhost': localhost_conf,
|
||||
'alias': server_conf,
|
||||
@@ -139,7 +140,8 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
'www.example.com': example_conf,
|
||||
'test.www.example.com': foo_conf,
|
||||
'abc.www.foo.com': foo_conf,
|
||||
'www.bar.co.uk': localhost_conf}
|
||||
'www.bar.co.uk': localhost_conf,
|
||||
'ipv6.com': ipv6_conf}
|
||||
|
||||
conf_path = {'localhost': "etc_nginx/nginx.conf",
|
||||
'alias': "etc_nginx/nginx.conf",
|
||||
@@ -148,7 +150,8 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
'www.example.com': "etc_nginx/sites-enabled/example.com",
|
||||
'test.www.example.com': "etc_nginx/foo.conf",
|
||||
'abc.www.foo.com': "etc_nginx/foo.conf",
|
||||
'www.bar.co.uk': "etc_nginx/nginx.conf"}
|
||||
'www.bar.co.uk': "etc_nginx/nginx.conf",
|
||||
'ipv6.com': "etc_nginx/sites-enabled/ipv6.com"}
|
||||
|
||||
bad_results = ['www.foo.com', 'example', 't.www.bar.co',
|
||||
'69.255.225.155']
|
||||
@@ -159,11 +162,24 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
|
||||
self.assertEqual(results[name], vhost.names)
|
||||
self.assertEqual(conf_path[name], path)
|
||||
# IPv6 specific checks
|
||||
if name == "ipv6.com":
|
||||
self.assertTrue(vhost.ipv6_enabled())
|
||||
# Make sure that we have SSL enabled also for IPv6 addr
|
||||
self.assertTrue(
|
||||
any([True for x in vhost.addrs if x.ssl and x.ipv6]))
|
||||
|
||||
for name in bad_results:
|
||||
self.assertRaises(errors.MisconfigurationError,
|
||||
self.config.choose_vhost, name)
|
||||
|
||||
def test_ipv6only(self):
|
||||
# ipv6_info: (ipv6_active, ipv6only_present)
|
||||
self.assertEquals((True, False), self.config.ipv6_info("80"))
|
||||
# Port 443 has ipv6only=on because of ipv6ssl.com vhost
|
||||
self.assertEquals((True, True), self.config.ipv6_info("443"))
|
||||
|
||||
|
||||
def test_more_info(self):
|
||||
self.assertTrue('nginx.conf' in self.config.more_info())
|
||||
|
||||
@@ -427,10 +443,7 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
def test_redirect_enhance(self):
|
||||
# Test that we successfully add a redirect when there is
|
||||
# a listen directive
|
||||
expected = [
|
||||
['if', '($scheme', '!=', '"https")'],
|
||||
[['return', '301', 'https://$host$request_uri']]
|
||||
]
|
||||
expected = ['return', '301', 'https://$host$request_uri']
|
||||
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
self.config.enhance("www.example.com", "redirect")
|
||||
@@ -446,6 +459,35 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
generated_conf = self.config.parser.parsed[migration_conf]
|
||||
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
|
||||
|
||||
def test_split_for_redirect(self):
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
self.config.deploy_cert(
|
||||
"example.org",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
self.config.enhance("www.example.com", "redirect")
|
||||
generated_conf = self.config.parser.parsed[example_conf]
|
||||
self.assertEqual(
|
||||
[[['server'], [
|
||||
['server_name', '.example.com'],
|
||||
['server_name', 'example.*'], [],
|
||||
['listen', '5001', 'ssl'], ['#', ' managed by Certbot'],
|
||||
['ssl_certificate', 'example/fullchain.pem'], ['#', ' managed by Certbot'],
|
||||
['ssl_certificate_key', 'example/key.pem'], ['#', ' managed by Certbot'],
|
||||
['include', self.config.mod_ssl_conf], ['#', ' managed by Certbot'],
|
||||
['ssl_dhparam', self.config.ssl_dhparams], ['#', ' managed by Certbot'],
|
||||
[], []]],
|
||||
[['server'], [
|
||||
['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
['server_name', '.example.com'],
|
||||
['server_name', 'example.*'],
|
||||
['return', '301', 'https://$host$request_uri'], ['#', ' managed by Certbot'],
|
||||
[], []]]],
|
||||
generated_conf)
|
||||
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
|
||||
def test_certbot_redirect_exists(self, mock_has_redirect, mock_contains_list):
|
||||
@@ -478,9 +520,38 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
generated_conf = self.config.parser.parsed[example_conf]
|
||||
expected = [
|
||||
['#', ' Redirect non-https traffic to https'],
|
||||
['#', ' if ($scheme != "https") {'],
|
||||
['#', ' return 301 https://$host$request_uri;'],
|
||||
['#', ' } # managed by Certbot']
|
||||
['#', ' return 301 https://$host$request_uri;'],
|
||||
]
|
||||
for line in expected:
|
||||
self.assertTrue(util.contains_at_depth(generated_conf, line, 2))
|
||||
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.contains_list')
|
||||
@mock.patch('certbot_nginx.obj.VirtualHost.has_redirect')
|
||||
def test_non_certbot_redirect_exists_has_ssl_copy(self, mock_has_redirect, mock_contains_list):
|
||||
# Test that we add a redirect as a comment if there is already a
|
||||
# redirect-class statement in the block that isn't managed by certbot
|
||||
example_conf = self.config.parser.abs_path('sites-enabled/example.com')
|
||||
|
||||
self.config.deploy_cert(
|
||||
"example.org",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
|
||||
# Has a non-Certbot redirect, and has no existing comment
|
||||
mock_contains_list.return_value = False
|
||||
mock_has_redirect.return_value = True
|
||||
with mock.patch("certbot_nginx.configurator.logger") as mock_logger:
|
||||
self.config.enhance("www.example.com", "redirect")
|
||||
self.assertEqual(mock_logger.info.call_args[0][0],
|
||||
"The appropriate server block is already redirecting "
|
||||
"traffic. To enable redirect anyway, uncomment the "
|
||||
"redirect lines in %s.")
|
||||
generated_conf = self.config.parser.parsed[example_conf]
|
||||
expected = [
|
||||
['#', ' Redirect non-https traffic to https'],
|
||||
['#', ' return 301 https://$host$request_uri;'],
|
||||
]
|
||||
for line in expected:
|
||||
self.assertTrue(util.contains_at_depth(generated_conf, line, 2))
|
||||
@@ -558,6 +629,149 @@ class NginxConfiguratorTest(util.NginxTest):
|
||||
self.assertTrue(util.contains_at_depth(
|
||||
generated_conf, ['ssl_stapling_verify', 'on'], 2))
|
||||
|
||||
def test_deploy_no_match_default_set(self):
|
||||
default_conf = self.config.parser.abs_path('sites-enabled/default')
|
||||
foo_conf = self.config.parser.abs_path('foo.conf')
|
||||
del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server
|
||||
self.config.version = (1, 3, 1)
|
||||
|
||||
self.config.deploy_cert(
|
||||
"www.nomatch.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
|
||||
parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf])
|
||||
|
||||
self.assertEqual([[['server'],
|
||||
[['listen', 'myhost', 'default_server'],
|
||||
['listen', 'otherhost', 'default_server'],
|
||||
['server_name', 'www.example.org'],
|
||||
[['location', '/'],
|
||||
[['root', 'html'],
|
||||
['index', 'index.html', 'index.htm']]]]],
|
||||
[['server'],
|
||||
[['listen', 'myhost'],
|
||||
['listen', 'otherhost'],
|
||||
['server_name', 'www.nomatch.com'],
|
||||
[['location', '/'],
|
||||
[['root', 'html'],
|
||||
['index', 'index.html', 'index.htm']]],
|
||||
['listen', '5001', 'ssl'],
|
||||
['ssl_certificate', 'example/fullchain.pem'],
|
||||
['ssl_certificate_key', 'example/key.pem'],
|
||||
['include', self.config.mod_ssl_conf],
|
||||
['ssl_dhparam', self.config.ssl_dhparams]]]],
|
||||
parsed_default_conf)
|
||||
|
||||
self.config.deploy_cert(
|
||||
"nomatch.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
|
||||
parsed_default_conf = util.filter_comments(self.config.parser.parsed[default_conf])
|
||||
|
||||
self.assertTrue(util.contains_at_depth(parsed_default_conf, "nomatch.com", 3))
|
||||
|
||||
def test_deploy_no_match_default_set_multi_level_path(self):
|
||||
default_conf = self.config.parser.abs_path('sites-enabled/default')
|
||||
foo_conf = self.config.parser.abs_path('foo.conf')
|
||||
del self.config.parser.parsed[default_conf][0][1][0]
|
||||
del self.config.parser.parsed[default_conf][0][1][0]
|
||||
self.config.version = (1, 3, 1)
|
||||
|
||||
self.config.deploy_cert(
|
||||
"www.nomatch.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
|
||||
parsed_foo_conf = util.filter_comments(self.config.parser.parsed[foo_conf])
|
||||
|
||||
self.assertEqual([['server'],
|
||||
[['listen', '*:80', 'ssl'],
|
||||
['server_name', 'www.nomatch.com'],
|
||||
['root', '/home/ubuntu/sites/foo/'],
|
||||
[['location', '/status'], [[['types'], [['image/jpeg', 'jpg']]]]],
|
||||
[['location', '~', 'case_sensitive\\.php$'], [['index', 'index.php'],
|
||||
['root', '/var/root']]],
|
||||
[['location', '~*', 'case_insensitive\\.php$'], []],
|
||||
[['location', '=', 'exact_match\\.php$'], []],
|
||||
[['location', '^~', 'ignore_regex\\.php$'], []],
|
||||
['ssl_certificate', 'example/fullchain.pem'],
|
||||
['ssl_certificate_key', 'example/key.pem']]],
|
||||
parsed_foo_conf[1][1][1])
|
||||
|
||||
def test_deploy_no_match_no_default_set(self):
|
||||
default_conf = self.config.parser.abs_path('sites-enabled/default')
|
||||
foo_conf = self.config.parser.abs_path('foo.conf')
|
||||
del self.config.parser.parsed[default_conf][0][1][0]
|
||||
del self.config.parser.parsed[default_conf][0][1][0]
|
||||
del self.config.parser.parsed[foo_conf][2][1][0][1][0]
|
||||
self.config.version = (1, 3, 1)
|
||||
|
||||
self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert,
|
||||
"www.nomatch.com", "example/cert.pem", "example/key.pem",
|
||||
"example/chain.pem", "example/fullchain.pem")
|
||||
|
||||
def test_deploy_no_match_fail_multiple_defaults(self):
|
||||
self.config.version = (1, 3, 1)
|
||||
self.assertRaises(errors.MisconfigurationError, self.config.deploy_cert,
|
||||
"www.nomatch.com", "example/cert.pem", "example/key.pem",
|
||||
"example/chain.pem", "example/fullchain.pem")
|
||||
|
||||
def test_deploy_no_match_add_redirect(self):
|
||||
default_conf = self.config.parser.abs_path('sites-enabled/default')
|
||||
foo_conf = self.config.parser.abs_path('foo.conf')
|
||||
del self.config.parser.parsed[foo_conf][2][1][0][1][0] # remove default_server
|
||||
self.config.version = (1, 3, 1)
|
||||
|
||||
self.config.deploy_cert(
|
||||
"www.nomatch.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
|
||||
self.config.deploy_cert(
|
||||
"nomatch.com",
|
||||
"example/cert.pem",
|
||||
"example/key.pem",
|
||||
"example/chain.pem",
|
||||
"example/fullchain.pem")
|
||||
|
||||
self.config.enhance("www.nomatch.com", "redirect")
|
||||
|
||||
self.config.save()
|
||||
|
||||
self.config.parser.load()
|
||||
|
||||
expected = ['return', '301', 'https://$host$request_uri']
|
||||
|
||||
generated_conf = self.config.parser.parsed[default_conf]
|
||||
self.assertTrue(util.contains_at_depth(generated_conf, expected, 2))
|
||||
|
||||
@mock.patch('certbot.reverter.logger')
|
||||
@mock.patch('certbot_nginx.parser.NginxParser.load')
|
||||
def test_parser_reload_after_config_changes(self, mock_parser_load, unused_mock_logger):
|
||||
self.config.recovery_routine()
|
||||
self.config.revert_challenge_config()
|
||||
self.config.rollback_checkpoints()
|
||||
self.assertTrue(mock_parser_load.call_count == 3)
|
||||
|
||||
class InstallSslOptionsConfTest(util.NginxTest):
|
||||
"""Test that the options-ssl-nginx.conf file is installed and updated properly."""
|
||||
|
||||
|
||||
@@ -171,8 +171,8 @@ class VirtualHostTest(unittest.TestCase):
|
||||
def test_contains_list(self):
|
||||
from certbot_nginx.obj import VirtualHost
|
||||
from certbot_nginx.obj import Addr
|
||||
from certbot_nginx.configurator import TEST_REDIRECT_BLOCK
|
||||
test_needle = TEST_REDIRECT_BLOCK
|
||||
from certbot_nginx.configurator import REDIRECT_BLOCK, _test_block_from_block
|
||||
test_needle = _test_block_from_block(REDIRECT_BLOCK)
|
||||
test_haystack = [['listen', '80'], ['root', '/var/www/html'],
|
||||
['index', 'index.html index.htm index.nginx-debian.html'],
|
||||
['server_name', 'two.functorkitten.xyz'], ['listen', '443 ssl'],
|
||||
@@ -181,9 +181,7 @@ class VirtualHostTest(unittest.TestCase):
|
||||
['#', ' managed by Certbot'],
|
||||
['ssl_certificate_key', '/etc/letsencrypt/live/two.functorkitten.xyz/privkey.pem'],
|
||||
['#', ' managed by Certbot'],
|
||||
[['if', '($scheme', '!=', '"https")'],
|
||||
[['return', '301', 'https://$host$request_uri']]
|
||||
],
|
||||
['return', '301', 'https://$host$request_uri'],
|
||||
['#', ' managed by Certbot'], []]
|
||||
vhost_haystack = VirtualHost(
|
||||
"filp",
|
||||
|
||||
@@ -50,7 +50,9 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
|
||||
'sites-enabled/example.com',
|
||||
'sites-enabled/migration.com',
|
||||
'sites-enabled/sslon.com',
|
||||
'sites-enabled/globalssl.com']]),
|
||||
'sites-enabled/globalssl.com',
|
||||
'sites-enabled/ipv6.com',
|
||||
'sites-enabled/ipv6ssl.com']]),
|
||||
set(nparser.parsed.keys()))
|
||||
self.assertEqual([['server_name', 'somename', 'alias', 'another.alias']],
|
||||
nparser.parsed[nparser.abs_path('server.conf')])
|
||||
@@ -74,7 +76,7 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
|
||||
parsed = nparser._parse_files(nparser.abs_path(
|
||||
'sites-enabled/example.com.test'))
|
||||
self.assertEqual(3, len(glob.glob(nparser.abs_path('*.test'))))
|
||||
self.assertEqual(5, len(
|
||||
self.assertEqual(7, len(
|
||||
glob.glob(nparser.abs_path('sites-enabled/*.test'))))
|
||||
self.assertEqual([[['server'], [['listen', '69.50.225.155:9000'],
|
||||
['listen', '127.0.0.1'],
|
||||
@@ -110,7 +112,8 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
|
||||
vhosts = nparser.get_vhosts()
|
||||
|
||||
vhost = obj.VirtualHost(nparser.abs_path('sites-enabled/globalssl.com'),
|
||||
[obj.Addr('4.8.2.6', '57', True, False)],
|
||||
[obj.Addr('4.8.2.6', '57', True, False,
|
||||
False, False)],
|
||||
True, True, set(['globalssl.com']), [], [0])
|
||||
|
||||
globalssl_com = [x for x in vhosts if 'globalssl.com' in x.filep][0]
|
||||
@@ -121,34 +124,42 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
|
||||
vhosts = nparser.get_vhosts()
|
||||
|
||||
vhost1 = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
||||
[obj.Addr('', '8080', False, False)],
|
||||
[obj.Addr('', '8080', False, False,
|
||||
False, False)],
|
||||
False, True,
|
||||
set(['localhost',
|
||||
r'~^(www\.)?(example|bar)\.']),
|
||||
[], [10, 1, 9])
|
||||
vhost2 = obj.VirtualHost(nparser.abs_path('nginx.conf'),
|
||||
[obj.Addr('somename', '8080', False, False),
|
||||
obj.Addr('', '8000', False, False)],
|
||||
[obj.Addr('somename', '8080', False, False,
|
||||
False, False),
|
||||
obj.Addr('', '8000', False, False,
|
||||
False, False)],
|
||||
False, True,
|
||||
set(['somename', 'another.alias', 'alias']),
|
||||
[], [10, 1, 12])
|
||||
vhost3 = obj.VirtualHost(nparser.abs_path('sites-enabled/example.com'),
|
||||
[obj.Addr('69.50.225.155', '9000',
|
||||
False, False),
|
||||
obj.Addr('127.0.0.1', '', False, False)],
|
||||
False, False, False, False),
|
||||
obj.Addr('127.0.0.1', '', False, False,
|
||||
False, False)],
|
||||
False, True,
|
||||
set(['.example.com', 'example.*']), [], [0])
|
||||
vhost4 = obj.VirtualHost(nparser.abs_path('sites-enabled/default'),
|
||||
[obj.Addr('myhost', '', False, True)],
|
||||
[obj.Addr('myhost', '', False, True,
|
||||
False, False),
|
||||
obj.Addr('otherhost', '', False, True,
|
||||
False, False)],
|
||||
False, True, set(['www.example.org']),
|
||||
[], [0])
|
||||
vhost5 = obj.VirtualHost(nparser.abs_path('foo.conf'),
|
||||
[obj.Addr('*', '80', True, True)],
|
||||
[obj.Addr('*', '80', True, True,
|
||||
False, False)],
|
||||
True, True, set(['*.www.foo.com',
|
||||
'*.www.example.com']),
|
||||
[], [2, 1, 0])
|
||||
|
||||
self.assertEqual(10, len(vhosts))
|
||||
self.assertEqual(12, len(vhosts))
|
||||
example_com = [x for x in vhosts if 'example.com' in x.filep][0]
|
||||
self.assertEqual(vhost3, example_com)
|
||||
default = [x for x in vhosts if 'default' in x.filep][0]
|
||||
@@ -323,9 +334,9 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
|
||||
["\n", "a", " ", "b", "\n"],
|
||||
["c", " ", "d"],
|
||||
["\n", "e", " ", "f"]])
|
||||
from certbot_nginx.parser import _comment_directive, COMMENT_BLOCK
|
||||
_comment_directive(block, 1)
|
||||
_comment_directive(block, 0)
|
||||
from certbot_nginx.parser import comment_directive, COMMENT_BLOCK
|
||||
comment_directive(block, 1)
|
||||
comment_directive(block, 0)
|
||||
self.assertEqual(block.spaced, [
|
||||
["\n", "a", " ", "b", "\n"],
|
||||
COMMENT_BLOCK,
|
||||
@@ -395,6 +406,29 @@ class NginxParserTest(util.NginxTest): #pylint: disable=too-many-public-methods
|
||||
])
|
||||
self.assertTrue(server['ssl'])
|
||||
|
||||
def test_duplicate_vhost(self):
|
||||
nparser = parser.NginxParser(self.config_path)
|
||||
|
||||
vhosts = nparser.get_vhosts()
|
||||
default = [x for x in vhosts if 'default' in x.filep][0]
|
||||
new_vhost = nparser.duplicate_vhost(default, delete_default=True)
|
||||
nparser.filedump(ext='')
|
||||
|
||||
# check properties of new vhost
|
||||
self.assertFalse(next(iter(new_vhost.addrs)).default)
|
||||
self.assertNotEqual(new_vhost.path, default.path)
|
||||
|
||||
# check that things are written to file correctly
|
||||
new_nparser = parser.NginxParser(self.config_path)
|
||||
new_vhosts = new_nparser.get_vhosts()
|
||||
new_defaults = [x for x in new_vhosts if 'default' in x.filep]
|
||||
self.assertEqual(len(new_defaults), 2)
|
||||
new_vhost_parsed = new_defaults[1]
|
||||
self.assertFalse(next(iter(new_vhost_parsed.addrs)).default)
|
||||
self.assertEqual(next(iter(default.names)), next(iter(new_vhost_parsed.names)))
|
||||
self.assertEqual(len(default.raw), len(new_vhost_parsed.raw))
|
||||
self.assertTrue(next(iter(default.addrs)).super_eq(next(iter(new_vhost_parsed.addrs))))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main() # pragma: no cover
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
server {
|
||||
listen myhost default_server;
|
||||
listen otherhost default_server;
|
||||
server_name www.example.org;
|
||||
|
||||
location / {
|
||||
|
||||
5
certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com
vendored
Normal file
5
certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6.com
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name ipv6.com;
|
||||
}
|
||||
5
certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com
vendored
Normal file
5
certbot-nginx/certbot_nginx/tests/testdata/etc_nginx/sites-enabled/ipv6ssl.com
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl ipv6only=on;
|
||||
server_name ipv6ssl.com;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ from certbot_nginx.tests import util
|
||||
class TlsSniPerformTest(util.NginxTest):
|
||||
"""Test the NginxTlsSni01 challenge."""
|
||||
|
||||
account_key = common_test.TLSSNI01Test.auth_key
|
||||
account_key = common_test.AUTH_KEY
|
||||
achalls = [
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
@@ -66,7 +66,7 @@ class TlsSniPerformTest(util.NginxTest):
|
||||
self.sni.add_chall(self.achalls[1])
|
||||
mock_choose.return_value = None
|
||||
result = self.sni.perform()
|
||||
self.assertTrue(result is None)
|
||||
self.assertFalse(result is None)
|
||||
|
||||
def test_perform0(self):
|
||||
responses = self.sni.perform()
|
||||
@@ -125,10 +125,10 @@ class TlsSniPerformTest(util.NginxTest):
|
||||
self.sni.add_chall(self.achalls[0])
|
||||
self.sni.add_chall(self.achalls[2])
|
||||
|
||||
v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False),
|
||||
obj.Addr("127.0.0.1", "", False, False)]
|
||||
v_addr2 = [obj.Addr("myhost", "", False, True)]
|
||||
v_addr2_print = [obj.Addr("myhost", "", False, False)]
|
||||
v_addr1 = [obj.Addr("69.50.225.155", "9000", True, False, False, False),
|
||||
obj.Addr("127.0.0.1", "", False, False, False, False)]
|
||||
v_addr2 = [obj.Addr("myhost", "", False, True, False, False)]
|
||||
v_addr2_print = [obj.Addr("myhost", "", False, False, False, False)]
|
||||
ll_addr = [v_addr1, v_addr2]
|
||||
self.sni._mod_config(ll_addr) # pylint: disable=protected-access
|
||||
|
||||
|
||||
@@ -5,11 +5,10 @@ import pkg_resources
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
import zope.component
|
||||
|
||||
from acme import jose
|
||||
|
||||
from certbot import configuration
|
||||
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
@@ -51,19 +51,32 @@ class NginxTlsSni01(common.TLSSNI01):
|
||||
default_addr = "{0} ssl".format(
|
||||
self.configurator.config.tls_sni_01_port)
|
||||
|
||||
for achall in self.achalls:
|
||||
vhost = self.configurator.choose_vhost(achall.domain)
|
||||
if vhost is None:
|
||||
logger.error(
|
||||
"No nginx vhost exists with server_name matching: %s. "
|
||||
"Please specify server_names in the Nginx config.",
|
||||
achall.domain)
|
||||
return None
|
||||
ipv6, ipv6only = self.configurator.ipv6_info(
|
||||
self.configurator.config.tls_sni_01_port)
|
||||
|
||||
if vhost.addrs:
|
||||
for achall in self.achalls:
|
||||
vhost = self.configurator.choose_vhost(achall.domain, create_if_no_match=True)
|
||||
|
||||
if vhost is not None and vhost.addrs:
|
||||
addresses.append(list(vhost.addrs))
|
||||
else:
|
||||
addresses.append([obj.Addr.fromstring(default_addr)])
|
||||
if ipv6:
|
||||
# If IPv6 is active in Nginx configuration
|
||||
ipv6_addr = "[::]:{0} ssl".format(
|
||||
self.configurator.config.tls_sni_01_port)
|
||||
if not ipv6only:
|
||||
# If ipv6only=on is not already present in the config
|
||||
ipv6_addr = ipv6_addr + " ipv6only=on"
|
||||
addresses.append([obj.Addr.fromstring(default_addr),
|
||||
obj.Addr.fromstring(ipv6_addr)])
|
||||
logger.info(("Using default addresses %s and %s for " +
|
||||
"TLSSNI01 authentication."),
|
||||
default_addr,
|
||||
ipv6_addr)
|
||||
else:
|
||||
addresses.append([obj.Addr.fromstring(default_addr)])
|
||||
logger.info("Using default address %s for TLSSNI01 authentication.",
|
||||
default_addr)
|
||||
|
||||
# Create challenge certs
|
||||
responses = [self._setup_challenge_cert(x) for x in self.achalls]
|
||||
@@ -117,7 +130,6 @@ class NginxTlsSni01(common.TLSSNI01):
|
||||
raise errors.MisconfigurationError(
|
||||
'Certbot could not find an HTTP block to include '
|
||||
'TLS-SNI-01 challenges in %s.' % root)
|
||||
|
||||
config = [self._make_server_block(pair[0], pair[1])
|
||||
for pair in six.moves.zip(self.achalls, ll_addrs)]
|
||||
config = nginxparser.UnspacedList(config)
|
||||
|
||||
@@ -4,7 +4,7 @@ from setuptools import setup
|
||||
from setuptools import find_packages
|
||||
|
||||
|
||||
version = '0.20.0.dev0'
|
||||
version = '0.21.0.dev0'
|
||||
|
||||
# Please update tox.ini when modifying dependency version requirements
|
||||
install_requires = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Certbot client."""
|
||||
|
||||
# version number like 1.2.3a0, must have at least 2 parts, like 1.2
|
||||
__version__ = '0.20.0.dev0'
|
||||
__version__ = '0.21.0.dev0'
|
||||
|
||||
@@ -7,13 +7,13 @@ import shutil
|
||||
import socket
|
||||
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
import josepy as jose
|
||||
import pyrfc3339
|
||||
import pytz
|
||||
import six
|
||||
import zope.component
|
||||
|
||||
from acme import fields as acme_fields
|
||||
from acme import jose
|
||||
from acme import messages
|
||||
|
||||
from certbot import errors
|
||||
|
||||
@@ -19,8 +19,9 @@ Note, that all annotated challenges act as a proxy objects::
|
||||
"""
|
||||
import logging
|
||||
|
||||
import josepy as jose
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -1220,6 +1220,18 @@ def _create_subparsers(helpful):
|
||||
key=constants.REVOCATION_REASONS.get)),
|
||||
action=_EncodeReasonAction, default=flag_default("reason"),
|
||||
help="Specify reason for revoking certificate. (default: unspecified)")
|
||||
helpful.add("revoke",
|
||||
"--delete-after-revoke", action="store_true",
|
||||
default=flag_default("delete_after_revoke"),
|
||||
help="Delete certificates after revoking them.")
|
||||
helpful.add("revoke",
|
||||
"--no-delete-after-revoke", action="store_false",
|
||||
dest="delete_after_revoke",
|
||||
default=flag_default("delete_after_revoke"),
|
||||
help="Do not delete certificates after revoking them. This "
|
||||
"option should be used with caution because the 'renew' "
|
||||
"subcommand will attempt to renew undeleted revoked "
|
||||
"certificates.")
|
||||
helpful.add("rollback",
|
||||
"--checkpoints", type=int, metavar="N",
|
||||
default=flag_default("rollback_checkpoints"),
|
||||
|
||||
@@ -5,13 +5,13 @@ import platform
|
||||
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
import josepy as jose
|
||||
import OpenSSL
|
||||
import zope.component
|
||||
|
||||
from acme import client as acme_client
|
||||
from acme import crypto_util as acme_crypto_util
|
||||
from acme import errors as acme_errors
|
||||
from acme import jose
|
||||
from acme import messages
|
||||
|
||||
import certbot
|
||||
|
||||
@@ -71,6 +71,7 @@ CLI_DEFAULTS = dict(
|
||||
user_agent_comment=None,
|
||||
csr=None,
|
||||
reason=0,
|
||||
delete_after_revoke=None,
|
||||
rollback_checkpoints=1,
|
||||
init=False,
|
||||
prepare=False,
|
||||
|
||||
@@ -14,9 +14,9 @@ import six
|
||||
import zope.component
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography import x509
|
||||
import josepy as jose
|
||||
|
||||
from acme import crypto_util as acme_crypto_util
|
||||
from acme import jose
|
||||
|
||||
from certbot import errors
|
||||
from certbot import interfaces
|
||||
@@ -368,7 +368,7 @@ def dump_pyopenssl_chain(chain, filetype=OpenSSL.crypto.FILETYPE_PEM):
|
||||
"""Dump certificate chain into a bundle.
|
||||
|
||||
:param list chain: List of `OpenSSL.crypto.X509` (or wrapped in
|
||||
`acme.jose.ComparableX509`).
|
||||
:class:`josepy.util.ComparableX509`).
|
||||
|
||||
"""
|
||||
# XXX: returns empty string when no chain is available, which
|
||||
|
||||
461
certbot/main.py
461
certbot/main.py
@@ -4,11 +4,12 @@ import functools
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import configobj
|
||||
import josepy as jose
|
||||
import zope.component
|
||||
|
||||
from acme import jose
|
||||
from acme import errors as acme_errors
|
||||
|
||||
import certbot
|
||||
@@ -43,7 +44,15 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _suggest_donation_if_appropriate(config):
|
||||
"""Potentially suggest a donation to support Certbot."""
|
||||
"""Potentially suggest a donation to support Certbot.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
assert config.verb != "renew"
|
||||
if config.staging:
|
||||
# --dry-run implies --staging
|
||||
@@ -55,6 +64,15 @@ def _suggest_donation_if_appropriate(config):
|
||||
reporter_util.add_message(msg, reporter_util.LOW_PRIORITY)
|
||||
|
||||
def _report_successful_dry_run(config):
|
||||
"""Reports on successful dry run
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
assert config.verb != "renew"
|
||||
reporter_util.add_message("The dry run was successful.",
|
||||
@@ -68,8 +86,23 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N
|
||||
then performs that action. Includes calls to hooks, various reports,
|
||||
checks, and requests for user input.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param domains: List of domain names to get a certificate. Defaults to `None`
|
||||
:type domains: `list` of `str`
|
||||
|
||||
:param certname: Name of new certificate. Defaults to `None`
|
||||
:type certname: str
|
||||
|
||||
:param lineage: Certificate lineage object. Defaults to `None`
|
||||
:type lineage: storage.RenewableCert
|
||||
|
||||
:returns: the issued certificate or `None` if doing a dry run
|
||||
:rtype: `storage.RenewableCert` or `None`
|
||||
:rtype: storage.RenewableCert or None
|
||||
|
||||
:raises errors.Error: if certificate could not be obtained
|
||||
|
||||
"""
|
||||
hooks.pre_hook(config)
|
||||
try:
|
||||
@@ -96,11 +129,18 @@ def _get_and_save_cert(le_client, config, domains=None, certname=None, lineage=N
|
||||
def _handle_subset_cert_request(config, domains, cert):
|
||||
"""Figure out what to do if a previous cert had a subset of the names now requested
|
||||
|
||||
:param storage.RenewableCert cert:
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param domains: List of domain names
|
||||
:type domains: `list` of `str`
|
||||
|
||||
:param cert: Certificate object
|
||||
:type cert: storage.RenewableCert
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
|
||||
action can be: "newcert" | "renew" | "reinstall"
|
||||
:rtype: tuple
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
"""
|
||||
existing = ", ".join(cert.names())
|
||||
@@ -137,11 +177,15 @@ def _handle_subset_cert_request(config, domains, cert):
|
||||
def _handle_identical_cert_request(config, lineage):
|
||||
"""Figure out what to do if a lineage has the same names as a previously obtained one
|
||||
|
||||
:param storage.RenewableCert lineage:
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: storage.RenewableCert
|
||||
|
||||
:returns: Tuple of (str action, cert_or_None) as per _find_lineage_for_domains_and_certname
|
||||
action can be: "newcert" | "renew" | "reinstall"
|
||||
:rtype: tuple
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
"""
|
||||
if not lineage.ensure_deployed():
|
||||
@@ -186,11 +230,18 @@ def _find_lineage_for_domains(config, domains):
|
||||
the client run if the user chooses to cancel the operation when
|
||||
prompted).
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param domains: List of domain names
|
||||
:type domains: `list` of `str`
|
||||
|
||||
:returns: Two-element tuple containing desired new-certificate behavior as
|
||||
a string token ("reinstall", "renew", or "newcert"), plus either
|
||||
a RenewableCert instance or None if renewal shouldn't occur.
|
||||
a RenewableCert instance or `None` if renewal shouldn't occur.
|
||||
:rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None`
|
||||
|
||||
:raises .Error: If the user would like to rerun the client again.
|
||||
:raises errors.Error: If the user would like to rerun the client again.
|
||||
|
||||
"""
|
||||
# Considering the possibility that the requested certificate is
|
||||
@@ -214,9 +265,20 @@ def _find_lineage_for_domains(config, domains):
|
||||
def _find_cert(config, domains, certname):
|
||||
"""Finds an existing certificate object given domains and/or a certificate name.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param domains: List of domain names
|
||||
:type domains: `list` of `str`
|
||||
|
||||
:param certname: Name of certificate
|
||||
:type certname: str
|
||||
|
||||
:returns: Two-element tuple of a boolean that indicates if this function should be
|
||||
followed by a call to fetch a certificate from the server, and either a
|
||||
RenewableCert instance or None.
|
||||
:rtype: `tuple` of `bool` and :class:`storage.RenewableCert` or `None`
|
||||
|
||||
"""
|
||||
action, lineage = _find_lineage_for_domains_and_certname(config, domains, certname)
|
||||
if action == "reinstall":
|
||||
@@ -226,11 +288,22 @@ def _find_cert(config, domains, certname):
|
||||
def _find_lineage_for_domains_and_certname(config, domains, certname):
|
||||
"""Find appropriate lineage based on given domains and/or certname.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param domains: List of domain names
|
||||
:type domains: `list` of `str`
|
||||
|
||||
:param certname: Name of certificate
|
||||
:type certname: str
|
||||
|
||||
:returns: Two-element tuple containing desired new-certificate behavior as
|
||||
a string token ("reinstall", "renew", or "newcert"), plus either
|
||||
a RenewableCert instance or None if renewal shouldn't occur.
|
||||
a RenewableCert instance or None if renewal should not occur.
|
||||
|
||||
:raises .Error: If the user would like to rerun the client again.
|
||||
:rtype: `tuple` of `str` and :class:`storage.RenewableCert` or `None`
|
||||
|
||||
:raises errors.Error: If the user would like to rerun the client again.
|
||||
|
||||
"""
|
||||
if not certname:
|
||||
@@ -253,18 +326,57 @@ def _find_lineage_for_domains_and_certname(config, domains, certname):
|
||||
"Use -d to specify domains, or run certbot --certificates to see "
|
||||
"possible certificate names.".format(certname))
|
||||
|
||||
def _get_added_removed(after, before):
|
||||
"""Get lists of items removed from `before`
|
||||
and a lists of items added to `after`
|
||||
"""
|
||||
added = list(set(after) - set(before))
|
||||
removed = list(set(before) - set(after))
|
||||
added.sort()
|
||||
removed.sort()
|
||||
return added, removed
|
||||
|
||||
def _format_list(character, strings):
|
||||
"""Format list with given character
|
||||
"""
|
||||
formatted = "{br}{ch} " + "{br}{ch} ".join(strings)
|
||||
return formatted.format(
|
||||
ch=character,
|
||||
br=os.linesep
|
||||
)
|
||||
|
||||
def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains):
|
||||
"""Ask user to confirm update cert certname to contain new_domains.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param new_domains: List of new domain names
|
||||
:type new_domains: `list` of `str`
|
||||
|
||||
:param certname: Name of certificate
|
||||
:type certname: str
|
||||
|
||||
:param old_domains: List of old domain names
|
||||
:type old_domains: `list` of `str`
|
||||
|
||||
:returns: None
|
||||
:rtype: None
|
||||
|
||||
:raises errors.ConfigurationError: if cert name and domains mismatch
|
||||
|
||||
"""
|
||||
if config.renew_with_new_domains:
|
||||
return
|
||||
|
||||
msg = ("You are updating certificate {0} to include domains: {1}{br}{br}"
|
||||
"It previously included domains: {2}{br}{br}"
|
||||
added, removed = _get_added_removed(new_domains, old_domains)
|
||||
|
||||
msg = ("You are updating certificate {0} to include new domain(s): {1}{br}{br}"
|
||||
"You are also removing previously included domain(s): {2}{br}{br}"
|
||||
"Did you intend to make this change?".format(
|
||||
certname,
|
||||
", ".join(new_domains),
|
||||
", ".join(old_domains),
|
||||
_format_list("+", added),
|
||||
_format_list("-", removed),
|
||||
br=os.linesep))
|
||||
obj = zope.component.getUtility(interfaces.IDisplay)
|
||||
if not obj.yesno(msg, "Update cert", "Cancel", default=True):
|
||||
@@ -272,6 +384,19 @@ def _ask_user_to_confirm_new_names(config, new_domains, certname, old_domains):
|
||||
|
||||
def _find_domains_or_certname(config, installer):
|
||||
"""Retrieve domains and certname from config or user input.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param installer: Installer object
|
||||
:type installer: interfaces.IInstaller
|
||||
|
||||
|
||||
:returns: Two-part tuple of domains and certname
|
||||
:rtype: `tuple` of list of `str` and `str`
|
||||
|
||||
:raises errors.Error: Usage message, if parameters are not used correctly
|
||||
|
||||
"""
|
||||
domains = None
|
||||
certname = config.certname
|
||||
@@ -299,9 +424,17 @@ def _find_domains_or_certname(config, installer):
|
||||
def _report_new_cert(config, cert_path, fullchain_path, key_path=None):
|
||||
"""Reports the creation of a new certificate to the user.
|
||||
|
||||
:param str cert_path: path to cert
|
||||
:param str fullchain_path: path to full chain
|
||||
:param str key_path: path to private key, if available
|
||||
:param cert_path: path to certificate
|
||||
:type cert_path: str
|
||||
|
||||
:param fullchain_path: path to full chain
|
||||
:type fullchain_path: str
|
||||
|
||||
:param key_path: path to private key, if available
|
||||
:type key_path: str
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
if config.dry_run:
|
||||
@@ -337,14 +470,14 @@ def _determine_account(config):
|
||||
if ``config.account`` is ``None``, it will be updated based on the
|
||||
user input. Same for ``config.email``.
|
||||
|
||||
:param argparse.Namespace config: CLI arguments
|
||||
:param certbot.interface.IConfig config: Configuration object
|
||||
:param .AccountStorage account_storage: Account storage.
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:returns: Account and optionally ACME client API (biproduct of new
|
||||
registration).
|
||||
:rtype: `tuple` of `certbot.account.Account` and
|
||||
`acme.client.Client`
|
||||
:rtype: tuple of :class:`certbot.account.Account` and :class:`acme.client.Client`
|
||||
|
||||
:raises errors.Error: If unable to register an account with ACME server
|
||||
|
||||
"""
|
||||
account_storage = account.AccountFileStorage(config)
|
||||
@@ -392,17 +525,23 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b
|
||||
deleting happens automatically, unless if both `--cert-name` and `--cert-path` were
|
||||
specified with conflicting values.
|
||||
|
||||
:param `configuration.NamespaceConfig` config: parsed command line arguments
|
||||
:param config: parsed command line arguments
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:raises `error.Errors`: If anything goes wrong, including bad user input, if an overlapping
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
:raises errors.Error: If anything goes wrong, including bad user input, if an overlapping
|
||||
archive dir is found for the specified lineage, etc ...
|
||||
"""
|
||||
display = zope.component.getUtility(interfaces.IDisplay)
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
|
||||
msg = ("Would you like to delete the cert(s) you just revoked?")
|
||||
attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No",
|
||||
force_interactive=True, default=True)
|
||||
attempt_deletion = config.delete_after_revoke
|
||||
if attempt_deletion is None:
|
||||
msg = ("Would you like to delete the cert(s) you just revoked?")
|
||||
attempt_deletion = display.yesno(msg, yes_label="Yes (recommended)", no_label="No",
|
||||
force_interactive=True, default=True)
|
||||
|
||||
if not attempt_deletion:
|
||||
reporter_util.add_message("Not deleting revoked certs.", reporter_util.LOW_PRIORITY)
|
||||
@@ -474,6 +613,20 @@ def _delete_if_appropriate(config): # pylint: disable=too-many-locals,too-many-b
|
||||
|
||||
|
||||
def _init_le_client(config, authenticator, installer):
|
||||
"""Initialize Let's Encrypt Client
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param authenticator: Acme authentication handler
|
||||
:type authenticator: interfaces.IAuthenticator
|
||||
:param installer: Installer object
|
||||
:type installer: interfaces.IInstaller
|
||||
|
||||
:returns: client: Client object
|
||||
:rtype: client.Client
|
||||
|
||||
"""
|
||||
if authenticator is not None:
|
||||
# if authenticator was given, then we will need account...
|
||||
acc, acme = _determine_account(config)
|
||||
@@ -487,7 +640,18 @@ def _init_le_client(config, authenticator, installer):
|
||||
|
||||
|
||||
def unregister(config, unused_plugins):
|
||||
"""Deactivate account on server"""
|
||||
"""Deactivate account on server
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
account_storage = account.AccountFileStorage(config)
|
||||
accounts = account_storage.find_all()
|
||||
reporter_util = zope.component.getUtility(interfaces.IReporter)
|
||||
@@ -516,8 +680,18 @@ def unregister(config, unused_plugins):
|
||||
|
||||
|
||||
def register(config, unused_plugins):
|
||||
"""Create or modify accounts on the server."""
|
||||
"""Create or modify accounts on the server.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None` or a string indicating and error
|
||||
:rtype: None or str
|
||||
|
||||
"""
|
||||
# Portion of _determine_account logic to see whether accounts already
|
||||
# exist or not.
|
||||
account_storage = account.AccountFileStorage(config)
|
||||
@@ -558,6 +732,24 @@ def register(config, unused_plugins):
|
||||
add_msg("Your e-mail address was updated to {0}.".format(config.email))
|
||||
|
||||
def _install_cert(config, le_client, domains, lineage=None):
|
||||
"""Install a cert
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param le_client: Client object
|
||||
:type le_client: client.Client
|
||||
|
||||
:param plugins: List of domains
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:param lineage: Certificate lineage object. Defaults to `None`
|
||||
:type lineage: storage.RenewableCert
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
path_provider = lineage if lineage else config
|
||||
assert path_provider.cert_path is not None
|
||||
|
||||
@@ -566,7 +758,18 @@ def _install_cert(config, le_client, domains, lineage=None):
|
||||
le_client.enhance_config(domains, path_provider.chain_path)
|
||||
|
||||
def install(config, plugins):
|
||||
"""Install a previously obtained cert in a server."""
|
||||
"""Install a previously obtained cert in a server.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param plugins: List of plugins
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
# XXX: Update for renewer/RenewableCert
|
||||
# FIXME: be consistent about whether errors are raised or returned from
|
||||
# this function ...
|
||||
@@ -582,7 +785,18 @@ def install(config, plugins):
|
||||
|
||||
|
||||
def plugins_cmd(config, plugins):
|
||||
"""List server software plugins."""
|
||||
"""List server software plugins.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param plugins: List of plugins
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
logger.debug("Expected interfaces: %s", config.ifaces)
|
||||
|
||||
ifaces = [] if config.ifaces is None else config.ifaces
|
||||
@@ -610,7 +824,18 @@ def plugins_cmd(config, plugins):
|
||||
|
||||
|
||||
def rollback(config, plugins):
|
||||
"""Rollback server configuration changes made during install."""
|
||||
"""Rollback server configuration changes made during install.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param plugins: List of plugins
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
client.rollback(config.installer, config.checkpoints, config, plugins)
|
||||
|
||||
|
||||
@@ -619,6 +844,15 @@ def config_changes(config, unused_plugins):
|
||||
|
||||
View checkpoints and associated configuration changes.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
client.view_config_changes(config, num=config.num)
|
||||
|
||||
@@ -627,6 +861,16 @@ def update_symlinks(config, unused_plugins):
|
||||
|
||||
Use the information in the config file to make symlinks point to
|
||||
the correct archive directory.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
cert_manager.update_live_symlinks(config)
|
||||
|
||||
@@ -635,6 +879,16 @@ def rename(config, unused_plugins):
|
||||
|
||||
Use the information in the config file to rename an existing
|
||||
lineage.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
cert_manager.rename_lineage(config)
|
||||
|
||||
@@ -643,16 +897,47 @@ def delete(config, unused_plugins):
|
||||
|
||||
Use the information in the config file to delete an existing
|
||||
lineage.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
cert_manager.delete(config)
|
||||
|
||||
def certificates(config, unused_plugins):
|
||||
"""Display information about certs configured with Certbot
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
cert_manager.certificates(config)
|
||||
|
||||
def revoke(config, unused_plugins): # TODO: coop with renewal config
|
||||
"""Revoke a previously obtained certificate."""
|
||||
"""Revoke a previously obtained certificate.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None` or string indicating error in case of error
|
||||
:rtype: None or str
|
||||
|
||||
"""
|
||||
# For user-agent construction
|
||||
config.installer = config.authenticator = "None"
|
||||
if config.key_path is not None: # revocation by cert key
|
||||
@@ -678,7 +963,18 @@ def revoke(config, unused_plugins): # TODO: coop with renewal config
|
||||
|
||||
|
||||
def run(config, plugins): # pylint: disable=too-many-branches,too-many-locals
|
||||
"""Obtain a certificate and install."""
|
||||
"""Obtain a certificate and install.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param plugins: List of plugins
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
# TODO: Make run as close to auth + install as possible
|
||||
# Possible difficulties: config.csr was hacked into auth
|
||||
try:
|
||||
@@ -718,6 +1014,16 @@ def _csr_get_and_save_cert(config, le_client):
|
||||
This works differently in the CSR case (for now) because we don't
|
||||
have the privkey, and therefore can't construct the files for a lineage.
|
||||
So we just save the cert & chain to disk :/
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param client: Client object
|
||||
:type client: client.Client
|
||||
|
||||
:returns: `cert_path` and `fullchain_path` as absolute paths to the actual files
|
||||
:rtype: `tuple` of `str`
|
||||
|
||||
"""
|
||||
csr, _ = config.actual_csr
|
||||
certr, chain = le_client.obtain_certificate_from_csr(config.domains, csr)
|
||||
@@ -730,7 +1036,23 @@ def _csr_get_and_save_cert(config, le_client):
|
||||
return cert_path, fullchain_path
|
||||
|
||||
def renew_cert(config, plugins, lineage):
|
||||
"""Renew & save an existing cert. Do not install it."""
|
||||
"""Renew & save an existing cert. Do not install it.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param plugins: List of plugins
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:param lineage: Certificate lineage object
|
||||
:type lineage: storage.RenewableCert
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
:raises errors.PluginSelectionError: MissingCommandlineFlag if supplied parameters do not pass
|
||||
|
||||
"""
|
||||
try:
|
||||
# installers are used in auth mode to determine domain names
|
||||
installer, auth = plug_sel.choose_configurator_plugins(config, plugins, "certonly")
|
||||
@@ -757,8 +1079,20 @@ def renew_cert(config, plugins, lineage):
|
||||
def certonly(config, plugins):
|
||||
"""Authenticate & obtain cert, but do not install it.
|
||||
|
||||
This implements the 'certonly' subcommand."""
|
||||
This implements the 'certonly' subcommand.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param plugins: List of plugins
|
||||
:type plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
:raises errors.Error: If specified plugin could not be used
|
||||
|
||||
"""
|
||||
# SETUP: Select plugins and construct a client instance
|
||||
try:
|
||||
# installers are used in auth mode to determine domain names
|
||||
@@ -792,7 +1126,18 @@ def certonly(config, plugins):
|
||||
_suggest_donation_if_appropriate(config)
|
||||
|
||||
def renew(config, unused_plugins):
|
||||
"""Renew previously-obtained certificates."""
|
||||
"""Renew previously-obtained certificates.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:param unused_plugins: List of plugins (deprecated)
|
||||
:type unused_plugins: `list` of `str`
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
try:
|
||||
renewal.handle_renewal_request(config)
|
||||
finally:
|
||||
@@ -800,7 +1145,15 @@ def renew(config, unused_plugins):
|
||||
|
||||
|
||||
def make_or_verify_needed_dirs(config):
|
||||
"""Create or verify existence of config, work, and hook directories."""
|
||||
"""Create or verify existence of config, work, and hook directories.
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
util.set_up_core_dir(config.config_dir, constants.CONFIG_DIRS_MODE,
|
||||
os.geteuid(), config.strict_permissions)
|
||||
util.set_up_core_dir(config.work_dir, constants.CONFIG_DIRS_MODE,
|
||||
@@ -816,7 +1169,15 @@ def make_or_verify_needed_dirs(config):
|
||||
|
||||
|
||||
def set_displayer(config):
|
||||
"""Set the displayer"""
|
||||
"""Set the displayer
|
||||
|
||||
:param config: Configuration object
|
||||
:type config: interfaces.IConfig
|
||||
|
||||
:returns: `None`
|
||||
:rtype: None
|
||||
|
||||
"""
|
||||
if config.quiet:
|
||||
config.noninteractive_mode = True
|
||||
displayer = display_util.NoninteractiveDisplay(open(os.devnull, "w"))
|
||||
@@ -829,7 +1190,14 @@ def set_displayer(config):
|
||||
|
||||
|
||||
def main(cli_args=sys.argv[1:]):
|
||||
"""Command line argument parsing and main script execution."""
|
||||
"""Command line argument parsing and main script execution.
|
||||
|
||||
:returns: result of requested command
|
||||
|
||||
:raises errors.Error: OS errors triggered by wrong permissions
|
||||
:raises errors.Error: error if plugin command is not supported
|
||||
|
||||
"""
|
||||
log.pre_arg_parse_setup()
|
||||
|
||||
plugins = plugins_disco.PluginsRegistry.find_all()
|
||||
@@ -850,6 +1218,17 @@ def main(cli_args=sys.argv[1:]):
|
||||
# Let plugins_cmd be run as un-privileged user.
|
||||
if config.func != plugins_cmd:
|
||||
raise
|
||||
deprecation_fmt = (
|
||||
"Python %s.%s support will be dropped in the next "
|
||||
"release of Certbot - please upgrade your Python version.")
|
||||
# We use the warnings system for Python 2.6 and logging for Python 3
|
||||
# because DeprecationWarnings are only reported by default in Python <= 2.6
|
||||
# and warnings can be disabled by the user.
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
warning = deprecation_fmt % sys.version_info[:2]
|
||||
warnings.warn(warning, DeprecationWarning)
|
||||
elif sys.version_info[:2] == (3, 3):
|
||||
logger.warning(deprecation_fmt, *sys.version_info[:2])
|
||||
|
||||
set_displayer(config)
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import OpenSSL
|
||||
import pkg_resources
|
||||
import zope.interface
|
||||
|
||||
from acme.jose import util as jose_util
|
||||
from josepy import util as jose_util
|
||||
|
||||
from certbot import constants
|
||||
from certbot import crypto_util
|
||||
@@ -251,7 +251,7 @@ class Addr(object):
|
||||
"""Normalized representation of addr/port tuple
|
||||
"""
|
||||
if self.ipv6:
|
||||
return (self._normalize_ipv6(self.tup[0]), self.tup[1])
|
||||
return (self.get_ipv6_exploded(), self.tup[1])
|
||||
return self.tup
|
||||
|
||||
def __eq__(self, other):
|
||||
@@ -315,23 +315,28 @@ class Addr(object):
|
||||
return result
|
||||
|
||||
|
||||
class TLSSNI01(object):
|
||||
"""Abstract base for TLS-SNI-01 challenge performers"""
|
||||
class ChallengePerformer(object):
|
||||
"""Abstract base for challenge performers.
|
||||
|
||||
:ivar configurator: Authenticator and installer plugin
|
||||
:ivar achalls: Annotated challenges
|
||||
:vartype achalls: `list` of `.KeyAuthorizationAnnotatedChallenge`
|
||||
:ivar indices: Holds the indices of challenges from a larger array
|
||||
so the user of the class doesn't have to.
|
||||
:vartype indices: `list` of `int`
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, configurator):
|
||||
self.configurator = configurator
|
||||
self.achalls = []
|
||||
self.indices = []
|
||||
self.challenge_conf = os.path.join(
|
||||
configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf")
|
||||
# self.completed = 0
|
||||
|
||||
def add_chall(self, achall, idx=None):
|
||||
"""Add challenge to TLSSNI01 object to perform at once.
|
||||
"""Store challenge to be performed when perform() is called.
|
||||
|
||||
:param .KeyAuthorizationAnnotatedChallenge achall: Annotated
|
||||
TLSSNI01 challenge.
|
||||
|
||||
challenge.
|
||||
:param int idx: index to challenge in a larger array
|
||||
|
||||
"""
|
||||
@@ -339,6 +344,27 @@ class TLSSNI01(object):
|
||||
if idx is not None:
|
||||
self.indices.append(idx)
|
||||
|
||||
def perform(self):
|
||||
"""Perform all added challenges.
|
||||
|
||||
:returns: challenge respones
|
||||
:rtype: `list` of `acme.challenges.KeyAuthorizationChallengeResponse`
|
||||
|
||||
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class TLSSNI01(ChallengePerformer):
|
||||
# pylint: disable=abstract-method
|
||||
"""Abstract base for TLS-SNI-01 challenge performers"""
|
||||
|
||||
def __init__(self, configurator):
|
||||
super(TLSSNI01, self).__init__(configurator)
|
||||
self.challenge_conf = os.path.join(
|
||||
configurator.config.config_dir, "le_tls_sni_01_cert_challenge.conf")
|
||||
# self.completed = 0
|
||||
|
||||
def get_cert_path(self, achall):
|
||||
"""Returns standardized name for challenge certificate.
|
||||
|
||||
|
||||
@@ -5,11 +5,11 @@ import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
import josepy as jose
|
||||
import mock
|
||||
import OpenSSL
|
||||
|
||||
from acme import challenges
|
||||
from acme import jose
|
||||
|
||||
from certbot import achallenges
|
||||
from certbot import crypto_util
|
||||
@@ -18,6 +18,17 @@ from certbot import errors
|
||||
from certbot.tests import acme_util
|
||||
from certbot.tests import util as test_util
|
||||
|
||||
AUTH_KEY = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
ACHALLS = [
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.TLSSNI01(token=b'token1'), "pending"),
|
||||
domain="encryption-example.demo", account_key=AUTH_KEY),
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.TLSSNI01(token=b'token2'), "pending"),
|
||||
domain="certbot.demo", account_key=AUTH_KEY),
|
||||
]
|
||||
|
||||
class NamespaceFunctionsTest(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.common.*_namespace functions."""
|
||||
@@ -261,21 +272,27 @@ class AddrTest(unittest.TestCase):
|
||||
self.assertEqual(set_c, set_d)
|
||||
|
||||
|
||||
class ChallengePerformerTest(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.common.ChallengePerformer."""
|
||||
|
||||
def setUp(self):
|
||||
configurator = mock.MagicMock()
|
||||
|
||||
from certbot.plugins.common import ChallengePerformer
|
||||
self.performer = ChallengePerformer(configurator)
|
||||
|
||||
def test_add_chall(self):
|
||||
self.performer.add_chall(ACHALLS[0], 0)
|
||||
self.assertEqual(1, len(self.performer.achalls))
|
||||
self.assertEqual([0], self.performer.indices)
|
||||
|
||||
def test_perform(self):
|
||||
self.assertRaises(NotImplementedError, self.performer.perform)
|
||||
|
||||
|
||||
class TLSSNI01Test(unittest.TestCase):
|
||||
"""Tests for certbot.plugins.common.TLSSNI01."""
|
||||
|
||||
auth_key = jose.JWKRSA.load(test_util.load_vector("rsa512_key.pem"))
|
||||
achalls = [
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.TLSSNI01(token=b'token1'), "pending"),
|
||||
domain="encryption-example.demo", account_key=auth_key),
|
||||
achallenges.KeyAuthorizationAnnotatedChallenge(
|
||||
challb=acme_util.chall_to_challb(
|
||||
challenges.TLSSNI01(token=b'token2'), "pending"),
|
||||
domain="certbot.demo", account_key=auth_key),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
self.tempdir = tempfile.mkdtemp()
|
||||
configurator = mock.MagicMock()
|
||||
@@ -288,11 +305,6 @@ class TLSSNI01Test(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
shutil.rmtree(self.tempdir)
|
||||
|
||||
def test_add_chall(self):
|
||||
self.sni.add_chall(self.achalls[0], 0)
|
||||
self.assertEqual(1, len(self.sni.achalls))
|
||||
self.assertEqual([0], self.sni.indices)
|
||||
|
||||
def test_setup_challenge_cert(self):
|
||||
# This is a helper function that can be used for handling
|
||||
# open context managers more elegantly. It avoids dealing with
|
||||
@@ -325,7 +337,7 @@ class TLSSNI01Test(unittest.TestCase):
|
||||
OpenSSL.crypto.dump_privatekey(OpenSSL.crypto.FILETYPE_PEM, key))
|
||||
|
||||
def test_get_z_domain(self):
|
||||
achall = self.achalls[0]
|
||||
achall = ACHALLS[0]
|
||||
self.assertEqual(self.sni.get_z_domain(achall),
|
||||
achall.response(achall.account_key).z_domain.decode("utf-8"))
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user