acme-0.4.1/0000755000175000017500000000000012665157732012033 5ustar bmwbmw00000000000000acme-0.4.1/examples/0000755000175000017500000000000012665157732013651 5ustar bmwbmw00000000000000acme-0.4.1/examples/standalone/0000755000175000017500000000000012665157732016001 5ustar bmwbmw00000000000000acme-0.4.1/examples/standalone/README0000644000175000017500000000010112665157707016653 0ustar bmwbmw00000000000000python -m acme.standalone -p 1234 curl -k https://localhost:1234 acme-0.4.1/examples/standalone/localhost/0000755000175000017500000000000012665157732017771 5ustar bmwbmw00000000000000acme-0.4.1/examples/standalone/localhost/cert.pem0000777000175000017500000000000012665157707026447 2../../../acme/testdata/cert.pemustar bmwbmw00000000000000acme-0.4.1/examples/standalone/localhost/key.pem0000777000175000017500000000000012665157707027232 2../../../acme/testdata/rsa512_key.pemustar bmwbmw00000000000000acme-0.4.1/examples/example_client.py0000644000175000017500000000273612665157707017226 0ustar bmwbmw00000000000000"""Example script showing how to use acme client API.""" import logging import os import pkg_resources from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa import OpenSSL from acme import client from acme import messages from acme import jose logging.basicConfig(level=logging.DEBUG) DIRECTORY_URL = 'https://acme-staging.api.letsencrypt.org/directory' BITS = 2048 # minimum for Boulder DOMAIN = 'example1.com' # example.com is ignored by Boulder # generate_private_key requires cryptography>=0.5 key = jose.JWKRSA(key=rsa.generate_private_key( public_exponent=65537, key_size=BITS, backend=default_backend())) acme = client.Client(DIRECTORY_URL, key) regr = acme.register() logging.info('Auto-accepting TOS: %s', regr.terms_of_service) acme.agree_to_tos(regr) logging.debug(regr) authzr = acme.request_challenges( identifier=messages.Identifier(typ=messages.IDENTIFIER_FQDN, value=DOMAIN), new_authzr_uri=regr.new_authzr_uri) logging.debug(authzr) authzr, authzr_response = acme.poll(authzr) csr = OpenSSL.crypto.load_certificate_request( OpenSSL.crypto.FILETYPE_ASN1, pkg_resources.resource_string( 'acme', os.path.join('testdata', 'csr.der'))) try: acme.request_issuance(jose.util.ComparableX509(csr), (authzr,)) except messages.Error as error: print ("This script is doomed to fail as no authorization " "challenges are ever solved. Error from server: {0}".format(error)) acme-0.4.1/acme.egg-info/0000755000175000017500000000000012665157732014432 5ustar bmwbmw00000000000000acme-0.4.1/acme.egg-info/dependency_links.txt0000644000175000017500000000000112665157732020500 0ustar bmwbmw00000000000000 acme-0.4.1/acme.egg-info/top_level.txt0000644000175000017500000000000512665157732017157 0ustar bmwbmw00000000000000acme acme-0.4.1/acme.egg-info/requires.txt0000644000175000017500000000027312665157732017034 0ustar bmwbmw00000000000000cryptography>=0.8 ndg-httpsclient pyasn1 PyOpenSSL>=0.13 pyrfc3339 pytz requests setuptools six mock [dev] nose pep8 tox [docs] Sphinx>=1.0 sphinx_rtd_theme sphinxcontrib-programoutput acme-0.4.1/acme.egg-info/entry_points.txt0000644000175000017500000000005712665157732017732 0ustar bmwbmw00000000000000[console_scripts] jws = acme.jose.jws:CLI.run acme-0.4.1/acme.egg-info/PKG-INFO0000644000175000017500000000160412665157732015530 0ustar bmwbmw00000000000000Metadata-Version: 1.1 Name: acme Version: 0.4.1 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Let's Encrypt Project Author-email: client-dev@letsencrypt.org License: Apache License 2.0 Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security acme-0.4.1/acme.egg-info/SOURCES.txt0000644000175000017500000000412312665157732016316 0ustar bmwbmw00000000000000LICENSE.txt MANIFEST.in README.rst setup.cfg setup.py acme/__init__.py acme/challenges.py acme/challenges_test.py acme/client.py acme/client_test.py acme/crypto_util.py acme/crypto_util_test.py acme/errors.py acme/errors_test.py acme/fields.py acme/fields_test.py acme/jws.py acme/jws_test.py acme/messages.py acme/messages_test.py acme/other.py acme/other_test.py acme/standalone.py acme/standalone_test.py acme/test_util.py acme/util.py acme/util_test.py acme.egg-info/PKG-INFO acme.egg-info/SOURCES.txt acme.egg-info/dependency_links.txt acme.egg-info/entry_points.txt acme.egg-info/requires.txt acme.egg-info/top_level.txt acme/jose/__init__.py acme/jose/b64.py acme/jose/b64_test.py acme/jose/errors.py acme/jose/errors_test.py acme/jose/interfaces.py acme/jose/interfaces_test.py acme/jose/json_util.py acme/jose/json_util_test.py acme/jose/jwa.py acme/jose/jwa_test.py acme/jose/jwk.py acme/jose/jwk_test.py acme/jose/jws.py acme/jose/jws_test.py acme/jose/util.py acme/jose/util_test.py acme/testdata/README acme/testdata/cert-100sans.pem acme/testdata/cert-idnsans.pem acme/testdata/cert-san.pem acme/testdata/cert.der acme/testdata/cert.pem acme/testdata/csr-100sans.pem acme/testdata/csr-6sans.pem acme/testdata/csr-idnsans.pem acme/testdata/csr-nosans.pem acme/testdata/csr-san.pem acme/testdata/csr.der acme/testdata/csr.pem acme/testdata/dsa512_key.pem acme/testdata/rsa1024_key.pem acme/testdata/rsa2048_key.pem acme/testdata/rsa256_key.pem acme/testdata/rsa512_key.pem docs/.gitignore docs/Makefile docs/api.rst docs/conf.py docs/index.rst docs/make.bat docs/_static/.gitignore docs/_templates/.gitignore docs/api/challenges.rst docs/api/client.rst docs/api/errors.rst docs/api/fields.rst docs/api/jose.rst docs/api/messages.rst docs/api/other.rst docs/api/standalone.rst docs/api/jose/base64.rst docs/api/jose/errors.rst docs/api/jose/interfaces.rst docs/api/jose/json_util.rst docs/api/jose/jwa.rst docs/api/jose/jwk.rst docs/api/jose/jws.rst docs/api/jose/util.rst docs/man/jws.rst examples/example_client.py examples/standalone/README examples/standalone/localhost/cert.pem examples/standalone/localhost/key.pemacme-0.4.1/acme/0000755000175000017500000000000012665157732012740 5ustar bmwbmw00000000000000acme-0.4.1/acme/crypto_util.py0000644000175000017500000001773412665157707015705 0ustar bmwbmw00000000000000"""Crypto utilities.""" import contextlib import logging import re import socket import sys import OpenSSL from acme import errors logger = logging.getLogger(__name__) # TLSSNI01 certificate serving and probing is not affected by SSL # vulnerabilities: prober needs to check certificate for expected # contents anyway. Working SNI is the only thing that's necessary for # the challenge and thus scoping down SSL/TLS method (version) would # cause interoperability issues: TLSv1_METHOD is only compatible with # TLSv1_METHOD, while SSLv23_METHOD is compatible with all other # methods, including TLSv2_METHOD (read more at # https://www.openssl.org/docs/ssl/SSLv23_method.html). _serve_sni # should be changed to use "set_options" to disable SSLv2 and SSLv3, # in case it's used for things other than probing/serving! _DEFAULT_TLSSNI01_SSL_METHOD = OpenSSL.SSL.SSLv23_METHOD class SSLSocket(object): # pylint: disable=too-few-public-methods """SSL wrapper for sockets. :ivar socket sock: Original wrapped socket. :ivar dict certs: Mapping from domain names (`bytes`) to `OpenSSL.crypto.X509`. :ivar method: See `OpenSSL.SSL.Context` for allowed values. """ def __init__(self, sock, certs, method=_DEFAULT_TLSSNI01_SSL_METHOD): self.sock = sock self.certs = certs self.method = method def __getattr__(self, name): return getattr(self.sock, name) def _pick_certificate_cb(self, connection): """SNI certificate callback. This method will set a new OpenSSL context object for this connection when an incoming connection provides an SNI name (in order to serve the appropriate certificate, if any). :param connection: The TLS connection object on which the SNI extension was received. :type connection: :class:`OpenSSL.Connection` """ server_name = connection.get_servername() try: key, cert = self.certs[server_name] except KeyError: logger.debug("Server name (%s) not recognized, dropping SSL", server_name) return new_context = OpenSSL.SSL.Context(self.method) new_context.use_privatekey(key) new_context.use_certificate(cert) connection.set_context(new_context) class FakeConnection(object): """Fake OpenSSL.SSL.Connection.""" # pylint: disable=too-few-public-methods,missing-docstring def __init__(self, connection): self._wrapped = connection def __getattr__(self, name): return getattr(self._wrapped, name) def shutdown(self, *unused_args): # OpenSSL.SSL.Connection.shutdown doesn't accept any args return self._wrapped.shutdown() def accept(self): # pylint: disable=missing-docstring sock, addr = self.sock.accept() context = OpenSSL.SSL.Context(self.method) context.set_tlsext_servername_callback(self._pick_certificate_cb) ssl_sock = self.FakeConnection(OpenSSL.SSL.Connection(context, sock)) ssl_sock.set_accept_state() logger.debug("Performing handshake with %s", addr) try: ssl_sock.do_handshake() except OpenSSL.SSL.Error as error: # _pick_certificate_cb might have returned without # creating SSL context (wrong server name) raise socket.error(error) return ssl_sock, addr def probe_sni(name, host, port=443, timeout=300, method=_DEFAULT_TLSSNI01_SSL_METHOD, source_address=('0', 0)): """Probe SNI server for SSL certificate. :param bytes name: Byte string to send as the server name in the client hello message. :param bytes host: Host to connect to. :param int port: Port to connect to. :param int timeout: Timeout in seconds. :param method: See `OpenSSL.SSL.Context` for allowed values. :param tuple source_address: Enables multi-path probing (selection of source interface). See `socket.creation_connection` for more info. Available only in Python 2.7+. :raises acme.errors.Error: In case of any problems. :returns: SSL certificate presented by the server. :rtype: OpenSSL.crypto.X509 """ context = OpenSSL.SSL.Context(method) context.set_timeout(timeout) socket_kwargs = {} if sys.version_info < (2, 7) else { 'source_address': source_address} try: # pylint: disable=star-args sock = socket.create_connection((host, port), **socket_kwargs) except socket.error as error: raise errors.Error(error) with contextlib.closing(sock) as client: client_ssl = OpenSSL.SSL.Connection(context, client) client_ssl.set_connect_state() client_ssl.set_tlsext_host_name(name) # pyOpenSSL>=0.13 try: client_ssl.do_handshake() client_ssl.shutdown() except OpenSSL.SSL.Error as error: raise errors.Error(error) return client_ssl.get_peer_certificate() def _pyopenssl_cert_or_req_san(cert_or_req): """Get Subject Alternative Names from certificate or CSR using pyOpenSSL. .. todo:: Implement directly in PyOpenSSL! .. note:: Although this is `acme` internal API, it is used by `letsencrypt`. :param cert_or_req: Certificate or CSR. :type cert_or_req: `OpenSSL.crypto.X509` or `OpenSSL.crypto.X509Req`. :returns: A list of Subject Alternative Names. :rtype: `list` of `unicode` """ # This function finds SANs by dumping the certificate/CSR to text and # searching for "X509v3 Subject Alternative Name" in the text. This method # is used to support PyOpenSSL version 0.13 where the # `_subjectAltNameString` and `get_extensions` methods are not available # for CSRs. # constants based on PyOpenSSL certificate/CSR text dump part_separator = ":" parts_separator = ", " prefix = "DNS" + part_separator if isinstance(cert_or_req, OpenSSL.crypto.X509): func = OpenSSL.crypto.dump_certificate else: func = OpenSSL.crypto.dump_certificate_request text = func(OpenSSL.crypto.FILETYPE_TEXT, cert_or_req).decode("utf-8") # WARNING: this function does not support multiple SANs extensions. # Multiple X509v3 extensions of the same type is disallowed by RFC 5280. match = re.search(r"X509v3 Subject Alternative Name:\s*(.*)", text) # WARNING: this function assumes that no SAN can include # parts_separator, hence the split! sans_parts = [] if match is None else match.group(1).split(parts_separator) return [part.split(part_separator)[1] for part in sans_parts if part.startswith(prefix)] def gen_ss_cert(key, domains, not_before=None, validity=(7 * 24 * 60 * 60), force_san=True): """Generate new self-signed certificate. :type domains: `list` of `unicode` :param OpenSSL.crypto.PKey key: :param bool force_san: If more than one domain is provided, all of the domains are put into ``subjectAltName`` X.509 extension and first domain is set as the subject CN. If only one domain is provided no ``subjectAltName`` extension is used, unless `force_san` is ``True``. """ assert domains, "Must provide one or more hostnames for the cert." cert = OpenSSL.crypto.X509() cert.set_serial_number(1337) cert.set_version(2) extensions = [ OpenSSL.crypto.X509Extension( b"basicConstraints", True, b"CA:TRUE, pathlen:0"), ] cert.get_subject().CN = domains[0] # TODO: what to put into cert.get_subject()? cert.set_issuer(cert.get_subject()) if force_san or len(domains) > 1: extensions.append(OpenSSL.crypto.X509Extension( b"subjectAltName", critical=False, value=b", ".join(b"DNS:" + d.encode() for d in domains) )) cert.add_extensions(extensions) cert.gmtime_adj_notBefore(0 if not_before is None else not_before) cert.gmtime_adj_notAfter(validity) cert.set_pubkey(key) cert.sign(key, "sha256") return cert acme-0.4.1/acme/jws.py0000644000175000017500000000254412665157707014124 0ustar bmwbmw00000000000000"""ACME JOSE JWS.""" from acme import jose class Header(jose.Header): """ACME JOSE Header. .. todo:: Implement ``acmePath``. """ nonce = jose.Field('nonce', omitempty=True, encoder=jose.encode_b64jose) @nonce.decoder def nonce(value): # pylint: disable=missing-docstring,no-self-argument try: return jose.decode_b64jose(value) except jose.DeserializationError as error: # TODO: custom error raise jose.DeserializationError("Invalid nonce: {0}".format(error)) class Signature(jose.Signature): """ACME Signature.""" __slots__ = jose.Signature._orig_slots # pylint: disable=no-member # TODO: decoder/encoder should accept cls? Otherwise, subclassing # JSONObjectWithFields is tricky... header_cls = Header header = jose.Field( 'header', omitempty=True, default=header_cls(), decoder=header_cls.from_json) # TODO: decoder should check that nonce is in the protected header class JWS(jose.JWS): """ACME JWS.""" signature_cls = Signature __slots__ = jose.JWS._orig_slots # pylint: disable=no-member @classmethod def sign(cls, payload, key, alg, nonce): # pylint: disable=arguments-differ return super(JWS, cls).sign(payload, key=key, alg=alg, protect=frozenset(['nonce']), nonce=nonce) acme-0.4.1/acme/jws_test.py0000644000175000017500000000263512665157707015164 0ustar bmwbmw00000000000000"""Tests for acme.jws.""" import unittest from acme import jose from acme import test_util KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) class HeaderTest(unittest.TestCase): """Tests for acme.jws.Header.""" good_nonce = jose.encode_b64jose(b'foo') wrong_nonce = u'F' # Following just makes sure wrong_nonce is wrong try: jose.b64decode(wrong_nonce) except (ValueError, TypeError): assert True else: assert False # pragma: no cover def test_nonce_decoder(self): from acme.jws import Header nonce_field = Header._fields['nonce'] self.assertRaises( jose.DeserializationError, nonce_field.decode, self.wrong_nonce) self.assertEqual(b'foo', nonce_field.decode(self.good_nonce)) class JWSTest(unittest.TestCase): """Tests for acme.jws.JWS.""" def setUp(self): self.privkey = KEY self.pubkey = self.privkey.public_key() self.nonce = jose.b64encode(b'Nonce') def test_it(self): from acme.jws import JWS jws = JWS.sign(payload=b'foo', key=self.privkey, alg=jose.RS256, nonce=self.nonce) self.assertEqual(jws.signature.combined.nonce, self.nonce) # TODO: check that nonce is in protected header self.assertEqual(jws, JWS.from_json(jws.to_json())) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/client.py0000644000175000017500000006001012665157707014567 0ustar bmwbmw00000000000000"""ACME client API.""" import collections import datetime from email.utils import parsedate_tz import heapq import logging import time import six from six.moves import http_client # pylint: disable=import-error import OpenSSL import requests import sys from acme import errors from acme import jose from acme import jws from acme import messages logger = logging.getLogger(__name__) # Prior to Python 2.7.9 the stdlib SSL module did not allow a user to configure # many important security related options. On these platforms we use PyOpenSSL # for SSL, which does allow these options to be configured. # https://urllib3.readthedocs.org/en/latest/security.html#insecureplatformwarning if sys.version_info < (2, 7, 9): # pragma: no cover requests.packages.urllib3.contrib.pyopenssl.inject_into_urllib3() class Client(object): # pylint: disable=too-many-instance-attributes """ACME client. .. todo:: Clean up raised error types hierarchy, document, and handle (wrap) instances of `.DeserializationError` raised in `from_json()`. :ivar messages.Directory directory: :ivar key: `.JWK` (private) :ivar alg: `.JWASignature` :ivar bool verify_ssl: Verify SSL certificates? :ivar .ClientNetwork net: Client network. Useful for testing. If not supplied, it will be initialized using `key`, `alg` and `verify_ssl`. """ DER_CONTENT_TYPE = 'application/pkix-cert' def __init__(self, directory, key, alg=jose.RS256, verify_ssl=True, net=None): """Initialize. :param directory: Directory Resource (`.messages.Directory`) or URI from which the resource will be downloaded. """ self.key = key self.net = ClientNetwork(key, alg, verify_ssl) if net is None else net if isinstance(directory, six.string_types): self.directory = messages.Directory.from_json( self.net.get(directory).json()) else: self.directory = directory @classmethod def _regr_from_response(cls, response, uri=None, new_authzr_uri=None, terms_of_service=None): if 'terms-of-service' in response.links: terms_of_service = response.links['terms-of-service']['url'] if 'next' in response.links: new_authzr_uri = response.links['next']['url'] if new_authzr_uri is None: raise errors.ClientError('"next" link missing') return messages.RegistrationResource( body=messages.Registration.from_json(response.json()), uri=response.headers.get('Location', uri), new_authzr_uri=new_authzr_uri, terms_of_service=terms_of_service) def register(self, new_reg=None): """Register. :param .NewRegistration new_reg: :returns: Registration Resource. :rtype: `.RegistrationResource` :raises .UnexpectedUpdate: """ new_reg = messages.NewRegistration() if new_reg is None else new_reg assert isinstance(new_reg, messages.NewRegistration) response = self.net.post(self.directory[new_reg], new_reg) # TODO: handle errors assert response.status_code == http_client.CREATED # "Instance of 'Field' has no key/contact member" bug: # pylint: disable=no-member regr = self._regr_from_response(response) if (regr.body.key != self.key.public_key() or regr.body.contact != new_reg.contact): raise errors.UnexpectedUpdate(regr) return regr def _send_recv_regr(self, regr, body): response = self.net.post(regr.uri, body) # TODO: Boulder returns httplib.ACCEPTED #assert response.status_code == httplib.OK # TODO: Boulder does not set Location or Link on update # (c.f. acme-spec #94) return self._regr_from_response( response, uri=regr.uri, new_authzr_uri=regr.new_authzr_uri, terms_of_service=regr.terms_of_service) def update_registration(self, regr, update=None): """Update registration. :param messages.RegistrationResource regr: Registration Resource. :param messages.Registration update: Updated body of the resource. If not provided, body will be taken from `regr`. :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ update = regr.body if update is None else update updated_regr = self._send_recv_regr( regr, body=messages.UpdateRegistration(**dict(update))) if updated_regr != regr: raise errors.UnexpectedUpdate(regr) return updated_regr def query_registration(self, regr): """Query server about registration. :param messages.RegistrationResource: Existing Registration Resource. """ return self._send_recv_regr(regr, messages.UpdateRegistration()) def agree_to_tos(self, regr): """Agree to the terms-of-service. Agree to the terms-of-service in a Registration Resource. :param regr: Registration Resource. :type regr: `.RegistrationResource` :returns: Updated Registration Resource. :rtype: `.RegistrationResource` """ return self.update_registration( regr.update(body=regr.body.update(agreement=regr.terms_of_service))) def _authzr_from_response(self, response, identifier, uri=None, new_cert_uri=None): # pylint: disable=no-self-use if new_cert_uri is None: try: new_cert_uri = response.links['next']['url'] except KeyError: raise errors.ClientError('"next" link missing') authzr = messages.AuthorizationResource( body=messages.Authorization.from_json(response.json()), uri=response.headers.get('Location', uri), new_cert_uri=new_cert_uri) if authzr.body.identifier != identifier: raise errors.UnexpectedUpdate(authzr) return authzr def request_challenges(self, identifier, new_authzr_uri=None): """Request challenges. :param .messages.Identifier identifier: Identifier to be challenged. :param str new_authzr_uri: ``new-authorization`` URI. If omitted, will default to value found in ``directory``. :returns: Authorization Resource. :rtype: `.AuthorizationResource` """ new_authz = messages.NewAuthorization(identifier=identifier) response = self.net.post(self.directory.new_authz if new_authzr_uri is None else new_authzr_uri, new_authz) # TODO: handle errors assert response.status_code == http_client.CREATED return self._authzr_from_response(response, identifier) def request_domain_challenges(self, domain, new_authzr_uri=None): """Request challenges for domain names. This is simply a convenience function that wraps around `request_challenges`, but works with domain names instead of generic identifiers. See ``request_challenges`` for more documentation. :param str domain: Domain name to be challenged. :returns: Authorization Resource. :rtype: `.AuthorizationResource` """ return self.request_challenges(messages.Identifier( typ=messages.IDENTIFIER_FQDN, value=domain), new_authzr_uri) def answer_challenge(self, challb, response): """Answer challenge. :param challb: Challenge Resource body. :type challb: `.ChallengeBody` :param response: Corresponding Challenge response :type response: `.challenges.ChallengeResponse` :returns: Challenge Resource with updated body. :rtype: `.ChallengeResource` :raises .UnexpectedUpdate: """ response = self.net.post(challb.uri, response) try: authzr_uri = response.links['up']['url'] except KeyError: raise errors.ClientError('"up" Link header missing') challr = messages.ChallengeResource( authzr_uri=authzr_uri, body=messages.ChallengeBody.from_json(response.json())) # TODO: check that challr.uri == response.headers['Location']? if challr.uri != challb.uri: raise errors.UnexpectedUpdate(challr.uri) return challr @classmethod def retry_after(cls, response, default): """Compute next `poll` time based on response ``Retry-After`` header. Handles integers and various datestring formats per https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.37 :param requests.Response response: Response from `poll`. :param int default: Default value (in seconds), used when ``Retry-After`` header is not present or invalid. :returns: Time point when next `poll` should be performed. :rtype: `datetime.datetime` """ retry_after = response.headers.get('Retry-After', str(default)) try: seconds = int(retry_after) except ValueError: # The RFC 2822 parser handles all of RFC 2616's cases in modern # environments (primarily HTTP 1.1+ but also py27+) when = parsedate_tz(retry_after) if when is not None: try: tz_secs = datetime.timedelta(when[-1] if when[-1] else 0) return datetime.datetime(*when[:7]) - tz_secs except (ValueError, OverflowError): pass seconds = default return datetime.datetime.now() + datetime.timedelta(seconds=seconds) def poll(self, authzr): """Poll Authorization Resource for status. :param authzr: Authorization Resource :type authzr: `.AuthorizationResource` :returns: Updated Authorization Resource and HTTP response. :rtype: (`.AuthorizationResource`, `requests.Response`) """ response = self.net.get(authzr.uri) updated_authzr = self._authzr_from_response( response, authzr.body.identifier, authzr.uri, authzr.new_cert_uri) # TODO: check and raise UnexpectedUpdate return updated_authzr, response def request_issuance(self, csr, authzrs): """Request issuance. :param csr: CSR :type csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` :param authzrs: `list` of `.AuthorizationResource` :returns: Issued certificate :rtype: `.messages.CertificateResource` """ assert authzrs, "Authorizations list is empty" logger.debug("Requesting issuance...") # TODO: assert len(authzrs) == number of SANs req = messages.CertificateRequest(csr=csr) content_type = self.DER_CONTENT_TYPE # TODO: add 'cert_type 'argument response = self.net.post( authzrs[0].new_cert_uri, # TODO: acme-spec #90 req, content_type=content_type, headers={'Accept': content_type}) cert_chain_uri = response.links.get('up', {}).get('url') try: uri = response.headers['Location'] except KeyError: raise errors.ClientError('"Location" Header missing') return messages.CertificateResource( uri=uri, authzrs=authzrs, cert_chain_uri=cert_chain_uri, body=jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content))) def poll_and_request_issuance( self, csr, authzrs, mintime=5, max_attempts=10): """Poll and request issuance. This function polls all provided Authorization Resource URIs until all challenges are valid, respecting ``Retry-After`` HTTP headers, and then calls `request_issuance`. :param .ComparableX509 csr: CSR (`OpenSSL.crypto.X509Req` wrapped in `.ComparableX509`) :param authzrs: `list` of `.AuthorizationResource` :param int mintime: Minimum time before next attempt, used if ``Retry-After`` is not present in the response. :param int max_attempts: Maximum number of attempts (per authorization) before `PollError` with non-empty ``waiting`` is raised. :returns: ``(cert, updated_authzrs)`` `tuple` where ``cert`` is the issued certificate (`.messages.CertificateResource`), and ``updated_authzrs`` is a `tuple` consisting of updated Authorization Resources (`.AuthorizationResource`) as present in the responses from server, and in the same order as the input ``authzrs``. :rtype: `tuple` :raises PollError: in case of timeout or if some authorization was marked by the CA as invalid """ # pylint: disable=too-many-locals assert max_attempts > 0 attempts = collections.defaultdict(int) exhausted = set() # priority queue with datetime.datetime (based on Retry-After) as key, # and original Authorization Resource as value waiting = [(datetime.datetime.now(), authzr) for authzr in authzrs] # mapping between original Authorization Resource and the most # recently updated one updated = dict((authzr, authzr) for authzr in authzrs) while waiting: # find the smallest Retry-After, and sleep if necessary when, authzr = heapq.heappop(waiting) now = datetime.datetime.now() if when > now: seconds = (when - now).seconds logger.debug('Sleeping for %d seconds', seconds) time.sleep(seconds) # Note that we poll with the latest updated Authorization # URI, which might have a different URI than initial one updated_authzr, response = self.poll(updated[authzr]) updated[authzr] = updated_authzr attempts[authzr] += 1 # pylint: disable=no-member if updated_authzr.body.status not in ( messages.STATUS_VALID, messages.STATUS_INVALID): if attempts[authzr] < max_attempts: # push back to the priority queue, with updated retry_after heapq.heappush(waiting, (self.retry_after( response, default=mintime), authzr)) else: exhausted.add(authzr) if exhausted or any(authzr.body.status == messages.STATUS_INVALID for authzr in six.itervalues(updated)): raise errors.PollError(exhausted, updated) updated_authzrs = tuple(updated[authzr] for authzr in authzrs) return self.request_issuance(csr, updated_authzrs), updated_authzrs def _get_cert(self, uri): """Returns certificate from URI. :param str uri: URI of certificate :returns: tuple of the form (response, :class:`acme.jose.ComparableX509`) :rtype: tuple """ content_type = self.DER_CONTENT_TYPE # TODO: make it a param response = self.net.get(uri, headers={'Accept': content_type}, content_type=content_type) return response, jose.ComparableX509(OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_ASN1, response.content)) def check_cert(self, certr): """Check for new cert. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: acme-spec 5.1 table action should be renamed to # "refresh cert", and this method integrated with self.refresh response, cert = self._get_cert(certr.uri) if 'Location' not in response.headers: raise errors.ClientError('Location header missing') if response.headers['Location'] != certr.uri: raise errors.UnexpectedUpdate(response.text) return certr.update(body=cert) def refresh(self, certr): """Refresh certificate. :param certr: Certificate Resource :type certr: `.CertificateResource` :returns: Updated Certificate Resource. :rtype: `.CertificateResource` """ # TODO: If a client sends a refresh request and the server is # not willing to refresh the certificate, the server MUST # respond with status code 403 (Forbidden) return self.check_cert(certr) def fetch_chain(self, certr, max_length=10): """Fetch chain for certificate. :param .CertificateResource certr: Certificate Resource :param int max_length: Maximum allowed length of the chain. Note that each element in the certificate requires new ``HTTP GET`` request, and the length of the chain is controlled by the ACME CA. :raises errors.Error: if recursion exceeds `max_length` :returns: Certificate chain for the Certificate Resource. It is a list ordered so that the first element is a signer of the certificate from Certificate Resource. Will be empty if ``cert_chain_uri`` is ``None``. :rtype: `list` of `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ chain = [] uri = certr.cert_chain_uri while uri is not None and len(chain) < max_length: response, cert = self._get_cert(uri) uri = response.links.get('up', {}).get('url') chain.append(cert) if uri is not None: raise errors.Error( "Recursion limit reached. Didn't get {0}".format(uri)) return chain def revoke(self, cert): """Revoke certificate. :param .ComparableX509 cert: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` :raises .ClientError: If revocation is unsuccessful. """ response = self.net.post(self.directory[messages.Revocation], messages.Revocation(certificate=cert), content_type=None) if response.status_code != http_client.OK: raise errors.ClientError( 'Successful revocation must return HTTP OK status') class ClientNetwork(object): # pylint: disable=too-many-instance-attributes """Client network.""" JSON_CONTENT_TYPE = 'application/json' JSON_ERROR_CONTENT_TYPE = 'application/problem+json' REPLAY_NONCE_HEADER = 'Replay-Nonce' def __init__(self, key, alg=jose.RS256, verify_ssl=True, user_agent='acme-python'): self.key = key self.alg = alg self.verify_ssl = verify_ssl self._nonces = set() self.user_agent = user_agent def _wrap_in_jws(self, obj, nonce): """Wrap `JSONDeSerializable` object in JWS. .. todo:: Implement ``acmePath``. :param .JSONDeSerializable obj: :param bytes nonce: :rtype: `.JWS` """ jobj = obj.json_dumps().encode() logger.debug('Serialized JSON: %s', jobj) return jws.JWS.sign( payload=jobj, key=self.key, alg=self.alg, nonce=nonce).json_dumps() @classmethod def _check_response(cls, response, content_type=None): """Check response content and its type. .. note:: Checking is not strict: wrong server response ``Content-Type`` HTTP header is ignored if response is an expected JSON object (c.f. Boulder #56). :param str content_type: Expected Content-Type response header. If JSON is expected and not present in server response, this function will raise an error. Otherwise, wrong Content-Type is ignored, but logged. :raises .messages.Error: If server response body carries HTTP Problem (draft-ietf-appsawg-http-problem-00). :raises .ClientError: In case of other networking errors. """ logger.debug('Received response %s (headers: %s): %r', response, response.headers, response.content) response_ct = response.headers.get('Content-Type') try: # TODO: response.json() is called twice, once here, and # once in _get and _post clients jobj = response.json() except ValueError: jobj = None if not response.ok: if jobj is not None: if response_ct != cls.JSON_ERROR_CONTENT_TYPE: logger.debug( 'Ignoring wrong Content-Type (%r) for JSON Error', response_ct) try: raise messages.Error.from_json(jobj) except jose.DeserializationError as error: # Couldn't deserialize JSON object raise errors.ClientError((response, error)) else: # response is not JSON object raise errors.ClientError(response) else: if jobj is not None and response_ct != cls.JSON_CONTENT_TYPE: logger.debug( 'Ignoring wrong Content-Type (%r) for JSON decodable ' 'response', response_ct) if content_type == cls.JSON_CONTENT_TYPE and jobj is None: raise errors.ClientError( 'Unexpected response Content-Type: {0}'.format(response_ct)) return response def _send_request(self, method, url, *args, **kwargs): """Send HTTP request. Makes sure that `verify_ssl` is respected. Logs request and response (with headers). For allowed parameters please see `requests.request`. :param str method: method for the new `requests.Request` object :param str url: URL for the new `requests.Request` object :raises requests.exceptions.RequestException: in case of any problems :returns: HTTP Response :rtype: `requests.Response` """ logging.debug('Sending %s request to %s. args: %r, kwargs: %r', method, url, args, kwargs) kwargs['verify'] = self.verify_ssl kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('User-Agent', self.user_agent) response = requests.request(method, url, *args, **kwargs) logging.debug('Received %s. Headers: %s. Content: %r', response, response.headers, response.content) return response def head(self, *args, **kwargs): """Send HEAD request without checking the response. Note, that `_check_response` is not called, as it is expected that status code other than successfully 2xx will be returned, or messages2.Error will be raised by the server. """ return self._send_request('HEAD', *args, **kwargs) def get(self, url, content_type=JSON_CONTENT_TYPE, **kwargs): """Send GET request and check response.""" return self._check_response( self._send_request('GET', url, **kwargs), content_type=content_type) def _add_nonce(self, response): if self.REPLAY_NONCE_HEADER in response.headers: nonce = response.headers[self.REPLAY_NONCE_HEADER] try: decoded_nonce = jws.Header._fields['nonce'].decode(nonce) except jose.DeserializationError as error: raise errors.BadNonce(nonce, error) logger.debug('Storing nonce: %r', decoded_nonce) self._nonces.add(decoded_nonce) else: raise errors.MissingNonce(response) def _get_nonce(self, url): if not self._nonces: logging.debug('Requesting fresh nonce') self._add_nonce(self.head(url)) return self._nonces.pop() def post(self, url, obj, content_type=JSON_CONTENT_TYPE, **kwargs): """POST object wrapped in `.JWS` and check response.""" data = self._wrap_in_jws(obj, self._get_nonce(url)) response = self._send_request('POST', url, data=data, **kwargs) self._add_nonce(response) return self._check_response(response, content_type=content_type) acme-0.4.1/acme/messages.py0000644000175000017500000003256012665157707015131 0ustar bmwbmw00000000000000"""ACME protocol messages.""" import collections from acme import challenges from acme import errors from acme import fields from acme import jose from acme import util class Error(jose.JSONObjectWithFields, errors.Error): """ACME error. https://tools.ietf.org/html/draft-ietf-appsawg-http-problem-00 :ivar unicode typ: :ivar unicode title: :ivar unicode detail: """ ERROR_TYPE_DESCRIPTIONS = dict( ('urn:acme:error:' + name, description) for name, description in ( ('badCSR', 'The CSR is unacceptable (e.g., due to a short key)'), ('badNonce', 'The client sent an unacceptable anti-replay nonce'), ('connection', 'The server could not connect to the client to ' 'verify the domain'), ('dnssec', 'The server could not validate a DNSSEC signed domain'), ('invalidEmail', 'The provided email for a registration was invalid'), ('malformed', 'The request message was malformed'), ('rateLimited', 'There were too many requests of a given type'), ('serverInternal', 'The server experienced an internal error'), ('tls', 'The server experienced a TLS error during domain ' 'verification'), ('unauthorized', 'The client lacks sufficient authorization'), ('unknownHost', 'The server could not resolve a domain name'), ) ) typ = jose.Field('type') title = jose.Field('title', omitempty=True) detail = jose.Field('detail') @property def description(self): """Hardcoded error description based on its type. :returns: Description if standard ACME error or ``None``. :rtype: unicode """ return self.ERROR_TYPE_DESCRIPTIONS.get(self.typ) def __str__(self): return ' :: '.join( part for part in (self.typ, self.description, self.detail, self.title) if part is not None) class _Constant(jose.JSONDeSerializable, collections.Hashable): """ACME constant.""" __slots__ = ('name',) POSSIBLE_NAMES = NotImplemented def __init__(self, name): self.POSSIBLE_NAMES[name] = self self.name = name def to_partial_json(self): return self.name @classmethod def from_json(cls, value): if value not in cls.POSSIBLE_NAMES: raise jose.DeserializationError( '{0} not recognized'.format(cls.__name__)) return cls.POSSIBLE_NAMES[value] def __repr__(self): return '{0}({1})'.format(self.__class__.__name__, self.name) def __eq__(self, other): return isinstance(other, type(self)) and other.name == self.name def __hash__(self): return hash((self.__class__, self.name)) def __ne__(self, other): return not self == other class Status(_Constant): """ACME "status" field.""" POSSIBLE_NAMES = {} STATUS_UNKNOWN = Status('unknown') STATUS_PENDING = Status('pending') STATUS_PROCESSING = Status('processing') STATUS_VALID = Status('valid') STATUS_INVALID = Status('invalid') STATUS_REVOKED = Status('revoked') class IdentifierType(_Constant): """ACME identifier type.""" POSSIBLE_NAMES = {} IDENTIFIER_FQDN = IdentifierType('dns') # IdentifierDNS in Boulder class Identifier(jose.JSONObjectWithFields): """ACME identifier. :ivar IdentifierType typ: :ivar unicode value: """ typ = jose.Field('type', decoder=IdentifierType.from_json) value = jose.Field('value') class Directory(jose.JSONDeSerializable): """Directory.""" _REGISTERED_TYPES = {} @classmethod def _canon_key(cls, key): return getattr(key, 'resource_type', key) @classmethod def register(cls, resource_body_cls): """Register resource.""" resource_type = resource_body_cls.resource_type assert resource_type not in cls._REGISTERED_TYPES cls._REGISTERED_TYPES[resource_type] = resource_body_cls return resource_body_cls def __init__(self, jobj): canon_jobj = util.map_keys(jobj, self._canon_key) if not set(canon_jobj).issubset(self._REGISTERED_TYPES): # TODO: acme-spec is not clear about this: 'It is a JSON # dictionary, whose keys are the "resource" values listed # in {{https-requests}}'z raise ValueError('Wrong directory fields') # TODO: check that everything is an absolute URL; acme-spec is # not clear on that self._jobj = canon_jobj def __getattr__(self, name): try: return self[name.replace('_', '-')] except KeyError as error: raise AttributeError(str(error)) def __getitem__(self, name): try: return self._jobj[self._canon_key(name)] except KeyError: raise KeyError('Directory field not found') def to_partial_json(self): return self._jobj @classmethod def from_json(cls, jobj): try: return cls(jobj) except ValueError as error: raise jose.DeserializationError(str(error)) class Resource(jose.JSONObjectWithFields): """ACME Resource. :ivar acme.messages.ResourceBody body: Resource body. """ body = jose.Field('body') class ResourceWithURI(Resource): """ACME Resource with URI. :ivar unicode uri: Location of the resource. """ uri = jose.Field('uri') # no ChallengeResource.uri class ResourceBody(jose.JSONObjectWithFields): """ACME Resource Body.""" class Registration(ResourceBody): """Registration Resource Body. :ivar acme.jose.jwk.JWK key: Public key. :ivar tuple contact: Contact information following ACME spec, `tuple` of `unicode`. :ivar unicode agreement: :ivar unicode authorizations: URI where `messages.Registration.Authorizations` can be found. :ivar unicode certificates: URI where `messages.Registration.Certificates` can be found. """ # on new-reg key server ignores 'key' and populates it based on # JWS.signature.combined.jwk key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) contact = jose.Field('contact', omitempty=True, default=()) agreement = jose.Field('agreement', omitempty=True) authorizations = jose.Field('authorizations', omitempty=True) certificates = jose.Field('certificates', omitempty=True) class Authorizations(jose.JSONObjectWithFields): """Authorizations granted to Account in the process of registration. :ivar tuple authorizations: URIs to Authorization Resources. """ authorizations = jose.Field('authorizations') class Certificates(jose.JSONObjectWithFields): """Certificates granted to Account in the process of registration. :ivar tuple certificates: URIs to Certificate Resources. """ certificates = jose.Field('certificates') phone_prefix = 'tel:' email_prefix = 'mailto:' @classmethod def from_data(cls, phone=None, email=None, **kwargs): """Create registration resource from contact details.""" details = list(kwargs.pop('contact', ())) if phone is not None: details.append(cls.phone_prefix + phone) if email is not None: details.append(cls.email_prefix + email) kwargs['contact'] = tuple(details) return cls(**kwargs) def _filter_contact(self, prefix): return tuple( detail[len(prefix):] for detail in self.contact if detail.startswith(prefix)) @property def phones(self): """All phones found in the ``contact`` field.""" return self._filter_contact(self.phone_prefix) @property def emails(self): """All emails found in the ``contact`` field.""" return self._filter_contact(self.email_prefix) @Directory.register class NewRegistration(Registration): """New registration.""" resource_type = 'new-reg' resource = fields.Resource(resource_type) class UpdateRegistration(Registration): """Update registration.""" resource_type = 'reg' resource = fields.Resource(resource_type) class RegistrationResource(ResourceWithURI): """Registration Resource. :ivar acme.messages.Registration body: :ivar unicode new_authzr_uri: URI found in the 'next' ``Link`` header :ivar unicode terms_of_service: URL for the CA TOS. """ body = jose.Field('body', decoder=Registration.from_json) new_authzr_uri = jose.Field('new_authzr_uri') terms_of_service = jose.Field('terms_of_service', omitempty=True) class ChallengeBody(ResourceBody): """Challenge Resource Body. .. todo:: Confusingly, this has a similar name to `.challenges.Challenge`, as well as `.achallenges.AnnotatedChallenge`. Please use names such as ``challb`` to distinguish instances of this class from ``achall``. :ivar acme.challenges.Challenge: Wrapped challenge. Conveniently, all challenge fields are proxied, i.e. you can call ``challb.x`` to get ``challb.chall.x`` contents. :ivar acme.messages.Status status: :ivar datetime.datetime validated: :ivar messages.Error error: """ __slots__ = ('chall',) uri = jose.Field('uri') status = jose.Field('status', decoder=Status.from_json, omitempty=True, default=STATUS_PENDING) validated = fields.RFC3339Field('validated', omitempty=True) error = jose.Field('error', decoder=Error.from_json, omitempty=True, default=None) def to_partial_json(self): jobj = super(ChallengeBody, self).to_partial_json() jobj.update(self.chall.to_partial_json()) return jobj @classmethod def fields_from_json(cls, jobj): jobj_fields = super(ChallengeBody, cls).fields_from_json(jobj) jobj_fields['chall'] = challenges.Challenge.from_json(jobj) return jobj_fields def __getattr__(self, name): return getattr(self.chall, name) class ChallengeResource(Resource): """Challenge Resource. :ivar acme.messages.ChallengeBody body: :ivar unicode authzr_uri: URI found in the 'up' ``Link`` header. """ body = jose.Field('body', decoder=ChallengeBody.from_json) authzr_uri = jose.Field('authzr_uri') @property def uri(self): # pylint: disable=missing-docstring,no-self-argument # bug? 'method already defined line None' # pylint: disable=function-redefined return self.body.uri # pylint: disable=no-member class Authorization(ResourceBody): """Authorization Resource Body. :ivar acme.messages.Identifier identifier: :ivar list challenges: `list` of `.ChallengeBody` :ivar tuple combinations: Challenge combinations (`tuple` of `tuple` of `int`, as opposed to `list` of `list` from the spec). :ivar acme.messages.Status status: :ivar datetime.datetime expires: """ identifier = jose.Field('identifier', decoder=Identifier.from_json) challenges = jose.Field('challenges', omitempty=True) combinations = jose.Field('combinations', omitempty=True) status = jose.Field('status', omitempty=True, decoder=Status.from_json) # TODO: 'expires' is allowed for Authorization Resources in # general, but for Key Authorization '[t]he "expires" field MUST # be absent'... then acme-spec gives example with 'expires' # present... That's confusing! expires = fields.RFC3339Field('expires', omitempty=True) @challenges.decoder def challenges(value): # pylint: disable=missing-docstring,no-self-argument return tuple(ChallengeBody.from_json(chall) for chall in value) @property def resolved_combinations(self): """Combinations with challenges instead of indices.""" return tuple(tuple(self.challenges[idx] for idx in combo) for combo in self.combinations) @Directory.register class NewAuthorization(Authorization): """New authorization.""" resource_type = 'new-authz' resource = fields.Resource(resource_type) class AuthorizationResource(ResourceWithURI): """Authorization Resource. :ivar acme.messages.Authorization body: :ivar unicode new_cert_uri: URI found in the 'next' ``Link`` header """ body = jose.Field('body', decoder=Authorization.from_json) new_cert_uri = jose.Field('new_cert_uri') @Directory.register class CertificateRequest(jose.JSONObjectWithFields): """ACME new-cert request. :ivar acme.jose.util.ComparableX509 csr: `OpenSSL.crypto.X509Req` wrapped in `.ComparableX509` """ resource_type = 'new-cert' resource = fields.Resource(resource_type) csr = jose.Field('csr', decoder=jose.decode_csr, encoder=jose.encode_csr) class CertificateResource(ResourceWithURI): """Certificate Resource. :ivar acme.jose.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`. """ cert_chain_uri = jose.Field('cert_chain_uri') authzrs = jose.Field('authzrs') @Directory.register class Revocation(jose.JSONObjectWithFields): """Revocation message. :ivar .ComparableX509 certificate: `OpenSSL.crypto.X509` wrapped in `.ComparableX509` """ resource_type = 'revoke-cert' resource = fields.Resource(resource_type) certificate = jose.Field( 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) acme-0.4.1/acme/errors_test.py0000644000175000017500000000276012665157707015674 0ustar bmwbmw00000000000000"""Tests for acme.errors.""" import unittest import mock class BadNonceTest(unittest.TestCase): """Tests for acme.errors.BadNonce.""" def setUp(self): from acme.errors import BadNonce self.error = BadNonce(nonce="xxx", error="error") def test_str(self): self.assertEqual("Invalid nonce ('xxx'): error", str(self.error)) class MissingNonceTest(unittest.TestCase): """Tests for acme.errors.MissingNonce.""" def setUp(self): from acme.errors import MissingNonce self.response = mock.MagicMock(headers={}) self.response.request.method = 'FOO' self.error = MissingNonce(self.response) def test_str(self): self.assertTrue("FOO" in str(self.error)) self.assertTrue("{}" in str(self.error)) class PollErrorTest(unittest.TestCase): """Tests for acme.errors.PollError.""" def setUp(self): from acme.errors import PollError self.timeout = PollError( exhausted=set([mock.sentinel.AR]), updated={}) self.invalid = PollError(exhausted=set(), updated={ mock.sentinel.AR: mock.sentinel.AR2}) def test_timeout(self): self.assertTrue(self.timeout.timeout) self.assertFalse(self.invalid.timeout) def test_repr(self): self.assertEqual('PollError(exhausted=%s, updated={sentinel.AR: ' 'sentinel.AR2})' % repr(set()), repr(self.invalid)) if __name__ == "__main__": unittest.main() # pragma: no cover acme-0.4.1/acme/jose/0000755000175000017500000000000012665157732013700 5ustar bmwbmw00000000000000acme-0.4.1/acme/jose/jwa.py0000644000175000017500000001216312665157707015040 0ustar bmwbmw00000000000000"""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 from cryptography.hazmat.primitives import hmac from cryptography.hazmat.primitives.asymmetric import padding 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): """JSON Web Signature Algorithm.""" SIGNATURES = {} 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')) acme-0.4.1/acme/jose/jws.py0000644000175000017500000003357312665157707015072 0ustar bmwbmw00000000000000"""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 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 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 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 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: 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 acme-0.4.1/acme/jose/jws_test.py0000644000175000017500000002074712665157707016130 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/jose/jwk_test.py0000644000175000017500000001604712665157707016116 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/jose/errors_test.py0000644000175000017500000000072512665157707016633 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/jose/b64.py0000644000175000017500000000276312665157707014657 0ustar bmwbmw00000000000000"""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))) acme-0.4.1/acme/jose/util_test.py0000644000175000017500000001475212665157707016301 0ustar bmwbmw00000000000000"""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), ''.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( '` 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 `. **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 `) 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 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') acme-0.4.1/acme/jose/json_util_test.py0000644000175000017500000003413312665157707017325 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/jose/errors.py0000644000175000017500000000145712665157707015577 0ustar bmwbmw00000000000000"""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) acme-0.4.1/acme/jose/b64_test.py0000644000175000017500000000444612665157707015716 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/jose/util.py0000644000175000017500000001637212665157707015242 0ustar bmwbmw00000000000000"""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): # 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): # 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())) acme-0.4.1/acme/jose/interfaces_test.py0000644000175000017500000000706712665157707017450 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/jose/json_util.py0000644000175000017500000003721212665157707016267 0ustar bmwbmw00000000000000"""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 cosidered "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)) if omitted: # pylint: disable=star-args logger.debug('Omitted empty fields: %s', ', '.join( '{0!s}={1!r}'.format(*field) for field in omitted)) 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 field 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)) acme-0.4.1/acme/jose/jwk.py0000644000175000017500000002220112665157707015044 0ustar bmwbmw00000000000000"""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 from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec 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 = {} cryptography_key_types = () """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, assymentric 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)) acme-0.4.1/acme/jose/jwa_test.py0000644000175000017500000000676012665157707016105 0ustar bmwbmw00000000000000"""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 acme-0.4.1/acme/other.py0000644000175000017500000000413412665157707014437 0ustar bmwbmw00000000000000"""Other ACME objects.""" import functools import logging import os from acme import jose logger = logging.getLogger(__name__) class Signature(jose.JSONObjectWithFields): """ACME signature. :ivar .JWASignature alg: Signature algorithm. :ivar bytes sig: Signature. :ivar bytes nonce: Nonce. :ivar .JWK jwk: JWK. """ NONCE_SIZE = 16 """Minimum size of nonce in bytes.""" alg = jose.Field('alg', decoder=jose.JWASignature.from_json) sig = jose.Field('sig', encoder=jose.encode_b64jose, decoder=jose.decode_b64jose) nonce = jose.Field( 'nonce', encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=NONCE_SIZE, minimum=True)) jwk = jose.Field('jwk', decoder=jose.JWK.from_json) @classmethod def from_msg(cls, msg, key, nonce=None, nonce_size=None, alg=jose.RS256): """Create signature with nonce prepended to the message. :param bytes msg: Message to be signed. :param key: Key used for signing. :type key: `cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` (optionally wrapped in `.ComparableRSAKey`). :param bytes nonce: Nonce to be used. If None, nonce of ``nonce_size`` will be randomly generated. :param int nonce_size: Size of the automatically generated nonce. Defaults to :const:`NONCE_SIZE`. :param .JWASignature alg: """ nonce_size = cls.NONCE_SIZE if nonce_size is None else nonce_size nonce = os.urandom(nonce_size) if nonce is None else nonce msg_with_nonce = nonce + msg sig = alg.sign(key, nonce + msg) logger.debug('%r signed as %r', msg_with_nonce, sig) return cls(alg=alg, sig=sig, nonce=nonce, jwk=alg.kty(key=key.public_key())) def verify(self, msg): """Verify the signature. :param bytes msg: Message that was used in signing. """ # self.alg is not Field, but JWA | pylint: disable=no-member return self.alg.verify(self.jwk.key, self.nonce + msg, self.sig) acme-0.4.1/acme/util_test.py0000644000175000017500000000071012665157707015326 0ustar bmwbmw00000000000000"""Tests for acme.util.""" import unittest class MapKeysTest(unittest.TestCase): """Tests for acme.util.map_keys.""" def test_it(self): from acme.util import map_keys self.assertEqual({'a': 'b', 'c': 'd'}, map_keys({'a': 'b', 'c': 'd'}, lambda key: key)) self.assertEqual({2: 2, 4: 4}, map_keys({1: 2, 3: 4}, lambda x: x + 1)) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/test_util.py0000644000175000017500000000433312665157707015333 0ustar bmwbmw00000000000000"""Test utilities. .. warning:: This module is not part of the public API. """ import os import pkg_resources from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import serialization import OpenSSL from acme import jose def vector_path(*names): """Path to a test vector.""" return pkg_resources.resource_filename( __name__, os.path.join('testdata', *names)) def load_vector(*names): """Load contents of a test vector.""" # luckily, resource_string opens file in binary mode return pkg_resources.resource_string( __name__, os.path.join('testdata', *names)) def _guess_loader(filename, loader_pem, loader_der): _, ext = os.path.splitext(filename) if ext.lower() == '.pem': return loader_pem elif ext.lower() == '.der': return loader_der else: # pragma: no cover raise ValueError("Loader could not be recognized based on extension") def load_cert(*names): """Load certificate.""" loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_certificate(loader, load_vector(*names)) def load_comparable_cert(*names): """Load ComparableX509 cert.""" return jose.ComparableX509(load_cert(*names)) def load_csr(*names): """Load certificate request.""" loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_certificate_request(loader, load_vector(*names)) def load_comparable_csr(*names): """Load ComparableX509 certificate request.""" return jose.ComparableX509(load_csr(*names)) def load_rsa_private_key(*names): """Load RSA private key.""" loader = _guess_loader(names[-1], serialization.load_pem_private_key, serialization.load_der_private_key) return jose.ComparableRSAKey(loader( load_vector(*names), password=None, backend=default_backend())) def load_pyopenssl_private_key(*names): """Load pyOpenSSL private key.""" loader = _guess_loader( names[-1], OpenSSL.crypto.FILETYPE_PEM, OpenSSL.crypto.FILETYPE_ASN1) return OpenSSL.crypto.load_privatekey(loader, load_vector(*names)) acme-0.4.1/acme/challenges.py0000644000175000017500000004715712665157707015437 0ustar bmwbmw00000000000000"""ACME Identifier Validation Challenges.""" import abc import functools import hashlib import logging import socket from cryptography.hazmat.primitives import hashes import OpenSSL import requests from acme import errors from acme import crypto_util from acme import fields from acme import jose from acme import other logger = logging.getLogger(__name__) # pylint: disable=too-few-public-methods class Challenge(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge.""" TYPES = {} @classmethod def from_json(cls, jobj): try: return super(Challenge, cls).from_json(jobj) except jose.UnrecognizedTypeError as error: logger.debug(error) return UnrecognizedChallenge.from_json(jobj) class ContinuityChallenge(Challenge): # pylint: disable=abstract-method """Client validation challenges.""" class DVChallenge(Challenge): # pylint: disable=abstract-method """Domain validation challenges.""" class ChallengeResponse(jose.TypedJSONObjectWithFields): # _fields_to_partial_json | pylint: disable=abstract-method """ACME challenge response.""" TYPES = {} resource_type = 'challenge' resource = fields.Resource(resource_type) class UnrecognizedChallenge(Challenge): """Unrecognized challenge. ACME specification defines a generic framework for challenges and defines some standard challenges that are implemented in this module. However, other implementations (including peers) might define additional challenge types, which should be ignored if unrecognized. :ivar jobj: Original JSON decoded object. """ def __init__(self, jobj): super(UnrecognizedChallenge, self).__init__() object.__setattr__(self, "jobj", jobj) def to_partial_json(self): # pylint: disable=no-member return self.jobj @classmethod def from_json(cls, jobj): return cls(jobj) class _TokenDVChallenge(DVChallenge): """DV Challenge with token. :ivar bytes token: """ TOKEN_SIZE = 128 / 8 # Based on the entropy value from the spec """Minimum size of the :attr:`token` in bytes.""" # TODO: acme-spec doesn't specify token as base64-encoded value token = jose.Field( "token", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=TOKEN_SIZE, minimum=True)) # XXX: rename to ~token_good_for_url @property def good_token(self): # XXX: @token.decoder """Is `token` good? .. todo:: acme-spec wants "It MUST NOT contain any non-ASCII characters", but it should also warrant that it doesn't contain ".." or "/"... """ # TODO: check that path combined with uri does not go above # URI_ROOT_PATH! return b'..' not in self.token and b'/' not in self.token class KeyAuthorizationChallengeResponse(ChallengeResponse): """Response to Challenges based on Key Authorization. :param unicode key_authorization: """ key_authorization = jose.Field("keyAuthorization") thumbprint_hash_function = hashes.SHA256 def verify(self, chall, account_public_key): """Verify the key authorization. :param KeyAuthorization chall: Challenge that corresponds to this response. :param JWK account_public_key: :return: ``True`` iff verification of the key authorization was successful. :rtype: bool """ parts = self.key_authorization.split('.') # pylint: disable=no-member if len(parts) != 2: logger.debug("Key authorization (%r) is not well formed", self.key_authorization) return False if parts[0] != chall.encode("token"): logger.debug("Mismatching token in key authorization: " "%r instead of %r", parts[0], chall.encode("token")) return False thumbprint = jose.b64encode(account_public_key.thumbprint( hash_function=self.thumbprint_hash_function)).decode() if parts[1] != thumbprint: logger.debug("Mismatching thumbprint in key authorization: " "%r instead of %r", parts[0], thumbprint) return False return True class KeyAuthorizationChallenge(_TokenDVChallenge): # pylint: disable=abstract-class-little-used,too-many-ancestors """Challenge based on Key Authorization. :param response_cls: Subclass of `KeyAuthorizationChallengeResponse` that will be used to generate `response`. """ __metaclass__ = abc.ABCMeta response_cls = NotImplemented thumbprint_hash_function = ( KeyAuthorizationChallengeResponse.thumbprint_hash_function) def key_authorization(self, account_key): """Generate Key Authorization. :param JWK account_key: :rtype unicode: """ return self.encode("token") + "." + jose.b64encode( account_key.thumbprint( hash_function=self.thumbprint_hash_function)).decode() def response(self, account_key): """Generate response to the challenge. :param JWK account_key: :returns: Response (initialized `response_cls`) to the challenge. :rtype: KeyAuthorizationChallengeResponse """ return self.response_cls( key_authorization=self.key_authorization(account_key)) @abc.abstractmethod def validation(self, account_key, **kwargs): """Generate validation for the challenge. Subclasses must implement this method, but they are likely to return completely different data structures, depending on what's necessary to complete the challenge. Interepretation of that return value must be known to the caller. :param JWK account_key: :returns: Challenge-specific validation. """ raise NotImplementedError() # pragma: no cover def response_and_validation(self, account_key, *args, **kwargs): """Generate response and validation. Convenience function that return results of `response` and `validation`. :param JWK account_key: :rtype: tuple """ return (self.response(account_key), self.validation(account_key, *args, **kwargs)) @ChallengeResponse.register class HTTP01Response(KeyAuthorizationChallengeResponse): """ACME http-01 challenge response.""" typ = "http-01" PORT = 80 """Verification port as defined by the protocol. You can override it (e.g. for testing) by passing ``port`` to `simple_verify`. """ WHITESPACE_CUTSET = "\n\r\t " """Whitespace characters which should be ignored at the end of the body.""" def simple_verify(self, chall, domain, account_public_key, port=None): """Simple verify. :param challenges.SimpleHTTP chall: Corresponding challenge. :param unicode domain: Domain name being verified. :param JWK account_public_key: Public key for the key pair being authorized. :param int port: Port used in the validation. :returns: ``True`` iff validation is successful, ``False`` otherwise. :rtype: bool """ if not self.verify(chall, account_public_key): logger.debug("Verification of key authorization in response failed") return False # TODO: ACME specification defines URI template that doesn't # allow to use a custom port... Make sure port is not in the # request URI, if it's standard. if port is not None and port != self.PORT: logger.warning( "Using non-standard port for http-01 verification: %s", port) domain += ":{0}".format(port) uri = chall.uri(domain) logger.debug("Verifying %s at %s...", chall.typ, uri) try: http_response = requests.get(uri) except requests.exceptions.RequestException as error: logger.error("Unable to reach %s: %s", uri, error) return False logger.debug("Received %s: %s. Headers: %s", http_response, http_response.text, http_response.headers) challenge_response = http_response.text.rstrip(self.WHITESPACE_CUTSET) if self.key_authorization != challenge_response: logger.debug("Key authorization from response (%r) doesn't match " "HTTP response (%r)", self.key_authorization, challenge_response) return False return True @Challenge.register # pylint: disable=too-many-ancestors class HTTP01(KeyAuthorizationChallenge): """ACME http-01 challenge.""" response_cls = HTTP01Response typ = response_cls.typ URI_ROOT_PATH = ".well-known/acme-challenge" """URI root path for the server provisioned resource.""" @property def path(self): """Path (starting with '/') for provisioned resource. :rtype: string """ return '/' + self.URI_ROOT_PATH + '/' + self.encode('token') def uri(self, domain): """Create an URI to the provisioned resource. Forms an URI to the HTTPS server provisioned resource (containing :attr:`~SimpleHTTP.token`). :param unicode domain: Domain name being verified. :rtype: string """ return "http://" + domain + self.path def validation(self, account_key, **unused_kwargs): """Generate validation. :param JWK account_key: :rtype: unicode """ return self.key_authorization(account_key) @ChallengeResponse.register class TLSSNI01Response(KeyAuthorizationChallengeResponse): """ACME tls-sni-01 challenge response.""" typ = "tls-sni-01" DOMAIN_SUFFIX = b".acme.invalid" """Domain name suffix.""" PORT = 443 """Verification port as defined by the protocol. You can override it (e.g. for testing) by passing ``port`` to `simple_verify`. """ @property def z(self): # pylint: disable=invalid-name """``z`` value used for verification. :rtype bytes: """ return hashlib.sha256( self.key_authorization.encode("utf-8")).hexdigest().lower().encode() @property def z_domain(self): """Domain name used for verification, generated from `z`. :rtype bytes: """ return self.z[:32] + b'.' + self.z[32:] + self.DOMAIN_SUFFIX def gen_cert(self, key=None, bits=2048): """Generate tls-sni-01 certificate. :param OpenSSL.crypto.PKey key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :param int bits: Number of bits for newly generated key. :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ if key is None: key = OpenSSL.crypto.PKey() key.generate_key(OpenSSL.crypto.TYPE_RSA, bits) return crypto_util.gen_ss_cert(key, [ # z_domain is too big to fit into CN, hence first dummy domain 'dummy', self.z_domain.decode()], force_san=True), key def probe_cert(self, domain, **kwargs): """Probe tls-sni-01 challenge certificate. :param unicode domain: """ # TODO: domain is not necessary if host is provided if "host" not in kwargs: host = socket.gethostbyname(domain) logging.debug('%s resolved to %s', domain, host) kwargs["host"] = host kwargs.setdefault("port", self.PORT) kwargs["name"] = self.z_domain # TODO: try different methods? # pylint: disable=protected-access return crypto_util.probe_sni(**kwargs) def verify_cert(self, cert): """Verify tls-sni-01 challenge certificate. :param OpensSSL.crypto.X509 cert: Challenge certificate. :returns: Whether the certificate was successfully verified. :rtype: bool """ # pylint: disable=protected-access sans = crypto_util._pyopenssl_cert_or_req_san(cert) logging.debug('Certificate %s. SANs: %s', cert.digest('sha1'), sans) return self.z_domain.decode() in sans def simple_verify(self, chall, domain, account_public_key, cert=None, **kwargs): """Simple verify. Verify ``validation`` using ``account_public_key``, optionally probe tls-sni-01 certificate and check using `verify_cert`. :param .challenges.TLSSNI01 chall: Corresponding challenge. :param str domain: Domain name being validated. :param JWK account_public_key: :param OpenSSL.crypto.X509 cert: Optional certificate. If not provided (``None``) certificate will be retrieved using `probe_cert`. :param int port: Port used to probe the certificate. :returns: ``True`` iff client's control of the domain has been verified, ``False`` otherwise. :rtype: bool """ if not self.verify(chall, account_public_key): logger.debug("Verification of key authorization in response failed") return False if cert is None: try: cert = self.probe_cert(domain=domain, **kwargs) except errors.Error as error: logger.debug(error, exc_info=True) return False return self.verify_cert(cert) @Challenge.register # pylint: disable=too-many-ancestors class TLSSNI01(KeyAuthorizationChallenge): """ACME tls-sni-01 challenge.""" response_cls = TLSSNI01Response typ = response_cls.typ # boulder#962, ietf-wg-acme#22 #n = jose.Field("n", encoder=int, decoder=int) def validation(self, account_key, **kwargs): """Generate validation. :param JWK account_key: :param OpenSSL.crypto.PKey cert_key: Optional private key used in certificate generation. If not provided (``None``), then fresh key will be generated. :rtype: `tuple` of `OpenSSL.crypto.X509` and `OpenSSL.crypto.PKey` """ return self.response(account_key).gen_cert(key=kwargs.get('cert_key')) @Challenge.register class RecoveryContact(ContinuityChallenge): """ACME "recoveryContact" challenge. :ivar unicode activation_url: :ivar unicode success_url: :ivar unicode contact: """ typ = "recoveryContact" activation_url = jose.Field("activationURL", omitempty=True) success_url = jose.Field("successURL", omitempty=True) contact = jose.Field("contact", omitempty=True) @ChallengeResponse.register class RecoveryContactResponse(ChallengeResponse): """ACME "recoveryContact" challenge response. :ivar unicode token: """ typ = "recoveryContact" token = jose.Field("token", omitempty=True) @Challenge.register class ProofOfPossession(ContinuityChallenge): """ACME "proofOfPossession" challenge. :ivar .JWAAlgorithm alg: :ivar bytes nonce: Random data, **not** base64-encoded. :ivar hints: Various clues for the client (:class:`Hints`). """ typ = "proofOfPossession" NONCE_SIZE = 16 class Hints(jose.JSONObjectWithFields): """Hints for "proofOfPossession" challenge. :ivar JWK jwk: JSON Web Key :ivar tuple cert_fingerprints: `tuple` of `unicode` :ivar tuple certs: Sequence of :class:`acme.jose.ComparableX509` certificates. :ivar tuple subject_key_identifiers: `tuple` of `unicode` :ivar tuple issuers: `tuple` of `unicode` :ivar tuple authorized_for: `tuple` of `unicode` """ jwk = jose.Field("jwk", decoder=jose.JWK.from_json) cert_fingerprints = jose.Field( "certFingerprints", omitempty=True, default=()) certs = jose.Field("certs", omitempty=True, default=()) subject_key_identifiers = jose.Field( "subjectKeyIdentifiers", omitempty=True, default=()) serial_numbers = jose.Field("serialNumbers", omitempty=True, default=()) issuers = jose.Field("issuers", omitempty=True, default=()) authorized_for = jose.Field("authorizedFor", omitempty=True, default=()) @certs.encoder def certs(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.encode_cert(cert) for cert in value) @certs.decoder def certs(value): # pylint: disable=missing-docstring,no-self-argument return tuple(jose.decode_cert(cert) for cert in value) alg = jose.Field("alg", decoder=jose.JWASignature.from_json) nonce = jose.Field( "nonce", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=NONCE_SIZE)) hints = jose.Field("hints", decoder=Hints.from_json) @ChallengeResponse.register class ProofOfPossessionResponse(ChallengeResponse): """ACME "proofOfPossession" challenge response. :ivar bytes nonce: Random data, **not** base64-encoded. :ivar acme.other.Signature signature: Sugnature of this message. """ typ = "proofOfPossession" NONCE_SIZE = ProofOfPossession.NONCE_SIZE nonce = jose.Field( "nonce", encoder=jose.encode_b64jose, decoder=functools.partial( jose.decode_b64jose, size=NONCE_SIZE)) signature = jose.Field("signature", decoder=other.Signature.from_json) def verify(self): """Verify the challenge.""" # self.signature is not Field | pylint: disable=no-member return self.signature.verify(self.nonce) @Challenge.register # pylint: disable=too-many-ancestors class DNS(_TokenDVChallenge): """ACME "dns" challenge.""" typ = "dns" LABEL = "_acme-challenge" """Label clients prepend to the domain name being validated.""" def gen_validation(self, account_key, alg=jose.RS256, **kwargs): """Generate validation. :param .JWK account_key: Private account key. :param .JWA alg: :returns: This challenge wrapped in `.JWS` :rtype: .JWS """ return jose.JWS.sign( payload=self.json_dumps(sort_keys=True).encode('utf-8'), key=account_key, alg=alg, **kwargs) def check_validation(self, validation, account_public_key): """Check validation. :param JWS validation: :param JWK account_public_key: :rtype: bool """ if not validation.verify(key=account_public_key): return False try: return self == self.json_loads( validation.payload.decode('utf-8')) except jose.DeserializationError as error: logger.debug("Checking validation for DNS failed: %s", error) return False def gen_response(self, account_key, **kwargs): """Generate response. :param .JWK account_key: Private account key. :param .JWA alg: :rtype: DNSResponse """ return DNSResponse(validation=self.gen_validation( self, account_key, **kwargs)) def validation_domain_name(self, name): """Domain name for TXT validation record. :param unicode name: Domain name being validated. """ return "{0}.{1}".format(self.LABEL, name) @ChallengeResponse.register class DNSResponse(ChallengeResponse): """ACME "dns" challenge response. :param JWS validation: """ typ = "dns" validation = jose.Field("validation", decoder=jose.JWS.from_json) def check_validation(self, chall, account_public_key): """Check validation. :param challenges.DNS chall: :param JWK account_public_key: :rtype: bool """ return chall.check_validation(self.validation, account_public_key) acme-0.4.1/acme/__init__.py0000644000175000017500000000045412665157707015056 0ustar bmwbmw00000000000000"""ACME protocol implementation. This module is an implementation of the `ACME protocol`_. Latest supported version: `draft-ietf-acme-01`_. .. _`ACME protocol`: https://ietf-wg-acme.github.io/acme .. _`draft-ietf-acme-01`: https://github.com/ietf-wg-acme/acme/tree/draft-ietf-acme-acme-01 """ acme-0.4.1/acme/crypto_util_test.py0000644000175000017500000001110712665157707016730 0ustar bmwbmw00000000000000"""Tests for acme.crypto_util.""" import itertools import socket import threading import time import unittest import six from six.moves import socketserver # pylint: disable=import-error from acme import errors from acme import jose from acme import test_util class SSLSocketAndProbeSNITest(unittest.TestCase): """Tests for acme.crypto_util.SSLSocket/probe_sni.""" def setUp(self): self.cert = test_util.load_comparable_cert('cert.pem') key = test_util.load_pyopenssl_private_key('rsa512_key.pem') # pylint: disable=protected-access certs = {b'foo': (key, self.cert.wrapped)} from acme.crypto_util import SSLSocket class _TestServer(socketserver.TCPServer): # pylint: disable=too-few-public-methods # six.moves.* | pylint: disable=attribute-defined-outside-init,no-init def server_bind(self): # pylint: disable=missing-docstring self.socket = SSLSocket(socket.socket(), certs=certs) socketserver.TCPServer.server_bind(self) self.server = _TestServer(('', 0), socketserver.BaseRequestHandler) self.port = self.server.socket.getsockname()[1] self.server_thread = threading.Thread( # pylint: disable=no-member target=self.server.handle_request) self.server_thread.start() time.sleep(1) # TODO: avoid race conditions in other way def tearDown(self): self.server_thread.join() def _probe(self, name): from acme.crypto_util import probe_sni return jose.ComparableX509(probe_sni( name, host='127.0.0.1', port=self.port)) def test_probe_ok(self): self.assertEqual(self.cert, self._probe(b'foo')) def test_probe_not_recognized_name(self): self.assertRaises(errors.Error, self._probe, b'bar') # TODO: py33/py34 tox hangs forever on do_hendshake in second probe #def probe_connection_error(self): # self._probe(b'foo') # #time.sleep(1) # TODO: avoid race conditions in other way # self.assertRaises(errors.Error, self._probe, b'bar') class PyOpenSSLCertOrReqSANTest(unittest.TestCase): """Test for acme.crypto_util._pyopenssl_cert_or_req_san.""" @classmethod def _call(cls, loader, name): # pylint: disable=protected-access from acme.crypto_util import _pyopenssl_cert_or_req_san return _pyopenssl_cert_or_req_san(loader(name)) @classmethod def _get_idn_names(cls): """Returns expected names from '{cert,csr}-idnsans.pem'.""" chars = [six.unichr(i) for i in itertools.chain(range(0x3c3, 0x400), range(0x641, 0x6fc), range(0x1820, 0x1877))] return [''.join(chars[i: i + 45]) + '.invalid' for i in range(0, len(chars), 45)] def _call_cert(self, name): return self._call(test_util.load_cert, name) def _call_csr(self, name): return self._call(test_util.load_csr, name) def test_cert_no_sans(self): self.assertEqual(self._call_cert('cert.pem'), []) def test_cert_two_sans(self): self.assertEqual(self._call_cert('cert-san.pem'), ['example.com', 'www.example.com']) def test_cert_hundred_sans(self): self.assertEqual(self._call_cert('cert-100sans.pem'), ['example{0}.com'.format(i) for i in range(1, 101)]) def test_cert_idn_sans(self): self.assertEqual(self._call_cert('cert-idnsans.pem'), self._get_idn_names()) def test_csr_no_sans(self): self.assertEqual(self._call_csr('csr-nosans.pem'), []) def test_csr_one_san(self): self.assertEqual(self._call_csr('csr.pem'), ['example.com']) def test_csr_two_sans(self): self.assertEqual(self._call_csr('csr-san.pem'), ['example.com', 'www.example.com']) def test_csr_six_sans(self): self.assertEqual(self._call_csr('csr-6sans.pem'), ['example.com', 'example.org', 'example.net', 'example.info', 'subdomain.example.com', 'other.subdomain.example.com']) def test_csr_hundred_sans(self): self.assertEqual(self._call_csr('csr-100sans.pem'), ['example{0}.com'.format(i) for i in range(1, 101)]) def test_csr_idn_sans(self): self.assertEqual(self._call_csr('csr-idnsans.pem'), self._get_idn_names()) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/client_test.py0000644000175000017500000006723312665157707015644 0ustar bmwbmw00000000000000"""Tests for acme.client.""" import datetime import json import unittest from six.moves import http_client # pylint: disable=import-error 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 from acme import test_util CERT_DER = test_util.load_vector('cert.der') KEY = jose.JWKRSA.load(test_util.load_vector('rsa512_key.pem')) KEY2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) class ClientTest(unittest.TestCase): """Tests for acme.client.Client.""" # pylint: disable=too-many-instance-attributes,too-many-public-methods def setUp(self): self.response = mock.MagicMock( ok=True, status_code=http_client.OK, headers={}, links={}) self.net = mock.MagicMock() self.net.post.return_value = self.response self.net.get.return_value = self.response self.directory = messages.Directory({ messages.NewRegistration: 'https://www.letsencrypt-demo.org/acme/new-reg', messages.Revocation: 'https://www.letsencrypt-demo.org/acme/revoke-cert', messages.NewAuthorization: 'https://www.letsencrypt-demo.org/acme/new-authz', }) from acme.client import Client self.client = Client( directory=self.directory, key=KEY, alg=jose.RS256, net=self.net) self.identifier = messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com') # Registration self.contact = ('mailto:cert-admin@example.com', 'tel:+12025551212') reg = messages.Registration( contact=self.contact, key=KEY.public_key()) self.new_reg = messages.NewRegistration(**dict(reg)) self.regr = messages.RegistrationResource( body=reg, uri='https://www.letsencrypt-demo.org/acme/reg/1', new_authzr_uri='https://www.letsencrypt-demo.org/acme/new-reg', terms_of_service='https://www.letsencrypt-demo.org/tos') # Authorization authzr_uri = 'https://www.letsencrypt-demo.org/acme/authz/1' challb = messages.ChallengeBody( uri=(authzr_uri + '/1'), status=messages.STATUS_VALID, chall=challenges.DNS(token=jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'))) self.challr = messages.ChallengeResource( body=challb, authzr_uri=authzr_uri) self.authz = messages.Authorization( identifier=messages.Identifier( typ=messages.IDENTIFIER_FQDN, value='example.com'), challenges=(challb,), combinations=None) self.authzr = messages.AuthorizationResource( body=self.authz, uri=authzr_uri, new_cert_uri='https://www.letsencrypt-demo.org/acme/new-cert') # Request issuance self.certr = messages.CertificateResource( body=messages_test.CERT, authzrs=(self.authzr,), uri='https://www.letsencrypt-demo.org/acme/cert/1', cert_chain_uri='https://www.letsencrypt-demo.org/ca') def test_init_downloads_directory(self): uri = 'http://www.letsencrypt-demo.org/directory' from acme.client import Client self.client = Client( directory=uri, key=KEY, alg=jose.RS256, net=self.net) self.net.get.assert_called_once_with(uri) def test_register(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member self.response.status_code = http_client.CREATED self.response.json.return_value = self.regr.body.to_json() self.response.headers['Location'] = self.regr.uri self.response.links.update({ 'next': {'url': self.regr.new_authzr_uri}, 'terms-of-service': {'url': self.regr.terms_of_service}, }) self.assertEqual(self.regr, self.client.register(self.new_reg)) # TODO: test POST call arguments # TODO: split here and separate test reg_wrong_key = self.regr.body.update(key=KEY2.public_key()) self.response.json.return_value = reg_wrong_key.to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.register, self.new_reg) def test_register_missing_next(self): self.response.status_code = http_client.CREATED self.assertRaises( errors.ClientError, self.client.register, self.new_reg) def test_update_registration(self): # "Instance of 'Field' has no to_json/update member" bug: # pylint: disable=no-member self.response.headers['Location'] = self.regr.uri self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.update_registration(self.regr)) # TODO: test POST call arguments # TODO: split here and separate test self.response.json.return_value = self.regr.body.update( contact=()).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.update_registration, self.regr) def test_query_registration(self): self.response.json.return_value = self.regr.body.to_json() self.assertEqual(self.regr, self.client.query_registration(self.regr)) def test_query_registration_updates_new_authzr_uri(self): self.response.json.return_value = self.regr.body.to_json() self.response.links = {'next': {'url': 'UPDATED'}} self.assertEqual( 'UPDATED', self.client.query_registration(self.regr).new_authzr_uri) def test_agree_to_tos(self): self.client.update_registration = mock.Mock() self.client.agree_to_tos(self.regr) regr = self.client.update_registration.call_args[0][0] self.assertEqual(self.regr.terms_of_service, regr.body.agreement) def _prepare_response_for_request_challenges(self): self.response.status_code = http_client.CREATED self.response.headers['Location'] = self.authzr.uri self.response.json.return_value = self.authz.to_json() self.response.links = { 'next': {'url': self.authzr.new_cert_uri}, } def test_request_challenges(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier) self.net.post.assert_called_once_with( self.directory.new_authz, messages.NewAuthorization(identifier=self.identifier)) def test_requets_challenges_custom_uri(self): self._prepare_response_for_request_challenges() self.client.request_challenges(self.identifier, 'URI') self.net.post.assert_called_once_with('URI', mock.ANY) def test_request_challenges_unexpected_update(self): self._prepare_response_for_request_challenges() self.response.json.return_value = self.authz.update( identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.request_challenges, self.identifier, self.authzr.uri) def test_request_challenges_missing_next(self): self.response.status_code = http_client.CREATED self.assertRaises(errors.ClientError, self.client.request_challenges, self.identifier) def test_request_domain_challenges(self): self.client.request_challenges = mock.MagicMock() self.assertEqual( self.client.request_challenges(self.identifier), self.client.request_domain_challenges('example.com')) def test_request_domain_challenges_custom_uri(self): self.client.request_challenges = mock.MagicMock() self.assertEqual( self.client.request_challenges(self.identifier, 'URI'), self.client.request_domain_challenges('example.com', 'URI')) def test_answer_challenge(self): self.response.links['up'] = {'url': self.challr.authzr_uri} self.response.json.return_value = self.challr.body.to_json() chall_response = challenges.DNSResponse(validation=None) self.client.answer_challenge(self.challr.body, chall_response) # TODO: split here and separate test self.assertRaises(errors.UnexpectedUpdate, self.client.answer_challenge, self.challr.body.update(uri='foo'), chall_response) def test_answer_challenge_missing_next(self): self.assertRaises( errors.ClientError, self.client.answer_challenge, self.challr.body, challenges.DNSResponse(validation=None)) def test_retry_after_date(self): self.response.headers['Retry-After'] = 'Fri, 31 Dec 1999 23:59:59 GMT' self.assertEqual( datetime.datetime(1999, 12, 31, 23, 59, 59), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_invalid(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.response.headers['Retry-After'] = 'foooo' self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_overflow(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta dt_mock.datetime.side_effect = datetime.datetime self.response.headers['Retry-After'] = "Tue, 116 Feb 2016 11:50:00 MST" self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_seconds(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.response.headers['Retry-After'] = '50' self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 50), self.client.retry_after(response=self.response, default=10)) @mock.patch('acme.client.datetime') def test_retry_after_missing(self, dt_mock): dt_mock.datetime.now.return_value = datetime.datetime(2015, 3, 27) dt_mock.timedelta = datetime.timedelta self.assertEqual( datetime.datetime(2015, 3, 27, 0, 0, 10), self.client.retry_after(response=self.response, default=10)) def test_poll(self): self.response.json.return_value = self.authzr.body.to_json() self.assertEqual((self.authzr, self.response), self.client.poll(self.authzr)) # TODO: split here and separate test self.response.json.return_value = self.authz.update( identifier=self.identifier.update(value='foo')).to_json() self.assertRaises( errors.UnexpectedUpdate, self.client.poll, self.authzr) def test_request_issuance(self): self.response.content = CERT_DER self.response.headers['Location'] = self.certr.uri self.response.links['up'] = {'url': self.certr.cert_chain_uri} self.assertEqual(self.certr, self.client.request_issuance( messages_test.CSR, (self.authzr,))) # TODO: check POST args def test_request_issuance_missing_up(self): self.response.content = CERT_DER self.response.headers['Location'] = self.certr.uri self.assertEqual( self.certr.update(cert_chain_uri=None), self.client.request_issuance(messages_test.CSR, (self.authzr,))) def test_request_issuance_missing_location(self): self.assertRaises( errors.ClientError, self.client.request_issuance, messages_test.CSR, (self.authzr,)) @mock.patch('acme.client.datetime') @mock.patch('acme.client.time') def test_poll_and_request_issuance(self, time_mock, dt_mock): # clock.dt | pylint: disable=no-member clock = mock.MagicMock(dt=datetime.datetime(2015, 3, 27)) def sleep(seconds): """increment clock""" clock.dt += datetime.timedelta(seconds=seconds) time_mock.sleep.side_effect = sleep def now(): """return current clock value""" return clock.dt dt_mock.datetime.now.side_effect = now dt_mock.timedelta = datetime.timedelta def poll(authzr): # pylint: disable=missing-docstring # record poll start time based on the current clock value authzr.times.append(clock.dt) # suppose it takes 2 seconds for server to produce the # result, increment clock clock.dt += datetime.timedelta(seconds=2) if len(authzr.retries) == 1: # no more retries done = mock.MagicMock(uri=authzr.uri, times=authzr.times) done.body.status = authzr.retries[0] return done, [] # response (2nd result tuple element) is reduced to only # Retry-After header contents represented as integer # seconds; authzr.retries is a list of Retry-After # headers, head(retries) is peeled of as a current # Retry-After header, and tail(retries) is persisted for # later poll() calls return (mock.MagicMock(retries=authzr.retries[1:], uri=authzr.uri + '.', times=authzr.times), authzr.retries[0]) self.client.poll = mock.MagicMock(side_effect=poll) mintime = 7 def retry_after(response, default): # pylint: disable=missing-docstring # check that poll_and_request_issuance correctly passes mintime self.assertEqual(default, mintime) return clock.dt + datetime.timedelta(seconds=response) self.client.retry_after = mock.MagicMock(side_effect=retry_after) def request_issuance(csr, authzrs): # pylint: disable=missing-docstring return csr, authzrs self.client.request_issuance = mock.MagicMock( side_effect=request_issuance) csr = mock.MagicMock() authzrs = ( mock.MagicMock(uri='a', times=[], retries=( 8, 20, 30, messages.STATUS_VALID)), mock.MagicMock(uri='b', times=[], retries=( 5, messages.STATUS_VALID)), ) cert, updated_authzrs = self.client.poll_and_request_issuance( csr, authzrs, mintime=mintime, # make sure that max_attempts is per-authorization, rather # than global max_attempts=max(len(authzrs[0].retries), len(authzrs[1].retries))) self.assertTrue(cert[0] is csr) self.assertTrue(cert[1] is updated_authzrs) self.assertEqual(updated_authzrs[0].uri, 'a...') self.assertEqual(updated_authzrs[1].uri, 'b.') self.assertEqual(updated_authzrs[0].times, [ datetime.datetime(2015, 3, 27), # a is scheduled for 10, but b is polling [9..11), so it # will be picked up as soon as b is finished, without # additional sleeping datetime.datetime(2015, 3, 27, 0, 0, 11), datetime.datetime(2015, 3, 27, 0, 0, 33), datetime.datetime(2015, 3, 27, 0, 1, 5), ]) self.assertEqual(updated_authzrs[1].times, [ datetime.datetime(2015, 3, 27, 0, 0, 2), datetime.datetime(2015, 3, 27, 0, 0, 9), ]) self.assertEqual(clock.dt, datetime.datetime(2015, 3, 27, 0, 1, 7)) # CA sets invalid | TODO: move to a separate test invalid_authzr = mock.MagicMock( times=[], retries=[messages.STATUS_INVALID]) self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs=(invalid_authzr,), mintime=mintime) # exceeded max_attemps | TODO: move to a separate test self.assertRaises( errors.PollError, self.client.poll_and_request_issuance, csr, authzrs, mintime=mintime, max_attempts=2) def test_check_cert(self): self.response.headers['Location'] = self.certr.uri self.response.content = CERT_DER self.assertEqual(self.certr.update(body=messages_test.CERT), self.client.check_cert(self.certr)) # TODO: split here and separate test self.response.headers['Location'] = 'foo' self.assertRaises( errors.UnexpectedUpdate, self.client.check_cert, self.certr) def test_check_cert_missing_location(self): self.response.content = CERT_DER self.assertRaises( errors.ClientError, self.client.check_cert, self.certr) def test_refresh(self): self.client.check_cert = mock.MagicMock() self.assertEqual( self.client.check_cert(self.certr), self.client.refresh(self.certr)) def test_fetch_chain_no_up_link(self): self.assertEqual([], self.client.fetch_chain(self.certr.update( cert_chain_uri=None))) def test_fetch_chain_single(self): # pylint: disable=protected-access self.client._get_cert = mock.MagicMock() self.client._get_cert.return_value = ( mock.MagicMock(links={}), "certificate") self.assertEqual([self.client._get_cert(self.certr.cert_chain_uri)[1]], self.client.fetch_chain(self.certr)) def test_fetch_chain_max(self): # pylint: disable=protected-access up_response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) noup_response = mock.MagicMock(links={}) self.client._get_cert = mock.MagicMock() self.client._get_cert.side_effect = [ (up_response, "cert")] * 9 + [(noup_response, "last_cert")] chain = self.client.fetch_chain(self.certr, max_length=10) self.assertEqual(chain, ["cert"] * 9 + ["last_cert"]) def test_fetch_chain_too_many(self): # recursive # pylint: disable=protected-access response = mock.MagicMock(links={'up': {'url': 'http://cert'}}) self.client._get_cert = mock.MagicMock() self.client._get_cert.return_value = (response, "certificate") self.assertRaises(errors.Error, self.client.fetch_chain, self.certr) def test_revoke(self): self.client.revoke(self.certr.body) self.net.post.assert_called_once_with( self.directory[messages.Revocation], mock.ANY, content_type=None) def test_revoke_bad_status_raises_error(self): self.response.status_code = http_client.METHOD_NOT_ALLOWED self.assertRaises(errors.ClientError, self.client.revoke, self.certr) class ClientNetworkTest(unittest.TestCase): """Tests for acme.client.ClientNetwork.""" def setUp(self): self.verify_ssl = mock.MagicMock() self.wrap_in_jws = mock.MagicMock(return_value=mock.sentinel.wrapped) from acme.client import ClientNetwork self.net = ClientNetwork( key=KEY, alg=jose.RS256, verify_ssl=self.verify_ssl, user_agent='acme-python-test') self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} self.response.links = {} def test_init(self): self.assertTrue(self.net.verify_ssl is self.verify_ssl) def test_wrap_in_jws(self): class MockJSONDeSerializable(jose.JSONDeSerializable): # pylint: disable=missing-docstring def __init__(self, value): self.value = value def to_partial_json(self): return {'foo': self.value} @classmethod def from_json(cls, value): pass # pragma: no cover # pylint: disable=protected-access jws_dump = self.net._wrap_in_jws( MockJSONDeSerializable('foo'), nonce=b'Tg') jws = acme_jws.JWS.json_loads(jws_dump) self.assertEqual(json.loads(jws.payload.decode()), {'foo': 'foo'}) self.assertEqual(jws.signature.combined.nonce, b'Tg') def test_check_response_not_ok_jobj_no_error(self): self.response.ok = False self.response.json.return_value = {} # pylint: disable=protected-access self.assertRaises( errors.ClientError, self.net._check_response, self.response) def test_check_response_not_ok_jobj_error(self): self.response.ok = False self.response.json.return_value = messages.Error( detail='foo', typ='serverInternal', title='some title').to_json() # pylint: disable=protected-access self.assertRaises( messages.Error, self.net._check_response, self.response) def test_check_response_not_ok_no_jobj(self): self.response.ok = False self.response.json.side_effect = ValueError # pylint: disable=protected-access self.assertRaises( errors.ClientError, self.net._check_response, self.response) def test_check_response_ok_no_jobj_ct_required(self): self.response.json.side_effect = ValueError for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access self.assertRaises( errors.ClientError, self.net._check_response, self.response, content_type=self.net.JSON_CONTENT_TYPE) def test_check_response_ok_no_jobj_no_ct(self): self.response.json.side_effect = ValueError for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access,no-value-for-parameter self.assertEqual( self.response, self.net._check_response(self.response)) def test_check_response_jobj(self): self.response.json.return_value = {} for response_ct in [self.net.JSON_CONTENT_TYPE, 'foo']: self.response.headers['Content-Type'] = response_ct # pylint: disable=protected-access,no-value-for-parameter self.assertEqual( self.response, self.net._check_response(self.response)) @mock.patch('acme.client.requests') def test_send_request(self, mock_requests): mock_requests.request.return_value = self.response # pylint: disable=protected-access self.assertEqual(self.response, self.net._send_request( 'HEAD', 'url', 'foo', bar='baz')) mock_requests.request.assert_called_once_with( 'HEAD', 'url', 'foo', verify=mock.ANY, bar='baz', headers=mock.ANY) @mock.patch('acme.client.requests') def test_send_request_verify_ssl(self, mock_requests): # pylint: disable=protected-access for verify in True, False: mock_requests.request.reset_mock() mock_requests.request.return_value = self.response self.net.verify_ssl = verify # pylint: disable=protected-access self.assertEqual( self.response, self.net._send_request('GET', 'url')) mock_requests.request.assert_called_once_with( 'GET', 'url', verify=verify, headers=mock.ANY) @mock.patch('acme.client.requests') def test_send_request_user_agent(self, mock_requests): mock_requests.request.return_value = self.response # pylint: disable=protected-access self.net._send_request('GET', 'url', headers={'bar': 'baz'}) mock_requests.request.assert_called_once_with( 'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'acme-python-test', 'bar': 'baz'}) self.net._send_request('GET', 'url', headers={'User-Agent': 'foo2'}) mock_requests.request.assert_called_with( 'GET', 'url', verify=mock.ANY, headers={'User-Agent': 'foo2'}) @mock.patch('acme.client.requests') def test_requests_error_passthrough(self, mock_requests): mock_requests.exceptions = requests.exceptions mock_requests.request.side_effect = requests.exceptions.RequestException # pylint: disable=protected-access self.assertRaises(requests.exceptions.RequestException, self.net._send_request, 'GET', 'uri') class ClientNetworkWithMockedResponseTest(unittest.TestCase): """Tests for acme.client.ClientNetwork which mock out response.""" # pylint: disable=too-many-instance-attributes def setUp(self): from acme.client import ClientNetwork self.net = ClientNetwork(key=None, alg=None) self.response = mock.MagicMock(ok=True, status_code=http_client.OK) self.response.headers = {} self.response.links = {} self.checked_response = mock.MagicMock() self.obj = mock.MagicMock() self.wrapped_obj = mock.MagicMock() self.content_type = mock.sentinel.content_type self.all_nonces = [jose.b64encode(b'Nonce'), jose.b64encode(b'Nonce2')] self.available_nonces = self.all_nonces[:] def send_request(*args, **kwargs): # pylint: disable=unused-argument,missing-docstring if self.available_nonces: self.response.headers = { self.net.REPLAY_NONCE_HEADER: self.available_nonces.pop().decode()} else: self.response.headers = {} return self.response # pylint: disable=protected-access self.net._send_request = self.send_request = mock.MagicMock( side_effect=send_request) self.net._check_response = self.check_response self.net._wrap_in_jws = mock.MagicMock(return_value=self.wrapped_obj) def check_response(self, response, content_type): # pylint: disable=missing-docstring self.assertEqual(self.response, response) self.assertEqual(self.content_type, content_type) return self.checked_response def test_head(self): self.assertEqual(self.response, self.net.head('url', 'foo', bar='baz')) self.send_request.assert_called_once_with( 'HEAD', 'url', 'foo', bar='baz') def test_get(self): self.assertEqual(self.checked_response, self.net.get( 'url', content_type=self.content_type, bar='baz')) self.send_request.assert_called_once_with('GET', 'url', bar='baz') def test_post(self): # pylint: disable=protected-access self.assertEqual(self.checked_response, self.net.post( 'uri', self.obj, content_type=self.content_type)) self.net._wrap_in_jws.assert_called_once_with( self.obj, jose.b64decode(self.all_nonces.pop())) assert not self.available_nonces self.assertRaises(errors.MissingNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) self.net._wrap_in_jws.assert_called_with( self.obj, jose.b64decode(self.all_nonces.pop())) def test_post_wrong_initial_nonce(self): # HEAD self.available_nonces = [b'f', jose.b64encode(b'good')] self.assertRaises(errors.BadNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) def test_post_wrong_post_response_nonce(self): self.available_nonces = [jose.b64encode(b'good'), b'f'] self.assertRaises(errors.BadNonce, self.net.post, 'uri', self.obj, content_type=self.content_type) def test_head_get_post_error_passthrough(self): self.send_request.side_effect = requests.exceptions.RequestException for method in self.net.head, self.net.get: self.assertRaises( requests.exceptions.RequestException, method, 'GET', 'uri') self.assertRaises(requests.exceptions.RequestException, self.net.post, 'uri', obj=self.obj) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/standalone_test.py0000644000175000017500000001215312665157707016505 0ustar bmwbmw00000000000000"""Tests for acme.standalone.""" import os import shutil import threading import tempfile import time import unittest from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # pylint: disable=import-error import requests from acme import challenges from acme import crypto_util from acme import errors from acme import jose from acme import test_util class TLSServerTest(unittest.TestCase): """Tests for acme.standalone.TLSServer.""" def test_bind(self): # pylint: disable=no-self-use from acme.standalone import TLSServer server = TLSServer( ('', 0), socketserver.BaseRequestHandler, bind_and_activate=True) server.server_close() # pylint: disable=no-member class TLSSNI01ServerTest(unittest.TestCase): """Test for acme.standalone.TLSSNI01Server.""" def setUp(self): self.certs = {b'localhost': ( test_util.load_pyopenssl_private_key('rsa512_key.pem'), test_util.load_cert('cert.pem'), )} from acme.standalone import TLSSNI01Server self.server = TLSSNI01Server(("", 0), certs=self.certs) # pylint: disable=no-member self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): self.server.shutdown() # pylint: disable=no-member self.thread.join() def test_it(self): host, port = self.server.socket.getsockname()[:2] cert = crypto_util.probe_sni( b'localhost', host=host, port=port, timeout=1) self.assertEqual(jose.ComparableX509(cert), jose.ComparableX509(self.certs[b'localhost'][1])) class HTTP01ServerTest(unittest.TestCase): """Tests for acme.standalone.HTTP01Server.""" def setUp(self): self.account_key = jose.JWK.load( test_util.load_vector('rsa1024_key.pem')) self.resources = set() from acme.standalone import HTTP01Server self.server = HTTP01Server(('', 0), resources=self.resources) # pylint: disable=no-member self.port = self.server.socket.getsockname()[1] self.thread = threading.Thread(target=self.server.serve_forever) self.thread.start() def tearDown(self): self.server.shutdown() # pylint: disable=no-member self.thread.join() def test_index(self): response = requests.get( 'http://localhost:{0}'.format(self.port), verify=False) self.assertEqual( response.text, 'ACME client standalone challenge solver') self.assertTrue(response.ok) def test_404(self): response = requests.get( 'http://localhost:{0}/foo'.format(self.port), verify=False) self.assertEqual(response.status_code, http_client.NOT_FOUND) def _test_http01(self, add): chall = challenges.HTTP01(token=(b'x' * 16)) response, validation = chall.response_and_validation(self.account_key) from acme.standalone import HTTP01RequestHandler resource = HTTP01RequestHandler.HTTP01Resource( chall=chall, response=response, validation=validation) if add: self.resources.add(resource) return resource.response.simple_verify( resource.chall, 'localhost', self.account_key.public_key(), port=self.port) def test_http01_found(self): self.assertTrue(self._test_http01(add=True)) def test_http01_not_found(self): self.assertFalse(self._test_http01(add=False)) class TestSimpleTLSSNI01Server(unittest.TestCase): """Tests for acme.standalone.simple_tls_sni_01_server.""" def setUp(self): # mirror ../examples/standalone self.test_cwd = tempfile.mkdtemp() localhost_dir = os.path.join(self.test_cwd, 'localhost') os.makedirs(localhost_dir) shutil.copy(test_util.vector_path('cert.pem'), localhost_dir) shutil.copy(test_util.vector_path('rsa512_key.pem'), os.path.join(localhost_dir, 'key.pem')) from acme.standalone import simple_tls_sni_01_server self.port = 1234 self.thread = threading.Thread( target=simple_tls_sni_01_server, kwargs={ 'cli_args': ('xxx', '--port', str(self.port)), 'forever': False, }, ) self.old_cwd = os.getcwd() os.chdir(self.test_cwd) self.thread.start() def tearDown(self): os.chdir(self.old_cwd) self.thread.join() shutil.rmtree(self.test_cwd) def test_it(self): max_attempts = 5 while max_attempts: max_attempts -= 1 try: cert = crypto_util.probe_sni( b'localhost', b'0.0.0.0', self.port) except errors.Error: self.assertTrue(max_attempts > 0, "Timeout!") time.sleep(1) # wait until thread starts else: self.assertEqual(jose.ComparableX509(cert), test_util.load_comparable_cert('cert.pem')) break if __name__ == "__main__": unittest.main() # pragma: no cover acme-0.4.1/acme/fields.py0000644000175000017500000000331612665157707014565 0ustar bmwbmw00000000000000"""ACME JSON fields.""" import logging import pyrfc3339 from acme import jose logger = logging.getLogger(__name__) class Fixed(jose.Field): """Fixed field.""" def __init__(self, json_name, value): self.value = value super(Fixed, self).__init__( json_name=json_name, default=value, omitempty=False) def decode(self, value): if value != self.value: raise jose.DeserializationError('Expected {0!r}'.format(self.value)) return self.value def encode(self, value): if value != self.value: logger.warn( 'Overriding fixed field (%s) with %r', self.json_name, value) return value class RFC3339Field(jose.Field): """RFC3339 field encoder/decoder. Handles decoding/encoding between RFC3339 strings and aware (not naive) `datetime.datetime` objects (e.g. ``datetime.datetime.now(pytz.utc)``). """ @classmethod def default_encoder(cls, value): return pyrfc3339.generate(value) @classmethod def default_decoder(cls, value): try: return pyrfc3339.parse(value) except ValueError as error: raise jose.DeserializationError(error) class Resource(jose.Field): """Resource MITM field.""" def __init__(self, resource_type, *args, **kwargs): self.resource_type = resource_type super(Resource, self).__init__( 'resource', default=resource_type, *args, **kwargs) def decode(self, value): if value != self.resource_type: raise jose.DeserializationError( 'Wrong resource type: {0} instead of {1}'.format( value, self.resource_type)) return value acme-0.4.1/acme/other_test.py0000644000175000017500000000625712665157707015506 0ustar bmwbmw00000000000000"""Tests for acme.sig.""" import unittest from acme import jose from acme import test_util KEY = test_util.load_rsa_private_key('rsa512_key.pem') class SignatureTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes """Tests for acme.sig.Signature.""" def setUp(self): self.msg = b'message' self.sig = (b'IC\xd8*\xe7\x14\x9e\x19S\xb7\xcf\xec3\x12\xe2\x8a\x03' b'\x98u\xff\xf0\x94\xe2\xd7<\x8f\xa8\xed\xa4KN\xc3\xaa' b'\xb9X\xc3w\xaa\xc0_\xd0\x05$y>l#\x10<\x96\xd2\xcdr\xa3' b'\x1b\xa1\xf5!f\xef\xc64\xb6\x13') self.nonce = b'\xec\xd6\xf2oYH\xeb\x13\xd5#q\xe0\xdd\xa2\x92\xa9' self.alg = jose.RS256 self.jwk = jose.JWKRSA(key=KEY.public_key()) b64sig = ('SUPYKucUnhlTt8_sMxLiigOYdf_wlOLXPI-o7aRLTsOquVjDd6r' 'AX9AFJHk-bCMQPJbSzXKjG6H1IWbvxjS2Ew') b64nonce = '7Nbyb1lI6xPVI3Hg3aKSqQ' self.jsig_to = { 'nonce': b64nonce, 'alg': self.alg, 'jwk': self.jwk, 'sig': b64sig, } self.jsig_from = { 'nonce': b64nonce, 'alg': self.alg.to_partial_json(), 'jwk': self.jwk.to_partial_json(), 'sig': b64sig, } from acme.other import Signature self.signature = Signature( alg=self.alg, sig=self.sig, nonce=self.nonce, jwk=self.jwk) def test_attributes(self): self.assertEqual(self.signature.nonce, self.nonce) self.assertEqual(self.signature.alg, self.alg) self.assertEqual(self.signature.sig, self.sig) self.assertEqual(self.signature.jwk, self.jwk) def test_verify_good_succeeds(self): self.assertTrue(self.signature.verify(self.msg)) def test_verify_bad_fails(self): self.assertFalse(self.signature.verify(self.msg + b'x')) @classmethod def _from_msg(cls, *args, **kwargs): from acme.other import Signature return Signature.from_msg(*args, **kwargs) def test_create_from_msg(self): signature = self._from_msg(self.msg, KEY, self.nonce) self.assertEqual(self.signature, signature) def test_create_from_msg_random_nonce(self): signature = self._from_msg(self.msg, KEY) self.assertEqual(signature.alg, self.alg) self.assertEqual(signature.jwk, self.jwk) self.assertTrue(signature.verify(self.msg)) def test_to_partial_json(self): self.assertEqual(self.signature.to_partial_json(), self.jsig_to) def test_from_json(self): from acme.other import Signature self.assertEqual( self.signature, Signature.from_json(self.jsig_from)) def test_from_json_non_schema_errors(self): from acme.other import Signature jwk = self.jwk.to_partial_json() self.assertRaises( jose.DeserializationError, Signature.from_json, { 'alg': 'RS256', 'sig': 'x', 'nonce': '', 'jwk': jwk}) self.assertRaises( jose.DeserializationError, Signature.from_json, { 'alg': 'RS256', 'sig': '', 'nonce': 'x', 'jwk': jwk}) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/fields_test.py0000644000175000017500000000404312665157707015622 0ustar bmwbmw00000000000000"""Tests for acme.fields.""" import datetime import unittest import pytz from acme import jose class FixedTest(unittest.TestCase): """Tests for acme.fields.Fixed.""" def setUp(self): from acme.fields import Fixed self.field = Fixed('name', 'x') def test_decode(self): self.assertEqual('x', self.field.decode('x')) def test_decode_bad(self): self.assertRaises(jose.DeserializationError, self.field.decode, 'y') def test_encode(self): self.assertEqual('x', self.field.encode('x')) def test_encode_override(self): self.assertEqual('y', self.field.encode('y')) class RFC3339FieldTest(unittest.TestCase): """Tests for acme.fields.RFC3339Field.""" def setUp(self): self.decoded = datetime.datetime(2015, 3, 27, tzinfo=pytz.utc) self.encoded = '2015-03-27T00:00:00Z' def test_default_encoder(self): from acme.fields import RFC3339Field self.assertEqual( self.encoded, RFC3339Field.default_encoder(self.decoded)) def test_default_encoder_naive_fails(self): from acme.fields import RFC3339Field self.assertRaises( ValueError, RFC3339Field.default_encoder, datetime.datetime.now()) def test_default_decoder(self): from acme.fields import RFC3339Field self.assertEqual( self.decoded, RFC3339Field.default_decoder(self.encoded)) def test_default_decoder_raises_deserialization_error(self): from acme.fields import RFC3339Field self.assertRaises( jose.DeserializationError, RFC3339Field.default_decoder, '') class ResourceTest(unittest.TestCase): """Tests for acme.fields.Resource.""" def setUp(self): from acme.fields import Resource self.field = Resource('x') def test_decode_good(self): self.assertEqual('x', self.field.decode('x')) def test_decode_wrong(self): self.assertRaises(jose.DeserializationError, self.field.decode, 'y') if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/standalone.py0000644000175000017500000001466612665157707015461 0ustar bmwbmw00000000000000"""Support for standalone client challenge solvers. """ import argparse import collections import functools import logging import os import sys from six.moves import BaseHTTPServer # pylint: disable=import-error from six.moves import http_client # pylint: disable=import-error from six.moves import socketserver # pylint: disable=import-error import OpenSSL from acme import challenges from acme import crypto_util logger = logging.getLogger(__name__) # six.moves.* | pylint: disable=no-member,attribute-defined-outside-init # pylint: disable=too-few-public-methods,no-init class TLSServer(socketserver.TCPServer): """Generic TLS Server.""" def __init__(self, *args, **kwargs): self.certs = kwargs.pop("certs", {}) self.method = kwargs.pop( # pylint: disable=protected-access "method", crypto_util._DEFAULT_TLSSNI01_SSL_METHOD) self.allow_reuse_address = kwargs.pop("allow_reuse_address", True) socketserver.TCPServer.__init__(self, *args, **kwargs) def _wrap_sock(self): self.socket = crypto_util.SSLSocket( self.socket, certs=self.certs, method=self.method) def server_bind(self): # pylint: disable=missing-docstring self._wrap_sock() return socketserver.TCPServer.server_bind(self) class ACMEServerMixin: # pylint: disable=old-style-class """ACME server common settings mixin.""" # TODO: c.f. #858 server_version = "ACME client standalone challenge solver" allow_reuse_address = True class TLSSNI01Server(TLSServer, ACMEServerMixin): """TLSSNI01 Server.""" def __init__(self, server_address, certs): TLSServer.__init__( self, server_address, BaseRequestHandlerWithLogging, certs=certs) class BaseRequestHandlerWithLogging(socketserver.BaseRequestHandler): """BaseRequestHandler with logging.""" def log_message(self, format, *args): # pylint: disable=redefined-builtin """Log arbitrary message.""" logger.debug("%s - - %s", self.client_address[0], format % args) def handle(self): """Handle request.""" self.log_message("Incoming request") socketserver.BaseRequestHandler.handle(self) class HTTP01Server(BaseHTTPServer.HTTPServer, ACMEServerMixin): """HTTP01 Server.""" def __init__(self, server_address, resources): BaseHTTPServer.HTTPServer.__init__( self, server_address, HTTP01RequestHandler.partial_init( simple_http_resources=resources)) class HTTP01RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): """HTTP01 challenge handler. Adheres to the stdlib's `socketserver.BaseRequestHandler` interface. :ivar set simple_http_resources: A set of `HTTP01Resource` objects. TODO: better name? """ HTTP01Resource = collections.namedtuple( "HTTP01Resource", "chall response validation") def __init__(self, *args, **kwargs): self.simple_http_resources = kwargs.pop("simple_http_resources", set()) socketserver.BaseRequestHandler.__init__(self, *args, **kwargs) def log_message(self, format, *args): # pylint: disable=redefined-builtin """Log arbitrary message.""" logger.debug("%s - - %s", self.client_address[0], format % args) def handle(self): """Handle request.""" self.log_message("Incoming request") BaseHTTPServer.BaseHTTPRequestHandler.handle(self) def do_GET(self): # pylint: disable=invalid-name,missing-docstring if self.path == "/": self.handle_index() elif self.path.startswith("/" + challenges.HTTP01.URI_ROOT_PATH): self.handle_simple_http_resource() else: self.handle_404() def handle_index(self): """Handle index page.""" self.send_response(200) self.send_header("Content-Type", "text/html") self.end_headers() self.wfile.write(self.server.server_version.encode()) def handle_404(self): """Handler 404 Not Found errors.""" self.send_response(http_client.NOT_FOUND, message="Not Found") self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(b"404") def handle_simple_http_resource(self): """Handle HTTP01 provisioned resources.""" for resource in self.simple_http_resources: if resource.chall.path == self.path: self.log_message("Serving HTTP01 with token %r", resource.chall.encode("token")) self.send_response(http_client.OK) self.end_headers() self.wfile.write(resource.validation.encode()) return else: # pylint: disable=useless-else-on-loop self.log_message("No resources to serve") self.log_message("%s does not correspond to any resource. ignoring", self.path) @classmethod def partial_init(cls, simple_http_resources): """Partially initialize this handler. This is useful because `socketserver.BaseServer` takes uninitialized handler and initializes it with the current request. """ return functools.partial( cls, simple_http_resources=simple_http_resources) def simple_tls_sni_01_server(cli_args, forever=True): """Run simple standalone TLSSNI01 server.""" logging.basicConfig(level=logging.DEBUG) parser = argparse.ArgumentParser() parser.add_argument( "-p", "--port", default=0, help="Port to serve at. By default " "picks random free port.") args = parser.parse_args(cli_args[1:]) certs = {} _, hosts, _ = next(os.walk('.')) for host in hosts: with open(os.path.join(host, "cert.pem")) as cert_file: cert_contents = cert_file.read() with open(os.path.join(host, "key.pem")) as key_file: key_contents = key_file.read() certs[host.encode()] = ( OpenSSL.crypto.load_privatekey( OpenSSL.crypto.FILETYPE_PEM, key_contents), OpenSSL.crypto.load_certificate( OpenSSL.crypto.FILETYPE_PEM, cert_contents)) server = TLSSNI01Server(('', int(args.port)), certs=certs) logger.info("Serving at https://%s:%s...", *server.socket.getsockname()[:2]) if forever: # pragma: no cover server.serve_forever() else: server.handle_request() if __name__ == "__main__": sys.exit(simple_tls_sni_01_server(sys.argv)) # pragma: no cover acme-0.4.1/acme/errors.py0000644000175000017500000000433312665157707014633 0ustar bmwbmw00000000000000"""ACME errors.""" from acme.jose import errors as jose_errors class Error(Exception): """Generic ACME error.""" class SchemaValidationError(jose_errors.DeserializationError): """JSON schema ACME object validation error.""" class ClientError(Error): """Network error.""" class UnexpectedUpdate(ClientError): """Unexpected update error.""" class NonceError(ClientError): """Server response nonce error.""" class BadNonce(NonceError): """Bad nonce error.""" def __init__(self, nonce, error, *args, **kwargs): super(BadNonce, self).__init__(*args, **kwargs) self.nonce = nonce self.error = error def __str__(self): return 'Invalid nonce ({0!r}): {1}'.format(self.nonce, self.error) class MissingNonce(NonceError): """Missing nonce error. According to the specification an "ACME server MUST include an Replay-Nonce header field in each successful response to a POST it provides to a client (...)". :ivar requests.Response response: HTTP Response """ def __init__(self, response, *args, **kwargs): super(MissingNonce, self).__init__(*args, **kwargs) self.response = response def __str__(self): return ('Server {0} response did not include a replay ' 'nonce, headers: {1}'.format( self.response.request.method, self.response.headers)) class PollError(ClientError): """Generic error when polling for authorization fails. This might be caused by either timeout (`exhausted` will be non-empty) or by some authorization being invalid. :ivar exhausted: Set of `.AuthorizationResource` that didn't finish within max allowed attempts. :ivar updated: Mapping from original `.AuthorizationResource` to the most recently updated one """ def __init__(self, exhausted, updated): self.exhausted = exhausted self.updated = updated super(PollError, self).__init__() @property def timeout(self): """Was the error caused by timeout?""" return bool(self.exhausted) def __repr__(self): return '{0}(exhausted={1!r}, updated={2!r})'.format( self.__class__.__name__, self.exhausted, self.updated) acme-0.4.1/acme/messages_test.py0000644000175000017500000003047212665157707016170 0ustar bmwbmw00000000000000"""Tests for acme.messages.""" import unittest import mock from acme import challenges from acme import jose from acme import test_util CERT = test_util.load_comparable_cert('cert.der') CSR = test_util.load_comparable_csr('csr.der') KEY = test_util.load_rsa_private_key('rsa512_key.pem') class ErrorTest(unittest.TestCase): """Tests for acme.messages.Error.""" def setUp(self): from acme.messages import Error self.error = Error( detail='foo', typ='urn:acme:error:malformed', title='title') self.jobj = { 'detail': 'foo', 'title': 'some title', 'type': 'urn:acme:error:malformed', } self.error_custom = Error(typ='custom', detail='bar') self.jobj_cusom = {'type': 'custom', 'detail': 'bar'} def test_from_json_hashable(self): from acme.messages import Error hash(Error.from_json(self.error.to_json())) def test_description(self): self.assertEqual( 'The request message was malformed', self.error.description) self.assertTrue(self.error_custom.description is None) def test_str(self): self.assertEqual( 'urn:acme:error:malformed :: The request message was ' 'malformed :: foo :: title', str(self.error)) self.assertEqual('custom :: bar', str(self.error_custom)) class ConstantTest(unittest.TestCase): """Tests for acme.messages._Constant.""" def setUp(self): from acme.messages import _Constant class MockConstant(_Constant): # pylint: disable=missing-docstring POSSIBLE_NAMES = {} self.MockConstant = MockConstant # pylint: disable=invalid-name self.const_a = MockConstant('a') self.const_b = MockConstant('b') def test_to_partial_json(self): self.assertEqual('a', self.const_a.to_partial_json()) self.assertEqual('b', self.const_b.to_partial_json()) def test_from_json(self): self.assertEqual(self.const_a, self.MockConstant.from_json('a')) self.assertRaises( jose.DeserializationError, self.MockConstant.from_json, 'c') def test_from_json_hashable(self): hash(self.MockConstant.from_json('a')) def test_repr(self): self.assertEqual('MockConstant(a)', repr(self.const_a)) self.assertEqual('MockConstant(b)', repr(self.const_b)) def test_equality(self): const_a_prime = self.MockConstant('a') self.assertFalse(self.const_a == self.const_b) self.assertTrue(self.const_a == const_a_prime) self.assertTrue(self.const_a != self.const_b) self.assertFalse(self.const_a != const_a_prime) class DirectoryTest(unittest.TestCase): """Tests for acme.messages.Directory.""" def setUp(self): from acme.messages import Directory self.dir = Directory({ 'new-reg': 'reg', mock.MagicMock(resource_type='new-cert'): 'cert', }) def test_init_wrong_key_value_error(self): from acme.messages import Directory self.assertRaises(ValueError, Directory, {'foo': 'bar'}) def test_getitem(self): self.assertEqual('reg', self.dir['new-reg']) from acme.messages import NewRegistration self.assertEqual('reg', self.dir[NewRegistration]) self.assertEqual('reg', self.dir[NewRegistration()]) def test_getitem_fails_with_key_error(self): self.assertRaises(KeyError, self.dir.__getitem__, 'foo') def test_getattr(self): self.assertEqual('reg', self.dir.new_reg) def test_getattr_fails_with_attribute_error(self): self.assertRaises(AttributeError, self.dir.__getattr__, 'foo') def test_to_partial_json(self): self.assertEqual( self.dir.to_partial_json(), {'new-reg': 'reg', 'new-cert': 'cert'}) def test_from_json_deserialization_error_on_wrong_key(self): from acme.messages import Directory self.assertRaises( jose.DeserializationError, Directory.from_json, {'foo': 'bar'}) class RegistrationTest(unittest.TestCase): """Tests for acme.messages.Registration.""" def setUp(self): key = jose.jwk.JWKRSA(key=KEY.public_key()) contact = ( 'mailto:admin@foo.com', 'tel:1234', ) agreement = 'https://letsencrypt.org/terms' from acme.messages import Registration self.reg = Registration(key=key, contact=contact, agreement=agreement) self.reg_none = Registration(authorizations='uri/authorizations', certificates='uri/certificates') self.jobj_to = { 'contact': contact, 'agreement': agreement, 'key': key, } self.jobj_from = self.jobj_to.copy() self.jobj_from['key'] = key.to_json() def test_from_data(self): from acme.messages import Registration reg = Registration.from_data(phone='1234', email='admin@foo.com') self.assertEqual(reg.contact, ( 'tel:1234', 'mailto:admin@foo.com', )) def test_phones(self): self.assertEqual(('1234',), self.reg.phones) def test_emails(self): self.assertEqual(('admin@foo.com',), self.reg.emails) def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.reg.to_partial_json()) def test_from_json(self): from acme.messages import Registration self.assertEqual(self.reg, Registration.from_json(self.jobj_from)) def test_from_json_hashable(self): from acme.messages import Registration hash(Registration.from_json(self.jobj_from)) class UpdateRegistrationTest(unittest.TestCase): """Tests for acme.messages.UpdateRegistration.""" def test_empty(self): from acme.messages import UpdateRegistration jstring = '{"resource": "reg"}' self.assertEqual(jstring, UpdateRegistration().json_dumps()) self.assertEqual( UpdateRegistration(), UpdateRegistration.json_loads(jstring)) class RegistrationResourceTest(unittest.TestCase): """Tests for acme.messages.RegistrationResource.""" def setUp(self): from acme.messages import RegistrationResource self.regr = RegistrationResource( body=mock.sentinel.body, uri=mock.sentinel.uri, new_authzr_uri=mock.sentinel.new_authzr_uri, terms_of_service=mock.sentinel.terms_of_service) def test_to_partial_json(self): self.assertEqual(self.regr.to_json(), { 'body': mock.sentinel.body, 'uri': mock.sentinel.uri, 'new_authzr_uri': mock.sentinel.new_authzr_uri, 'terms_of_service': mock.sentinel.terms_of_service, }) class ChallengeResourceTest(unittest.TestCase): """Tests for acme.messages.ChallengeResource.""" def test_uri(self): from acme.messages import ChallengeResource self.assertEqual('http://challb', ChallengeResource(body=mock.MagicMock( uri='http://challb'), authzr_uri='http://authz').uri) class ChallengeBodyTest(unittest.TestCase): """Tests for acme.messages.ChallengeBody.""" def setUp(self): self.chall = challenges.DNS(token=jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) from acme.messages import ChallengeBody from acme.messages import Error from acme.messages import STATUS_INVALID self.status = STATUS_INVALID error = Error(typ='urn:acme:error:serverInternal', detail='Unable to communicate with DNS server') self.challb = ChallengeBody( uri='http://challb', chall=self.chall, status=self.status, error=error) self.jobj_to = { 'uri': 'http://challb', 'status': self.status, 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', 'error': error, } self.jobj_from = self.jobj_to.copy() self.jobj_from['status'] = 'invalid' self.jobj_from['error'] = { 'type': 'urn:acme:error:serverInternal', 'detail': 'Unable to communicate with DNS server', } def test_to_partial_json(self): self.assertEqual(self.jobj_to, self.challb.to_partial_json()) def test_from_json(self): from acme.messages import ChallengeBody self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) def test_from_json_hashable(self): from acme.messages import ChallengeBody hash(ChallengeBody.from_json(self.jobj_from)) def test_proxy(self): self.assertEqual(jose.b64decode( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA'), self.challb.token) class AuthorizationTest(unittest.TestCase): """Tests for acme.messages.Authorization.""" def setUp(self): from acme.messages import ChallengeBody from acme.messages import STATUS_VALID self.challbs = ( ChallengeBody( uri='http://challb1', status=STATUS_VALID, chall=challenges.HTTP01(token=b'IlirfxKKXAsHtmzK29Pj8A')), ChallengeBody(uri='http://challb2', status=STATUS_VALID, chall=challenges.DNS( token=b'DGyRejmCefe7v4NfDGDKfA')), ChallengeBody(uri='http://challb3', status=STATUS_VALID, chall=challenges.RecoveryContact()), ) combinations = ((0, 2), (1, 2)) from acme.messages import Authorization from acme.messages import Identifier from acme.messages import IDENTIFIER_FQDN identifier = Identifier(typ=IDENTIFIER_FQDN, value='example.com') self.authz = Authorization( identifier=identifier, combinations=combinations, challenges=self.challbs) self.jobj_from = { 'identifier': identifier.to_json(), 'challenges': [challb.to_json() for challb in self.challbs], 'combinations': combinations, } def test_from_json(self): from acme.messages import Authorization Authorization.from_json(self.jobj_from) def test_from_json_hashable(self): from acme.messages import Authorization hash(Authorization.from_json(self.jobj_from)) def test_resolved_combinations(self): self.assertEqual(self.authz.resolved_combinations, ( (self.challbs[0], self.challbs[2]), (self.challbs[1], self.challbs[2]), )) class AuthorizationResourceTest(unittest.TestCase): """Tests for acme.messages.AuthorizationResource.""" def test_json_de_serializable(self): from acme.messages import AuthorizationResource authzr = AuthorizationResource( uri=mock.sentinel.uri, body=mock.sentinel.body, new_cert_uri=mock.sentinel.new_cert_uri, ) self.assertTrue(isinstance(authzr, jose.JSONDeSerializable)) class CertificateRequestTest(unittest.TestCase): """Tests for acme.messages.CertificateRequest.""" def setUp(self): from acme.messages import CertificateRequest self.req = CertificateRequest(csr=CSR) def test_json_de_serializable(self): self.assertTrue(isinstance(self.req, jose.JSONDeSerializable)) from acme.messages import CertificateRequest self.assertEqual( self.req, CertificateRequest.from_json(self.req.to_json())) class CertificateResourceTest(unittest.TestCase): """Tests for acme.messages.CertificateResourceTest.""" def setUp(self): from acme.messages import CertificateResource self.certr = CertificateResource( body=CERT, uri=mock.sentinel.uri, authzrs=(), cert_chain_uri=mock.sentinel.cert_chain_uri) def test_json_de_serializable(self): self.assertTrue(isinstance(self.certr, jose.JSONDeSerializable)) from acme.messages import CertificateResource self.assertEqual( self.certr, CertificateResource.from_json(self.certr.to_json())) class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" def setUp(self): from acme.messages import Revocation self.rev = Revocation(certificate=CERT) def test_from_json_hashable(self): from acme.messages import Revocation hash(Revocation.from_json(self.rev.to_json())) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/acme/util.py0000644000175000017500000000024612665157707014273 0ustar bmwbmw00000000000000"""ACME utilities.""" import six def map_keys(dikt, func): """Map dictionary keys.""" return dict((func(key), value) for key, value in six.iteritems(dikt)) acme-0.4.1/acme/testdata/0000755000175000017500000000000012665157732014551 5ustar bmwbmw00000000000000acme-0.4.1/acme/testdata/README0000644000175000017500000000100612665157707015430 0ustar bmwbmw00000000000000In order for acme.test_util._guess_loader to work properly, make sure to use appropriate extension for vector filenames: .pem for PEM and .der for DER. The following command has been used to generate test keys: for x in 256 512 1024 2048; do openssl genrsa -out rsa${k}_key.pem $k; done and for the CSR: openssl req -key rsa2048_key.pem -new -subj '/CN=example.com' -outform DER > csr.der and for the certificate: openssl req -key rsa2047_key.pem -new -subj '/CN=example.com' -x509 -outform DER > cert.der acme-0.4.1/acme/testdata/csr.pem0000644000175000017500000000104612665157707016046 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBXTCCAQcCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoCkwJwYJKoZIhvcN AQkOMRowGDAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAANB AHJH/O6BtC9aGzEVCMGOZ7z9iIRHWSzr9x/bOzn7hLwsbXPAgO1QxEwL+X+4g20G n9XBE1N9W6HCIEut2d8wACg= -----END CERTIFICATE REQUEST----- acme-0.4.1/acme/testdata/cert-san.pem0000644000175000017500000000142212665157707016771 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIICFjCCAcCgAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAAaM2MDQwCQYDVR0TBAIwADAnBgNVHREEIDAeggtleGFt cGxlLmNvbYIPd3d3LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA0EASuvNKFTF nTJsvnSXn52f4BMZJJ2id/kW7+r+FJRm+L20gKQ1aqq8d3e/lzRUrv5SMf1TAOe7 RDjyGMKy5ZgM2w== -----END CERTIFICATE----- acme-0.4.1/acme/testdata/rsa512_key.pem0000644000175000017500000000075512665157707017152 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIIBOgIBAAJBAKx1c7RR7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79 vukFhN9HFoHZiUvOjm0c+pVE6K+EdE/twuUCAwEAAQJAMbrEnJCrQe8YqAbw1/Bn elAzIamndfE3U8bTavf9sgFpS4HL83rhd6PDbvx81ucaJAT/5x048fM/nFl4fzAc mQIhAOF/a9o3EIsDKEmUl+Z1OaOiUxDF3kqWSmALEsmvDhwXAiEAw8ljV5RO/rUp Zu2YMDFq3MKpyyMgBIJ8CxmGRc6gCmMCIGRQzkcmhfqBrhOFwkmozrqIBRIKJIjj 8TRm2LXWZZ2DAiAqVO7PztdNpynugUy4jtbGKKjBrTSNBRGA7OHlUgm0dQIhALQq 6oGU29Vxlvt3k0vmiRKU4AVfLyNXIGtcWcNG46h/ -----END RSA PRIVATE KEY----- acme-0.4.1/acme/testdata/csr-nosans.pem0000644000175000017500000000070412665157707017345 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBFTCBwAIBADBbMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEh MB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRQwEgYDVQQDDAtleGFt cGxlLm9yZzBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQD0thFxUTc2v6qV55wRxfwn BUOeN4bVfu5ywJqy65kzR7T1yZi5TPEiQyM7/3HgBVy9ddFc8RX4vNZaR+ROXNEz AgMBAAGgADANBgkqhkiG9w0BAQsFAANBAMikGL8Ch7hQCStXH7chhDp6+pt2+VSo wgsrPQ2Bw4veDMlSemUrH+4e0TwbbntHfvXTDHWs9P3BiIDJLxFrjuA= -----END CERTIFICATE REQUEST----- acme-0.4.1/acme/testdata/dsa512_key.pem0000644000175000017500000000125412665157707017127 0ustar bmwbmw00000000000000-----BEGIN DSA PARAMETERS----- MIGdAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqfn6GC OixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSPAkEA qfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xmrfvl 41pgNJpgu99YOYqPpS0g7A== -----END DSA PARAMETERS----- -----BEGIN DSA PRIVATE KEY----- MIH5AgEAAkEAwebEoGBfokKQeALHHnAZMQwYU35ILEBdV8oUmzv7qpSVUoHihyqf n6GCOixAKSP8EJYcTilIqPbFbfFyOPlbLwIVANoFHEDiQgknAvKrG78pHzAJdQSP AkEAqfka5Bnl+CeEMpzVZGrOVqZE/LFdZK9eT6YtWjzqtIkf3hwXUVxJsTnBG4xm rfvl41pgNJpgu99YOYqPpS0g7AJATQ2LUzjGQSM6UljcPY5I2OD9THkUR9kH2tth zZd70UoI9btrVaTizgqYShuok94glSQNK0H92JgUk3scJPaAkAIVAMDn61h6vrCE mNv063So6E+eYaIN -----END DSA PRIVATE KEY----- acme-0.4.1/acme/testdata/csr-idnsans.pem0000644000175000017500000000323312665157707017503 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIEpzCCBFECAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIDhjCCA4IGCSqGSIb3DQEJDjGCA3MwggNvMAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIDUwYDVR0RBIIDSjCCA0aCYs+Dz4TPhc+Gz4fP iM+Jz4rPi8+Mz43Pjs+Pz5DPkc+Sz5PPlM+Vz5bPl8+Yz5nPms+bz5zPnc+ez5/P oM+hz6LPo8+kz6XPps+nz6jPqc+qz6vPrM+tz67Pry5pbnZhbGlkgmLPsM+xz7LP s8+0z7XPts+3z7jPuc+6z7vPvM+9z77Pv9mB2YLZg9mE2YXZhtmH2YjZidmK2YvZ jNmN2Y7Zj9mQ2ZHZktmT2ZTZldmW2ZfZmNmZ2ZrZm9mc2Z0uaW52YWxpZIJi2Z7Z n9mg2aHZotmj2aTZpdmm2afZqNmp2arZq9ms2a3Zrtmv2bDZsdmy2bPZtNm12bbZ t9m42bnZutm72bzZvdm+2b/agNqB2oLag9qE2oXahtqH2ojaidqKLmludmFsaWSC YtqL2ozajdqO2o/akNqR2pLak9qU2pXaltqX2pjamdqa2pvanNqd2p7an9qg2qHa otqj2qTapdqm2qfaqNqp2qraq9qs2q3artqv2rDasdqy2rPatNq12rbaty5pbnZh bGlkgmLauNq52rrau9q82r3avtq/24DbgduC24PbhNuF24bbh9uI24nbituL24zb jduO24/bkNuR25Lbk9uU25XbltuX25jbmdua25vbnNud257bn9ug26Hbotuj26Qu aW52YWxpZIJ426Xbptun26jbqduq26vbrNut267br9uw27Hbstuz27Tbtdu227fb uNu527rbu+GgoOGgoeGgouGgo+GgpOGgpeGgpuGgp+GgqOGgqeGgquGgq+GgrOGg reGgruGgr+GgsOGgseGgsuGgs+GgtOGgtS5pbnZhbGlkgoGP4aC24aC34aC44aC5 4aC64aC74aC84aC94aC+4aC/4aGA4aGB4aGC4aGD4aGE4aGF4aGG4aGH4aGI4aGJ 4aGK4aGL4aGM4aGN4aGO4aGP4aGQ4aGR4aGS4aGT4aGU4aGV4aGW4aGX4aGY4aGZ 4aGa4aGb4aGc4aGd4aGe4aGf4aGg4aGh4aGiLmludmFsaWSCROGho+GhpOGhpeGh puGhp+GhqOGhqeGhquGhq+GhrOGhreGhruGhr+GhsOGhseGhsuGhs+GhtOGhteGh ti5pbnZhbGlkMA0GCSqGSIb3DQEBCwUAA0EAeNkY0M0+kMnjRo6dEUoGE4dX9fEr dfGrpPUBcwG0P5QBdZJWvZxTfRl14yuPYHbGHULXeGqRdkU6HK5pOlzpng== -----END CERTIFICATE REQUEST----- acme-0.4.1/acme/testdata/csr-100sans.pem0000644000175000017500000000502112665157707017226 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIHNTCCBt8CAQAwZDELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuY2lz Y28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRpZXIgRm91bmRhdGlvbjEUMBIG A1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0BAQEFAANLADBIAkEArHVztFHt H92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580rv2+6QWE30cWgdmJS86ObRz6 lUTor4R0T+3C5QIDAQABoIIGFDCCBhAGCSqGSIb3DQEJDjGCBgEwggX9MAkGA1Ud EwQCMAAwCwYDVR0PBAQDAgXgMIIF4QYDVR0RBIIF2DCCBdSCDGV4YW1wbGUxLmNv bYIMZXhhbXBsZTIuY29tggxleGFtcGxlMy5jb22CDGV4YW1wbGU0LmNvbYIMZXhh bXBsZTUuY29tggxleGFtcGxlNi5jb22CDGV4YW1wbGU3LmNvbYIMZXhhbXBsZTgu Y29tggxleGFtcGxlOS5jb22CDWV4YW1wbGUxMC5jb22CDWV4YW1wbGUxMS5jb22C DWV4YW1wbGUxMi5jb22CDWV4YW1wbGUxMy5jb22CDWV4YW1wbGUxNC5jb22CDWV4 YW1wbGUxNS5jb22CDWV4YW1wbGUxNi5jb22CDWV4YW1wbGUxNy5jb22CDWV4YW1w bGUxOC5jb22CDWV4YW1wbGUxOS5jb22CDWV4YW1wbGUyMC5jb22CDWV4YW1wbGUy MS5jb22CDWV4YW1wbGUyMi5jb22CDWV4YW1wbGUyMy5jb22CDWV4YW1wbGUyNC5j b22CDWV4YW1wbGUyNS5jb22CDWV4YW1wbGUyNi5jb22CDWV4YW1wbGUyNy5jb22C DWV4YW1wbGUyOC5jb22CDWV4YW1wbGUyOS5jb22CDWV4YW1wbGUzMC5jb22CDWV4 YW1wbGUzMS5jb22CDWV4YW1wbGUzMi5jb22CDWV4YW1wbGUzMy5jb22CDWV4YW1w bGUzNC5jb22CDWV4YW1wbGUzNS5jb22CDWV4YW1wbGUzNi5jb22CDWV4YW1wbGUz Ny5jb22CDWV4YW1wbGUzOC5jb22CDWV4YW1wbGUzOS5jb22CDWV4YW1wbGU0MC5j b22CDWV4YW1wbGU0MS5jb22CDWV4YW1wbGU0Mi5jb22CDWV4YW1wbGU0My5jb22C DWV4YW1wbGU0NC5jb22CDWV4YW1wbGU0NS5jb22CDWV4YW1wbGU0Ni5jb22CDWV4 YW1wbGU0Ny5jb22CDWV4YW1wbGU0OC5jb22CDWV4YW1wbGU0OS5jb22CDWV4YW1w bGU1MC5jb22CDWV4YW1wbGU1MS5jb22CDWV4YW1wbGU1Mi5jb22CDWV4YW1wbGU1 My5jb22CDWV4YW1wbGU1NC5jb22CDWV4YW1wbGU1NS5jb22CDWV4YW1wbGU1Ni5j b22CDWV4YW1wbGU1Ny5jb22CDWV4YW1wbGU1OC5jb22CDWV4YW1wbGU1OS5jb22C DWV4YW1wbGU2MC5jb22CDWV4YW1wbGU2MS5jb22CDWV4YW1wbGU2Mi5jb22CDWV4 YW1wbGU2My5jb22CDWV4YW1wbGU2NC5jb22CDWV4YW1wbGU2NS5jb22CDWV4YW1w bGU2Ni5jb22CDWV4YW1wbGU2Ny5jb22CDWV4YW1wbGU2OC5jb22CDWV4YW1wbGU2 OS5jb22CDWV4YW1wbGU3MC5jb22CDWV4YW1wbGU3MS5jb22CDWV4YW1wbGU3Mi5j b22CDWV4YW1wbGU3My5jb22CDWV4YW1wbGU3NC5jb22CDWV4YW1wbGU3NS5jb22C DWV4YW1wbGU3Ni5jb22CDWV4YW1wbGU3Ny5jb22CDWV4YW1wbGU3OC5jb22CDWV4 YW1wbGU3OS5jb22CDWV4YW1wbGU4MC5jb22CDWV4YW1wbGU4MS5jb22CDWV4YW1w bGU4Mi5jb22CDWV4YW1wbGU4My5jb22CDWV4YW1wbGU4NC5jb22CDWV4YW1wbGU4 NS5jb22CDWV4YW1wbGU4Ni5jb22CDWV4YW1wbGU4Ny5jb22CDWV4YW1wbGU4OC5j b22CDWV4YW1wbGU4OS5jb22CDWV4YW1wbGU5MC5jb22CDWV4YW1wbGU5MS5jb22C DWV4YW1wbGU5Mi5jb22CDWV4YW1wbGU5My5jb22CDWV4YW1wbGU5NC5jb22CDWV4 YW1wbGU5NS5jb22CDWV4YW1wbGU5Ni5jb22CDWV4YW1wbGU5Ny5jb22CDWV4YW1w bGU5OC5jb22CDWV4YW1wbGU5OS5jb22CDmV4YW1wbGUxMDAuY29tMA0GCSqGSIb3 DQEBCwUAA0EAW05UMFavHn2rkzMyUfzsOvWzVNlm43eO2yHu5h5TzDb23gkDnNEo duUAbQ+CLJHYd+MvRCmPQ+3ZnaPy7l/0Hg== -----END CERTIFICATE REQUEST----- acme-0.4.1/acme/testdata/rsa256_key.pem0000644000175000017500000000045212665157707017151 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIGrAgEAAiEAm2Fylv+Uz7trgTW8EBHP3FQSMeZs2GNQ6VRo1sIVJEkCAwEAAQIh AJT0BA/xD01dFCAXzSNyj9nfSZa3NpqzJZZn/eOm7vghAhEAzUVNZn4lLLBD1R6N E8TKNQIRAMHHyn3O5JeY36lwKwkUlEUCEAliRauN0L0+QZuYjfJ9aJECEGx4dru3 rTPCyighdqWNlHUCEQCiLjlwSRtWgmMBudCkVjzt -----END RSA PRIVATE KEY----- acme-0.4.1/acme/testdata/cert-idnsans.pem0000644000175000017500000000351212665157707017651 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIFNjCCBOCgAwIBAgIJAP4rNqqOKifCMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjIwMDg1OFoXDTE2MDEwNzIwMDg1OFowZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IDczCCA28wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggNTBgNVHREEggNKMIIDRoJiz4PPhM+Fz4bPh8+I z4nPis+Lz4zPjc+Oz4/PkM+Rz5LPk8+Uz5XPls+Xz5jPmc+az5vPnM+dz57Pn8+g z6HPos+jz6TPpc+mz6fPqM+pz6rPq8+sz63Prs+vLmludmFsaWSCYs+wz7HPss+z z7TPtc+2z7fPuM+5z7rPu8+8z73Pvs+/2YHZgtmD2YTZhdmG2YfZiNmJ2YrZi9mM 2Y3ZjtmP2ZDZkdmS2ZPZlNmV2ZbZl9mY2ZnZmtmb2ZzZnS5pbnZhbGlkgmLZntmf 2aDZodmi2aPZpNml2abZp9mo2anZqtmr2azZrdmu2a/ZsNmx2bLZs9m02bXZttm3 2bjZudm62bvZvNm92b7Zv9qA2oHagtqD2oTahdqG2ofaiNqJ2oouaW52YWxpZIJi 2ovajNqN2o7aj9qQ2pHaktqT2pTaldqW2pfamNqZ2pram9qc2p3antqf2qDaodqi 2qPapNql2qbap9qo2qnaqtqr2qzardqu2q/asNqx2rLas9q02rXattq3LmludmFs aWSCYtq42rnautq72rzavdq+2r/bgNuB24Lbg9uE24XbhtuH24jbiduK24vbjNuN 247bj9uQ25HbktuT25TblduW25fbmNuZ25rbm9uc253bntuf26Dbodui26PbpC5p bnZhbGlkgnjbpdum26fbqNup26rbq9us263brtuv27Dbsduy27PbtNu127bbt9u4 27nbutu74aCg4aCh4aCi4aCj4aCk4aCl4aCm4aCn4aCo4aCp4aCq4aCr4aCs4aCt 4aCu4aCv4aCw4aCx4aCy4aCz4aC04aC1LmludmFsaWSCgY/hoLbhoLfhoLjhoLnh oLrhoLvhoLzhoL3hoL7hoL/hoYDhoYHhoYLhoYPhoYThoYXhoYbhoYfhoYjhoYnh oYrhoYvhoYzhoY3hoY7hoY/hoZDhoZHhoZLhoZPhoZThoZXhoZbhoZfhoZjhoZnh oZrhoZvhoZzhoZ3hoZ7hoZ/hoaDhoaHhoaIuaW52YWxpZIJE4aGj4aGk4aGl4aGm 4aGn4aGo4aGp4aGq4aGr4aGs4aGt4aGu4aGv4aGw4aGx4aGy4aGz4aG04aG14aG2 LmludmFsaWQwDQYJKoZIhvcNAQELBQADQQAzOQL/54yXxln87/YvEQbBm9ik9zoT TxEkvnZ4kmTRhDsUPtRjMXhY2FH7LOtXKnJQ7POUB7AsJ2Z6uq2w623G -----END CERTIFICATE----- acme-0.4.1/acme/testdata/csr-6sans.pem0000644000175000017500000000124412665157707017076 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBuzCCAWUCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCE1pY2hpZ2FuMRIw EAYDVQQHEwlBbm4gQXJib3IxDDAKBgNVBAoTA0VGRjEfMB0GA1UECxMWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEA9LYRcVE3Nr+qleecEcX8JwVDnjeG1X7ucsCasuuZM0e0 9cmYuUzxIkMjO/9x4AVcvXXRXPEV+LzWWkfkTlzRMwIDAQABoIGGMIGDBgkqhkiG 9w0BCQ4xdjB0MHIGA1UdEQRrMGmCC2V4YW1wbGUuY29tggtleGFtcGxlLm9yZ4IL ZXhhbXBsZS5uZXSCDGV4YW1wbGUuaW5mb4IVc3ViZG9tYWluLmV4YW1wbGUuY29t ghtvdGhlci5zdWJkb21haW4uZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADQQBd k4BE5qvEvkYoZM/2++Xd9RrQ6wsdj0QiJQCozfsI4lQx6ZJnbtNc7HpDrX4W6XIv IvzVBz/nD11drfz/RNuX -----END CERTIFICATE REQUEST----- acme-0.4.1/acme/testdata/cert.pem0000644000175000017500000000130512665157707016212 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIB3jCCAYigAwIBAgICBTkwDQYJKoZIhvcNAQELBQAwdzELMAkGA1UEBhMCVVMx ETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3IxKzApBgNVBAoM IlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDASBgNVBAMMC2V4 YW1wbGUuY29tMB4XDTE0MTIxMTIyMzQ0NVoXDTE0MTIxODIyMzQ0NVowdzELMAkG A1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIwEAYDVQQHDAlBbm4gQXJib3Ix KzApBgNVBAoMIlVuaXZlcnNpdHkgb2YgTWljaGlnYW4gYW5kIHRoZSBFRkYxFDAS BgNVBAMMC2V4YW1wbGUuY29tMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKx1c7RR 7R/drnBSQ/zfx1vQLHUbFLh1AQQQ5R8DZUXd36efNK79vukFhN9HFoHZiUvOjm0c +pVE6K+EdE/twuUCAwEAATANBgkqhkiG9w0BAQsFAANBAC24z0IdwIVKSlntksll vr6zJepBH5fMndfk3XJp10jT6VE+14KNtjh02a56GoraAvJAT5/H67E8GvJ/ocNn B/o= -----END CERTIFICATE----- acme-0.4.1/acme/testdata/cert-100sans.pem0000644000175000017500000000530012665157707017374 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE----- MIIHxDCCB26gAwIBAgIJAOGrG1Un9lHiMA0GCSqGSIb3DQEBCwUAMGQxCzAJBgNV BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbmNpc2NvMScwJQYDVQQLDB5FbGVjdHJv bmljIEZyb250aWVyIEZvdW5kYXRpb24xFDASBgNVBAMMC2V4YW1wbGUuY29tMB4X DTE2MDEwNjE5MDkzN1oXDTE2MDEwNzE5MDkzN1owZDELMAkGA1UECAwCQ0ExFjAU BgNVBAcMDVNhbiBGcmFuY2lzY28xJzAlBgNVBAsMHkVsZWN0cm9uaWMgRnJvbnRp ZXIgRm91bmRhdGlvbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG9w0B AQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3fp580 rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABo4IGATCCBf0wCQYDVR0T BAIwADALBgNVHQ8EBAMCBeAwggXhBgNVHREEggXYMIIF1IIMZXhhbXBsZTEuY29t ggxleGFtcGxlMi5jb22CDGV4YW1wbGUzLmNvbYIMZXhhbXBsZTQuY29tggxleGFt cGxlNS5jb22CDGV4YW1wbGU2LmNvbYIMZXhhbXBsZTcuY29tggxleGFtcGxlOC5j b22CDGV4YW1wbGU5LmNvbYINZXhhbXBsZTEwLmNvbYINZXhhbXBsZTExLmNvbYIN ZXhhbXBsZTEyLmNvbYINZXhhbXBsZTEzLmNvbYINZXhhbXBsZTE0LmNvbYINZXhh bXBsZTE1LmNvbYINZXhhbXBsZTE2LmNvbYINZXhhbXBsZTE3LmNvbYINZXhhbXBs ZTE4LmNvbYINZXhhbXBsZTE5LmNvbYINZXhhbXBsZTIwLmNvbYINZXhhbXBsZTIx LmNvbYINZXhhbXBsZTIyLmNvbYINZXhhbXBsZTIzLmNvbYINZXhhbXBsZTI0LmNv bYINZXhhbXBsZTI1LmNvbYINZXhhbXBsZTI2LmNvbYINZXhhbXBsZTI3LmNvbYIN ZXhhbXBsZTI4LmNvbYINZXhhbXBsZTI5LmNvbYINZXhhbXBsZTMwLmNvbYINZXhh bXBsZTMxLmNvbYINZXhhbXBsZTMyLmNvbYINZXhhbXBsZTMzLmNvbYINZXhhbXBs ZTM0LmNvbYINZXhhbXBsZTM1LmNvbYINZXhhbXBsZTM2LmNvbYINZXhhbXBsZTM3 LmNvbYINZXhhbXBsZTM4LmNvbYINZXhhbXBsZTM5LmNvbYINZXhhbXBsZTQwLmNv bYINZXhhbXBsZTQxLmNvbYINZXhhbXBsZTQyLmNvbYINZXhhbXBsZTQzLmNvbYIN ZXhhbXBsZTQ0LmNvbYINZXhhbXBsZTQ1LmNvbYINZXhhbXBsZTQ2LmNvbYINZXhh bXBsZTQ3LmNvbYINZXhhbXBsZTQ4LmNvbYINZXhhbXBsZTQ5LmNvbYINZXhhbXBs ZTUwLmNvbYINZXhhbXBsZTUxLmNvbYINZXhhbXBsZTUyLmNvbYINZXhhbXBsZTUz LmNvbYINZXhhbXBsZTU0LmNvbYINZXhhbXBsZTU1LmNvbYINZXhhbXBsZTU2LmNv bYINZXhhbXBsZTU3LmNvbYINZXhhbXBsZTU4LmNvbYINZXhhbXBsZTU5LmNvbYIN ZXhhbXBsZTYwLmNvbYINZXhhbXBsZTYxLmNvbYINZXhhbXBsZTYyLmNvbYINZXhh bXBsZTYzLmNvbYINZXhhbXBsZTY0LmNvbYINZXhhbXBsZTY1LmNvbYINZXhhbXBs ZTY2LmNvbYINZXhhbXBsZTY3LmNvbYINZXhhbXBsZTY4LmNvbYINZXhhbXBsZTY5 LmNvbYINZXhhbXBsZTcwLmNvbYINZXhhbXBsZTcxLmNvbYINZXhhbXBsZTcyLmNv bYINZXhhbXBsZTczLmNvbYINZXhhbXBsZTc0LmNvbYINZXhhbXBsZTc1LmNvbYIN ZXhhbXBsZTc2LmNvbYINZXhhbXBsZTc3LmNvbYINZXhhbXBsZTc4LmNvbYINZXhh bXBsZTc5LmNvbYINZXhhbXBsZTgwLmNvbYINZXhhbXBsZTgxLmNvbYINZXhhbXBs ZTgyLmNvbYINZXhhbXBsZTgzLmNvbYINZXhhbXBsZTg0LmNvbYINZXhhbXBsZTg1 LmNvbYINZXhhbXBsZTg2LmNvbYINZXhhbXBsZTg3LmNvbYINZXhhbXBsZTg4LmNv bYINZXhhbXBsZTg5LmNvbYINZXhhbXBsZTkwLmNvbYINZXhhbXBsZTkxLmNvbYIN ZXhhbXBsZTkyLmNvbYINZXhhbXBsZTkzLmNvbYINZXhhbXBsZTk0LmNvbYINZXhh bXBsZTk1LmNvbYINZXhhbXBsZTk2LmNvbYINZXhhbXBsZTk3LmNvbYINZXhhbXBs ZTk4LmNvbYINZXhhbXBsZTk5LmNvbYIOZXhhbXBsZTEwMC5jb20wDQYJKoZIhvcN AQELBQADQQBEunJbKUXcyNKTSfA0pKRyWNiKmkoBqYgfZS6eHNrNH/hjFzHtzyDQ XYHHK6kgEWBvHfRXGmqhFvht+b1tQKkG -----END CERTIFICATE----- acme-0.4.1/acme/testdata/csr.der0000644000175000017500000000113712665157707016040 0ustar bmwbmw000000000000000‚[0‚C010U example.com0‚"0  *†H†÷ ‚0‚ ‚ð|0wˆ˜â3Ïçð$ÂG‚žav€±ŽÞÿ1.k>=a÷e¼\u[DŸnXß‹"¢:áÄjP]t±ÊeMÞpŸš›© ¹¹ÛŽ*ô厘8(¸iú•Ãf 3-/Üe\všóuãi ÔD™KªîlKƒË· Æn€šÇþaÁl3gð×3$áa°LOØGLç) 3S¦ñª“ª£Ì€û›ëÏëá,¦øËžôjå s‡øÂ|àH"0ƒycî_ÈMä=À.Û¢ºh§5J‹Ihº¿^+üWɯž¸FîÐø½Ws½;?Gsåƒ9_̆eÕÆžþT4)³k§ 0  *†H†÷  ‚rö0Jê0*¢ŸŒ»;s kµÀöÏ[-ÑI%­æI!¤6gu%‘i¯ãÌ8OÔû¥ð}‹D6¤±>é:€}¬_QMKYBÃÄ4a8ôÄÔòš‡×6Ì1þpÀ @T EXá•ÁëáUž¯¥ÅBï• PrÅ>h†6•«(•.¢Îœhw Ù–€qa›ÎQöÉÍ_²PÎâ—kÏNš=ÖEК¢S?ôýE ëÎ %îO±™d±Ñ»ÓK=ª.ðgh¥›ì²¶½î³¢Be+õ_)uUusæZQÃVûù8ŸÅês0Qˆr½;ù`ÅzŒä=FI´acme-0.4.1/acme/testdata/cert.der0000644000175000017500000000140312665157707016202 0ustar bmwbmw000000000000000‚ÿ0‚ç  Á‚2WZŒdª0  *†H†÷  010U example.com0 151028072441Z 151127072441Z010U example.com0‚"0  *†H†÷ ‚0‚ ‚ð|0wˆ˜â3Ïçð$ÂG‚žav€±ŽÞÿ1.k>=a÷e¼\u[DŸnXß‹"¢:áÄjP]t±ÊeMÞpŸš›© ¹¹ÛŽ*ô厘8(¸iú•Ãf 3-/Üe\všóuãi ÔD™KªîlKƒË· Æn€šÇþaÁl3gð×3$áa°LOØGLç) 3S¦ñª“ª£Ì€û›ëÏëá,¦øËžôjå s‡øÂ|àH"0ƒycî_ÈMä=À.Û¢ºh§5J‹Ihº¿^+üWɯž¸FîÐø½Ws½;?Gsåƒ9_̆eÕÆžþT4)³k§£P0N0Uô6 òÉ )óõ–Ö+e£´¼X70U#0€ô6 òÉ )óõ–Ö+e£´¼X70 U0ÿ0  *†H†÷  ‚íTí2Ùðý£Oú]œíÄJ©7Ý öêÔ£­N^ÀÍÆ ·ºùxê°1µœ§ŽžÓ[òIK…,=^µ2k뮫mHGï[2”Ìü-ŽmÅlhf¸ll@Õ'Dp¥ˆH4¤†ªC¸'µñ{[øC c6¸0u;™d“è FõµAá›zk&eP Ǫa3ú¿šEu…ñP‚jW¼3uô„Îö ³› ó,‰ÖÚS—Ò~äC6Q8²YèOºfs˜ÓÙ U«pªM!È‘)+–m,(hEâ °{b ûÐãë³KêR¼V¢hæÙÅ—‡ô ;ÛÚlv1æ’acme-0.4.1/acme/testdata/rsa2048_key.pem0000644000175000017500000000321312665157707017230 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEA8HwZMHeImB/iM8/n8CTCR4KeYQB2gLGO3v8xLms+PWH3Zbxc dVtEn25Y34scIh+iOuEXBcSBalBddLHKBGVN3nCfmpupoLm52xgRG44q9OWODpg4 FSi4afqVw2agMx0RHi0v3GVcdpqB83UW42kK1ESZHUuq7mxLg8u3IMYZFm6Amsf+ YQjBbDNn8NczJOFhsExP2EdM5ykgM1Om8aqTqqPMgPub68/r4Sym+BjLnvRq5Qtz h/jCfOBIIpAwg3lj7l8OyE3kkD3ALtuiuminNUqLHEkUaLq/Xiv8V8mvnrhG7h3Q +L1Xc707P0dz5YM5XxTMhmUE1cae/lQ0KbNrpwIDAQABAoIBAAiDXCDrGlrIRimv YnaN1pLRfOnSKl/D6VrbjdIm2b0yip9/W4aMBJHgRiUjt4s9s3CCJ1585lftIGHR KWWecHM/aWb/u7GE4Z9v6qsfDUY+GhlKKjIVjvGxfTu9lk446TI4R0l2DR/luFP2 ASlrvoZlJ0ZyN0rZapLv0zvFx32Tukd+3rcMmXfHl7aRGMZG1YTKNmBJ4d9iJ6cP HG3fgSzLQMPLNO/20MzbXdREG5FNQtwaMuFnIcVbtMCvc/71lQQEfANMLCUweEed YWGOjgDeh+731nJsopel+2TSTgnf5VhcFrgChZZdqeKvP+HbXjTE2VkWo7BrzoM7 xICYBwECgYEA/ZF/JOjZfwIMUdikv8vldJzRMdFryl4NJWnh4NeThNOEpUcpxnyK wyMnnQaGJa51u9EEnzl0sZ2h2ODjD6KFpz6fkWaVRq5SWalVPAoKZGaoPZV3IUOI 8Tm0xkXho+A/FUUEcxCLME+3V9EdPfHaVRJOrbfDyxvNhsj4w9F0aAkCgYEA8sp7 XTrolOknJGv4Qt1w6gcm5+cMtLaRfi8ZHPHujl2x9eWE8/s2818az7jc0Xr/G4HQ NeU+3Es4BblEckSHmhUZhx26cZgkLSIIDofEtaEc6u8CyWfxsWvn3l4T3kMdeSLC 9UoLk59AH2tkMIh8vzV8LSisLJa341lMdgryQi8CgYAlJKr7PSCe+i3Tz2hSsAts iYwbQBIKErzaPihYRzvUuSc1DreP26535y5mUg5UdrnISVXj/Qaa/fw3SLn6EFSD qyi0o9I6CE8H00YpBU+AZYk/fCV3Oe1VaJ6SbKog1zhmZTXBpSq+aO7ybi9aY5MX 4xajW8fSeMAifk3yYTwsAQKBgErcEcOCOVpItU/uloKPYpRWFjHktK83p46fmP+q vOJak1d9KExOBfhuN4caucNBSE1D7l3fzE0CSEjDgg41gRYKMW/Ow8DopybfWlqY lBdokNEDVvmgug35dmnC2h9q1DiYdkJJTV57+Lp3U1H/k28lX59Q7h1lb1eDHic7 YszzAoGBAOx05dhOiYbzAJSTQu3oBHFn4mTYIqCcDO6cQrEJwPKAq7mAhT0yOk9N CrqRV/1aes665829cyTwcAZl6nqbzHv5XjX5+g6vmooCb4oCkq49rumHjoQdrX8D RR5b+Spkc1jo4rctCcExzSkgo+K5N3oBVYznecje7O7Z0/qiJE/8 -----END RSA PRIVATE KEY----- acme-0.4.1/acme/testdata/rsa1024_key.pem0000644000175000017500000000156712665157707017233 0ustar bmwbmw00000000000000-----BEGIN RSA PRIVATE KEY----- MIICXAIBAAKBgQCaifO0fGlcAcjjcYEAPYcIL0Hf0KiNa9VCJ14RBdlZxLWRrVFi 4tdNCKSKqzKuKrrA8DWd4PHFD7UpLyRrPPXY6GozAyCT+5UFBClGJ2KyNKu+eU6/ w4C1kpO4lpeXs8ptFc1lA9P8V1M/MkWzTE402nPNK0uUmZNo2tsFpGJUSQIDAQAB AoGAFjLWxQhSAhtnhfRZ+XTdHrnbFpFchOQGgDgzdPKIJDLzefeRh0jacIBbUmgB Ia+Vn/1hVkpnsEzvUvkonBbnoYWlYVQdpNTmrrew7SOztf8/1fYCsSkyDAvqGTXc TmHM0PaLS+junoWcKOvQRVb0N3k+43OnBkr2b393Sx30qGECQQDNO2IBWOsYs8cB CZQAZs8zBlbwBFZibqovqpLwXt9adBIsT9XzgagGbJMpzSuoHTUn3QqqJd9uHD8X UTmmoh4NAkEAwMRauo+PlNj8W1lusflko52KL17+E5cmeOERM2xvhZNpO7d3/1ak Co9dxVMicrYSh7jXbcXFNt3xNDTv6Dg8LQJAPuJwMDt/pc0IMCAwMkNOP7M0lkyt 73E7QmnAplhblcq0+tDnnLpgsr84BHnyY4u3iuRm7SW3pXSQPGPOB2nrTQJANBXa HgakWSe4KEal7ljgpITwzZPxOwHgV1EZALgP+hu2l3gfaFLUyDWstKCd8jjYEOwU 6YhCnWyiu+SB3lEzkQJBAJapJpfypFyY8kQNYlYILLBcPu5fmy3QUZKHJ4L3rIVJ c2UTLMeBBgGFHT04CtWntmjwzSv+V6lwiCxKXsIUySc= -----END RSA PRIVATE KEY----- acme-0.4.1/acme/testdata/csr-san.pem0000644000175000017500000000107612665157707016630 0ustar bmwbmw00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIBbjCCARgCAQAweTELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE1pY2hpZ2FuMRIw EAYDVQQHDAlBbm4gQXJib3IxDDAKBgNVBAoMA0VGRjEfMB0GA1UECwwWVW5pdmVy c2l0eSBvZiBNaWNoaWdhbjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wXDANBgkqhkiG 9w0BAQEFAANLADBIAkEArHVztFHtH92ucFJD/N/HW9AsdRsUuHUBBBDlHwNlRd3f p580rv2+6QWE30cWgdmJS86ObRz6lUTor4R0T+3C5QIDAQABoDowOAYJKoZIhvcN AQkOMSswKTAnBgNVHREEIDAeggtleGFtcGxlLmNvbYIPd3d3LmV4YW1wbGUuY29t MA0GCSqGSIb3DQEBCwUAA0EAZGBM8J1rRs7onFgtc76mOeoT1c3v0ZsEmxQfb2Wy tmReY6X1N4cs38D9VSow+VMRu2LWkKvzS7RUFSaTaeQz1A== -----END CERTIFICATE REQUEST----- acme-0.4.1/acme/challenges_test.py0000644000175000017500000005751412665157707016474 0ustar bmwbmw00000000000000"""Tests for acme.challenges.""" import unittest import mock import OpenSSL 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 other from acme import test_util CERT = test_util.load_comparable_cert('cert.pem') KEY = jose.JWKRSA(key=test_util.load_rsa_private_key('rsa512_key.pem')) class ChallengeTest(unittest.TestCase): def test_from_json_unrecognized(self): from acme.challenges import Challenge from acme.challenges import UnrecognizedChallenge chall = UnrecognizedChallenge({"type": "foo"}) # pylint: disable=no-member self.assertEqual(chall, Challenge.from_json(chall.jobj)) class UnrecognizedChallengeTest(unittest.TestCase): def setUp(self): from acme.challenges import UnrecognizedChallenge self.jobj = {"type": "foo"} self.chall = UnrecognizedChallenge(self.jobj) def test_to_partial_json(self): self.assertEqual(self.jobj, self.chall.to_partial_json()) def test_from_json(self): from acme.challenges import UnrecognizedChallenge self.assertEqual( self.chall, UnrecognizedChallenge.from_json(self.jobj)) class KeyAuthorizationChallengeResponseTest(unittest.TestCase): def setUp(self): def _encode(name): assert name == "token" return "foo" self.chall = mock.Mock() self.chall.encode.side_effect = _encode def test_verify_ok(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertTrue(response.verify(self.chall, KEY.public_key())) def test_verify_wrong_token(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='bar.oKGqedy-b-acd5eoybm2f-NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) def test_verify_wrong_thumbprint(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='foo.oKGqedy-b-acd5eoybm2f-NVFxv') self.assertFalse(response.verify(self.chall, KEY.public_key())) def test_verify_wrong_form(self): from acme.challenges import KeyAuthorizationChallengeResponse response = KeyAuthorizationChallengeResponse( key_authorization='.foo.oKGqedy-b-acd5eoybm2f-' 'NVFxvyOoET5CNy3xnv8WY') self.assertFalse(response.verify(self.chall, KEY.public_key())) class HTTP01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): from acme.challenges import HTTP01Response self.msg = HTTP01Response(key_authorization=u'foo') self.jmsg = { 'resource': 'challenge', 'type': 'http-01', 'keyAuthorization': u'foo', } from acme.challenges import HTTP01 self.chall = HTTP01(token=(b'x' * 16)) self.response = self.chall.response(KEY) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01Response self.assertEqual( self.msg, HTTP01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import HTTP01Response hash(HTTP01Response.from_json(self.jmsg)) def test_simple_verify_bad_key_authorization(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) @mock.patch("acme.challenges.requests.get") def test_simple_verify_good_validation(self, mock_get): validation = self.chall.validation(KEY) mock_get.return_value = mock.MagicMock(text=validation) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local")) @mock.patch("acme.challenges.requests.get") def test_simple_verify_bad_validation(self, mock_get): mock_get.return_value = mock.MagicMock(text="!") self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @mock.patch("acme.challenges.requests.get") def test_simple_verify_whitespace_validation(self, mock_get): from acme.challenges import HTTP01Response mock_get.return_value = mock.MagicMock( text=(self.chall.validation(KEY) + HTTP01Response.WHITESPACE_CUTSET)) self.assertTrue(self.response.simple_verify( self.chall, "local", KEY.public_key())) mock_get.assert_called_once_with(self.chall.uri("local")) @mock.patch("acme.challenges.requests.get") def test_simple_verify_connection_error(self, mock_get): mock_get.side_effect = requests.exceptions.RequestException self.assertFalse(self.response.simple_verify( self.chall, "local", KEY.public_key())) @mock.patch("acme.challenges.requests.get") def test_simple_verify_port(self, mock_get): self.response.simple_verify( self.chall, domain="local", account_public_key=KEY.public_key(), port=8080) self.assertEqual("local:8080", urllib_parse.urlparse( mock_get.mock_calls[0][1][0]).netloc) class HTTP01Test(unittest.TestCase): def setUp(self): from acme.challenges import HTTP01 self.msg = HTTP01( token=jose.decode_b64jose( 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ+PCt92wr+oA')) self.jmsg = { 'type': 'http-01', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_path(self): self.assertEqual(self.msg.path, '/.well-known/acme-challenge/' 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA') def test_uri(self): self.assertEqual( 'http://example.com/.well-known/acme-challenge/' 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', self.msg.uri('example.com')) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import HTTP01 self.assertEqual(self.msg, HTTP01.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import HTTP01 hash(HTTP01.from_json(self.jmsg)) def test_good_token(self): self.assertTrue(self.msg.good_token) self.assertFalse( self.msg.update(token=b'..').good_token) class TLSSNI01ResponseTest(unittest.TestCase): # pylint: disable=too-many-instance-attributes def setUp(self): from acme.challenges import TLSSNI01 self.chall = TLSSNI01( token=jose.b64decode(b'a82d5ff8ef740d12881f6d3c2277ab2e')) self.response = self.chall.response(KEY) self.jmsg = { 'resource': 'challenge', 'type': 'tls-sni-01', 'keyAuthorization': self.response.key_authorization, } # pylint: disable=invalid-name label1 = b'dc38d9c3fa1a4fdcc3a5501f2d38583f' label2 = b'b7793728f084394f2a1afd459556bb5c' self.z = label1 + label2 self.z_domain = label1 + b'.' + label2 + b'.acme.invalid' self.domain = 'foo.com' def test_z_and_domain(self): self.assertEqual(self.z, self.response.z) self.assertEqual(self.z_domain, self.response.z_domain) def test_to_partial_json(self): self.assertEqual(self.jmsg, self.response.to_partial_json()) def test_from_json(self): from acme.challenges import TLSSNI01Response self.assertEqual(self.response, TLSSNI01Response.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import TLSSNI01Response hash(TLSSNI01Response.from_json(self.jmsg)) @mock.patch('acme.challenges.socket.gethostbyname') @mock.patch('acme.challenges.crypto_util.probe_sni') def test_probe_cert(self, mock_probe_sni, mock_gethostbyname): mock_gethostbyname.return_value = '127.0.0.1' self.response.probe_cert('foo.com') mock_gethostbyname.assert_called_once_with('foo.com') mock_probe_sni.assert_called_once_with( host='127.0.0.1', port=self.response.PORT, name=self.z_domain) self.response.probe_cert('foo.com', host='8.8.8.8') mock_probe_sni.assert_called_with( host='8.8.8.8', port=mock.ANY, name=mock.ANY) self.response.probe_cert('foo.com', port=1234) mock_probe_sni.assert_called_with( host=mock.ANY, port=1234, name=mock.ANY) self.response.probe_cert('foo.com', bar='baz') mock_probe_sni.assert_called_with( host=mock.ANY, port=mock.ANY, name=mock.ANY, bar='baz') self.response.probe_cert('foo.com', name=b'xxx') mock_probe_sni.assert_called_with( host=mock.ANY, port=mock.ANY, name=self.z_domain) def test_gen_verify_cert(self): key1 = test_util.load_pyopenssl_private_key('rsa512_key.pem') cert, key2 = self.response.gen_cert(key1) self.assertEqual(key1, key2) self.assertTrue(self.response.verify_cert(cert)) def test_gen_verify_cert_gen_key(self): cert, key = self.response.gen_cert() self.assertTrue(isinstance(key, OpenSSL.crypto.PKey)) self.assertTrue(self.response.verify_cert(cert)) def test_verify_bad_cert(self): self.assertFalse(self.response.verify_cert( test_util.load_cert('cert.pem'))) def test_simple_verify_bad_key_authorization(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa256_key.pem')) self.response.simple_verify(self.chall, "local", key2.public_key()) @mock.patch('acme.challenges.TLSSNI01Response.verify_cert', autospec=True) def test_simple_verify(self, mock_verify_cert): mock_verify_cert.return_value = mock.sentinel.verification self.assertEqual( mock.sentinel.verification, self.response.simple_verify( self.chall, self.domain, KEY.public_key(), cert=mock.sentinel.cert)) mock_verify_cert.assert_called_once_with( self.response, mock.sentinel.cert) @mock.patch('acme.challenges.TLSSNI01Response.probe_cert') def test_simple_verify_false_on_probe_error(self, mock_probe_cert): mock_probe_cert.side_effect = errors.Error self.assertFalse(self.response.simple_verify( self.chall, self.domain, KEY.public_key())) class TLSSNI01Test(unittest.TestCase): def setUp(self): from acme.challenges import TLSSNI01 self.msg = TLSSNI01( token=jose.b64decode('a82d5ff8ef740d12881f6d3c2277ab2e')) self.jmsg = { 'type': 'tls-sni-01', 'token': 'a82d5ff8ef740d12881f6d3c2277ab2e', } def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import TLSSNI01 self.assertEqual(self.msg, TLSSNI01.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import TLSSNI01 hash(TLSSNI01.from_json(self.jmsg)) def test_from_json_invalid_token_length(self): from acme.challenges import TLSSNI01 self.jmsg['token'] = jose.encode_b64jose(b'abcd') self.assertRaises( jose.DeserializationError, TLSSNI01.from_json, self.jmsg) @mock.patch('acme.challenges.TLSSNI01Response.gen_cert') def test_validation(self, mock_gen_cert): mock_gen_cert.return_value = ('cert', 'key') self.assertEqual(('cert', 'key'), self.msg.validation( KEY, cert_key=mock.sentinel.cert_key)) mock_gen_cert.assert_called_once_with(key=mock.sentinel.cert_key) class RecoveryContactTest(unittest.TestCase): def setUp(self): from acme.challenges import RecoveryContact self.msg = RecoveryContact( activation_url='https://example.ca/sendrecovery/a5bd99383fb0', success_url='https://example.ca/confirmrecovery/bb1b9928932', contact='c********n@example.com') self.jmsg = { 'type': 'recoveryContact', 'activationURL': 'https://example.ca/sendrecovery/a5bd99383fb0', 'successURL': 'https://example.ca/confirmrecovery/bb1b9928932', 'contact': 'c********n@example.com', } def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import RecoveryContact self.assertEqual(self.msg, RecoveryContact.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import RecoveryContact hash(RecoveryContact.from_json(self.jmsg)) def test_json_without_optionals(self): del self.jmsg['activationURL'] del self.jmsg['successURL'] del self.jmsg['contact'] from acme.challenges import RecoveryContact msg = RecoveryContact.from_json(self.jmsg) self.assertTrue(msg.activation_url is None) self.assertTrue(msg.success_url is None) self.assertTrue(msg.contact is None) self.assertEqual(self.jmsg, msg.to_partial_json()) class RecoveryContactResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import RecoveryContactResponse self.msg = RecoveryContactResponse(token='23029d88d9e123e') self.jmsg = { 'resource': 'challenge', 'type': 'recoveryContact', 'token': '23029d88d9e123e', } def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import RecoveryContactResponse self.assertEqual( self.msg, RecoveryContactResponse.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import RecoveryContactResponse hash(RecoveryContactResponse.from_json(self.jmsg)) def test_json_without_optionals(self): del self.jmsg['token'] from acme.challenges import RecoveryContactResponse msg = RecoveryContactResponse.from_json(self.jmsg) self.assertTrue(msg.token is None) self.assertEqual(self.jmsg, msg.to_partial_json()) class ProofOfPossessionHintsTest(unittest.TestCase): def setUp(self): jwk = KEY.public_key() issuers = ( 'C=US, O=SuperT LLC, CN=SuperTrustworthy Public CA', 'O=LessTrustworthy CA Inc, CN=LessTrustworthy But StillSecure', ) cert_fingerprints = ( '93416768eb85e33adc4277f4c9acd63e7418fcfe', '16d95b7b63f1972b980b14c20291f3c0d1855d95', '48b46570d9fc6358108af43ad1649484def0debf', ) subject_key_identifiers = ('d0083162dcc4c8a23ecb8aecbd86120e56fd24e5') authorized_for = ('www.example.com', 'example.net') serial_numbers = (34234239832, 23993939911, 17) from acme.challenges import ProofOfPossession self.msg = ProofOfPossession.Hints( jwk=jwk, issuers=issuers, cert_fingerprints=cert_fingerprints, certs=(CERT,), subject_key_identifiers=subject_key_identifiers, authorized_for=authorized_for, serial_numbers=serial_numbers) self.jmsg_to = { 'jwk': jwk, 'certFingerprints': cert_fingerprints, 'certs': (jose.encode_b64jose(OpenSSL.crypto.dump_certificate( OpenSSL.crypto.FILETYPE_ASN1, CERT.wrapped)),), 'subjectKeyIdentifiers': subject_key_identifiers, 'serialNumbers': serial_numbers, 'issuers': issuers, 'authorizedFor': authorized_for, } self.jmsg_from = self.jmsg_to.copy() self.jmsg_from.update({'jwk': jwk.to_json()}) def test_to_partial_json(self): self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import ProofOfPossession self.assertEqual( self.msg, ProofOfPossession.Hints.from_json(self.jmsg_from)) def test_from_json_hashable(self): from acme.challenges import ProofOfPossession hash(ProofOfPossession.Hints.from_json(self.jmsg_from)) def test_json_without_optionals(self): for optional in ['certFingerprints', 'certs', 'subjectKeyIdentifiers', 'serialNumbers', 'issuers', 'authorizedFor']: del self.jmsg_from[optional] del self.jmsg_to[optional] from acme.challenges import ProofOfPossession msg = ProofOfPossession.Hints.from_json(self.jmsg_from) self.assertEqual(msg.cert_fingerprints, ()) self.assertEqual(msg.certs, ()) self.assertEqual(msg.subject_key_identifiers, ()) self.assertEqual(msg.serial_numbers, ()) self.assertEqual(msg.issuers, ()) self.assertEqual(msg.authorized_for, ()) self.assertEqual(self.jmsg_to, msg.to_partial_json()) class ProofOfPossessionTest(unittest.TestCase): def setUp(self): from acme.challenges import ProofOfPossession hints = ProofOfPossession.Hints( jwk=KEY.public_key(), cert_fingerprints=(), certs=(), serial_numbers=(), subject_key_identifiers=(), issuers=(), authorized_for=()) self.msg = ProofOfPossession( alg=jose.RS256, hints=hints, nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ') self.jmsg_to = { 'type': 'proofOfPossession', 'alg': jose.RS256, 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', 'hints': hints, } self.jmsg_from = { 'type': 'proofOfPossession', 'alg': jose.RS256.to_json(), 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', 'hints': hints.to_json(), } def test_to_partial_json(self): self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import ProofOfPossession self.assertEqual( self.msg, ProofOfPossession.from_json(self.jmsg_from)) def test_from_json_hashable(self): from acme.challenges import ProofOfPossession hash(ProofOfPossession.from_json(self.jmsg_from)) class ProofOfPossessionResponseTest(unittest.TestCase): def setUp(self): # acme-spec uses a confusing example in which both signature # nonce and challenge nonce are the same, don't make the same # mistake here... signature = other.Signature( alg=jose.RS256, jwk=KEY.public_key(), sig=b'\xa7\xc1\xe7\xe82o\xbc\xcd\xd0\x1e\x010#Z|\xaf\x15\x83' b'\x94\x8f#\x9b\nQo(\x80\x15,\x08\xfcz\x1d\xfd\xfd.\xaap' b'\xfa\x06\xd1\xa2f\x8d8X2>%d\xbd%\xe1T\xdd\xaa0\x18\xde' b'\x99\x08\xf0\x0e{', nonce=b'\x99\xc7Q\xb3f2\xbc\xdci\xfe\xd6\x98k\xc67\xdf', ) from acme.challenges import ProofOfPossessionResponse self.msg = ProofOfPossessionResponse( nonce=b'xD\xf9\xb9\xdbU\xed\xaa\x17\xf1y|\x81\x88\x99 ', signature=signature) self.jmsg_to = { 'resource': 'challenge', 'type': 'proofOfPossession', 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', 'signature': signature, } self.jmsg_from = { 'resource': 'challenge', 'type': 'proofOfPossession', 'nonce': 'eET5udtV7aoX8Xl8gYiZIA', 'signature': signature.to_json(), } def test_verify(self): self.assertTrue(self.msg.verify()) def test_to_partial_json(self): self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import ProofOfPossessionResponse self.assertEqual( self.msg, ProofOfPossessionResponse.from_json(self.jmsg_from)) def test_from_json_hashable(self): from acme.challenges import ProofOfPossessionResponse hash(ProofOfPossessionResponse.from_json(self.jmsg_from)) class DNSTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS self.msg = DNS(token=jose.b64decode( b'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA')) self.jmsg = { 'type': 'dns', 'token': 'evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA', } def test_to_partial_json(self): self.assertEqual(self.jmsg, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNS self.assertEqual(self.msg, DNS.from_json(self.jmsg)) def test_from_json_hashable(self): from acme.challenges import DNS hash(DNS.from_json(self.jmsg)) def test_gen_check_validation(self): self.assertTrue(self.msg.check_validation( self.msg.gen_validation(KEY), KEY.public_key())) def test_gen_check_validation_wrong_key(self): key2 = jose.JWKRSA.load(test_util.load_vector('rsa1024_key.pem')) self.assertFalse(self.msg.check_validation( self.msg.gen_validation(KEY), key2.public_key())) def test_check_validation_wrong_payload(self): validations = tuple( jose.JWS.sign(payload=payload, alg=jose.RS256, key=KEY) for payload in (b'', b'{}') ) for validation in validations: self.assertFalse(self.msg.check_validation( validation, KEY.public_key())) def test_check_validation_wrong_fields(self): bad_validation = jose.JWS.sign( payload=self.msg.update( token=b'x' * 20).json_dumps().encode('utf-8'), alg=jose.RS256, key=KEY) self.assertFalse(self.msg.check_validation( bad_validation, KEY.public_key())) def test_gen_response(self): with mock.patch('acme.challenges.DNS.gen_validation') as mock_gen: mock_gen.return_value = mock.sentinel.validation response = self.msg.gen_response(KEY) from acme.challenges import DNSResponse self.assertTrue(isinstance(response, DNSResponse)) self.assertEqual(response.validation, mock.sentinel.validation) def test_validation_domain_name(self): self.assertEqual( '_acme-challenge.le.wtf', self.msg.validation_domain_name('le.wtf')) class DNSResponseTest(unittest.TestCase): def setUp(self): from acme.challenges import DNS self.chall = DNS(token=jose.b64decode( b"evaGxfADs6pSRb2LAv9IZf17Dt3juxGJ-PCt92wr-oA")) self.validation = jose.JWS.sign( payload=self.chall.json_dumps(sort_keys=True).encode(), key=KEY, alg=jose.RS256) from acme.challenges import DNSResponse self.msg = DNSResponse(validation=self.validation) self.jmsg_to = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation, } self.jmsg_from = { 'resource': 'challenge', 'type': 'dns', 'validation': self.validation.to_json(), } def test_to_partial_json(self): self.assertEqual(self.jmsg_to, self.msg.to_partial_json()) def test_from_json(self): from acme.challenges import DNSResponse self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg_from)) def test_from_json_hashable(self): from acme.challenges import DNSResponse hash(DNSResponse.from_json(self.jmsg_from)) def test_check_validation(self): self.assertTrue( self.msg.check_validation(self.chall, KEY.public_key())) if __name__ == '__main__': unittest.main() # pragma: no cover acme-0.4.1/setup.py0000644000175000017500000000446412665157707013557 0ustar bmwbmw00000000000000import sys from setuptools import setup from setuptools import find_packages version = '0.4.1' # Please update tox.ini when modifying dependency version requirements install_requires = [ # load_pem_private/public_key (>=0.6) # rsa_recover_prime_factors (>=0.8) 'cryptography>=0.8', 'ndg-httpsclient', # urllib3 InsecurePlatformWarning (#304) 'pyasn1', # urllib3 InsecurePlatformWarning (#304) # Connection.set_tlsext_host_name (>=0.13) 'PyOpenSSL>=0.13', 'pyrfc3339', 'pytz', 'requests', 'setuptools', # pkg_resources 'six', ] # env markers in extras_require cause problems with older pip: #517 # Keep in sync with conditional_requirements.py. if sys.version_info < (2, 7): install_requires.extend([ # only some distros recognize stdlib argparse as already satisfying 'argparse', 'mock<1.1.0', ]) else: install_requires.append('mock') dev_extras = [ 'nose', 'pep8', 'tox', ] docs_extras = [ 'Sphinx>=1.0', # autodoc_member_order = 'bysource', autodoc_default_flags 'sphinx_rtd_theme', 'sphinxcontrib-programoutput', ] setup( name='acme', version=version, description='ACME protocol implementation in Python', url='https://github.com/letsencrypt/letsencrypt', author="Let's Encrypt Project", author_email='client-dev@letsencrypt.org', license='Apache License 2.0', classifiers=[ 'Development Status :: 3 - Alpha', 'Intended Audience :: Developers', 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Security', ], packages=find_packages(), include_package_data=True, install_requires=install_requires, extras_require={ 'dev': dev_extras, 'docs': docs_extras, }, entry_points={ 'console_scripts': [ 'jws = acme.jose.jws:CLI.run', ], }, test_suite='acme', ) acme-0.4.1/docs/0000755000175000017500000000000012665157732012763 5ustar bmwbmw00000000000000acme-0.4.1/docs/.gitignore0000644000175000017500000000001112665157707014745 0ustar bmwbmw00000000000000/_build/ acme-0.4.1/docs/man/0000755000175000017500000000000012665157732013536 5ustar bmwbmw00000000000000acme-0.4.1/docs/man/jws.rst0000644000175000017500000000004312665157707015072 0ustar bmwbmw00000000000000.. program-output:: jws --help all acme-0.4.1/docs/api.rst0000644000175000017500000000013012665157707014262 0ustar bmwbmw00000000000000================= API Documentation ================= .. toctree:: :glob: api/* acme-0.4.1/docs/make.bat0000644000175000017500000001612612665157707014400 0ustar bmwbmw00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled echo. coverage to run coverage check of the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) REM Check if sphinx-build is available and fallback to Python version if any %SPHINXBUILD% 2> nul if errorlevel 9009 goto sphinx_python goto sphinx_ok :sphinx_python set SPHINXBUILD=python -m sphinx.__init__ %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) :sphinx_ok if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\acme-python.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\acme-python.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %~dp0 echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "coverage" ( %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage if errorlevel 1 exit /b 1 echo. echo.Testing of coverage in the sources finished, look at the ^ results in %BUILDDIR%/coverage/python.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end acme-0.4.1/docs/Makefile0000644000175000017500000001640512665157707014433 0ustar bmwbmw00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/acme-python.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/acme-python.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/acme-python" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/acme-python" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." acme-0.4.1/docs/api/0000755000175000017500000000000012665157732013534 5ustar bmwbmw00000000000000acme-0.4.1/docs/api/jose.rst0000644000175000017500000000012512665157707015226 0ustar bmwbmw00000000000000JOSE ---- .. automodule:: acme.jose :members: .. toctree:: :glob: jose/* acme-0.4.1/docs/api/jose/0000755000175000017500000000000012665157732014474 5ustar bmwbmw00000000000000acme-0.4.1/docs/api/jose/json_util.rst0000644000175000017500000000012012665157707017227 0ustar bmwbmw00000000000000JSON utilities -------------- .. automodule:: acme.jose.json_util :members: acme-0.4.1/docs/api/jose/jwk.rst0000644000175000017500000000010612665157707016020 0ustar bmwbmw00000000000000JSON Web Key ------------ .. automodule:: acme.jose.jwk :members: acme-0.4.1/docs/api/jose/jwa.rst0000644000175000017500000000012412665157707016006 0ustar bmwbmw00000000000000JSON Web Algorithms ------------------- .. automodule:: acme.jose.jwa :members: acme-0.4.1/docs/api/jose/jws.rst0000644000175000017500000000012212665157707016026 0ustar bmwbmw00000000000000JSON Web Signature ------------------ .. automodule:: acme.jose.jws :members: acme-0.4.1/docs/api/jose/util.rst0000644000175000017500000000010112665157707016175 0ustar bmwbmw00000000000000Utilities --------- .. automodule:: acme.jose.util :members: acme-0.4.1/docs/api/jose/base64.rst0000644000175000017500000000010412665157707016307 0ustar bmwbmw00000000000000JOSE Base64 ----------- .. automodule:: acme.jose.b64 :members: acme-0.4.1/docs/api/jose/interfaces.rst0000644000175000017500000000011112665157707017344 0ustar bmwbmw00000000000000Interfaces ---------- .. automodule:: acme.jose.interfaces :members: acme-0.4.1/docs/api/jose/errors.rst0000644000175000017500000000007512665157707016546 0ustar bmwbmw00000000000000Errors ------ .. automodule:: acme.jose.errors :members: acme-0.4.1/docs/api/challenges.rst0000644000175000017500000000010412665157707016370 0ustar bmwbmw00000000000000Challenges ---------- .. automodule:: acme.challenges :members: acme-0.4.1/docs/api/other.rst0000644000175000017500000000011712665157707015410 0ustar bmwbmw00000000000000Other ACME objects ------------------ .. automodule:: acme.other :members: acme-0.4.1/docs/api/standalone.rst0000644000175000017500000000010412665157707016413 0ustar bmwbmw00000000000000Standalone ---------- .. automodule:: acme.standalone :members: acme-0.4.1/docs/api/fields.rst0000644000175000017500000000007012665157707015533 0ustar bmwbmw00000000000000Fields ------ .. automodule:: acme.fields :members: acme-0.4.1/docs/api/client.rst0000644000175000017500000000007012665157707015543 0ustar bmwbmw00000000000000Client ------ .. automodule:: acme.client :members: acme-0.4.1/docs/api/messages.rst0000644000175000017500000000007612665157707016102 0ustar bmwbmw00000000000000Messages -------- .. automodule:: acme.messages :members: acme-0.4.1/docs/api/errors.rst0000644000175000017500000000007012665157707015601 0ustar bmwbmw00000000000000Errors ------ .. automodule:: acme.errors :members: acme-0.4.1/docs/conf.py0000644000175000017500000002376212665157707014276 0ustar bmwbmw00000000000000# -*- coding: utf-8 -*- # # acme-python documentation build configuration file, created by # sphinx-quickstart on Sun Oct 18 13:38:06 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex here = os.path.abspath(os.path.dirname(__file__)) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath(os.path.join(here, '..'))) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', 'sphinxcontrib.programoutput', ] autodoc_member_order = 'bysource' autodoc_default_flags = ['show-inheritance', 'private-members'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'acme-python' copyright = u'2015-2015, Let\'s Encrypt Project' author = u'Let\'s Encrypt Project' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0' # The full version, including alpha/beta/rc tags. release = '0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = 'en' # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. default_role = 'py:obj' # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # http://docs.readthedocs.org/en/latest/theme.html#how-do-i-use-this-locally-and-on-read-the-docs # on_rtd is whether we are on readthedocs.org on_rtd = os.environ.get('READTHEDOCS', None) == 'True' if not on_rtd: # only import and set the theme if we're building docs locally import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # otherwise, readthedocs.org uses their theme by default, so no need to specify it # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'acme-pythondoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'acme-python.tex', u'acme-python Documentation', u'Let\'s Encrypt Project', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'acme-python', u'acme-python Documentation', [author], 1), ('man/jws', 'jws', u'jws script documentation', [project], 1), ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'acme-python', u'acme-python Documentation', author, 'acme-python', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False intersphinx_mapping = { 'python': ('https://docs.python.org/', None), } acme-0.4.1/docs/index.rst0000644000175000017500000000105712665157707014631 0ustar bmwbmw00000000000000.. acme-python documentation master file, created by sphinx-quickstart on Sun Oct 18 13:38:06 2015. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to acme-python's documentation! ======================================= Contents: .. toctree:: :maxdepth: 2 api .. automodule:: acme :members: Example client: .. include:: ../examples/example_client.py :code: python Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` acme-0.4.1/docs/_static/0000755000175000017500000000000012665157732014411 5ustar bmwbmw00000000000000acme-0.4.1/docs/_static/.gitignore0000644000175000017500000000000012665157707016371 0ustar bmwbmw00000000000000acme-0.4.1/docs/_templates/0000755000175000017500000000000012665157732015120 5ustar bmwbmw00000000000000acme-0.4.1/docs/_templates/.gitignore0000644000175000017500000000000012665157707017100 0ustar bmwbmw00000000000000acme-0.4.1/MANIFEST.in0000644000175000017500000000017712665157707013600 0ustar bmwbmw00000000000000include LICENSE.txt include README.rst recursive-include docs * recursive-include examples * recursive-include acme/testdata * acme-0.4.1/LICENSE.txt0000644000175000017500000002504212665157707013663 0ustar bmwbmw00000000000000 Copyright 2015 Electronic Frontier Foundation and others Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS acme-0.4.1/setup.cfg0000644000175000017500000000013012665157732013646 0ustar bmwbmw00000000000000[bdist_wheel] universal = 1 [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 acme-0.4.1/PKG-INFO0000644000175000017500000000160412665157732013131 0ustar bmwbmw00000000000000Metadata-Version: 1.1 Name: acme Version: 0.4.1 Summary: ACME protocol implementation in Python Home-page: https://github.com/letsencrypt/letsencrypt Author: Let's Encrypt Project Author-email: client-dev@letsencrypt.org License: Apache License 2.0 Description: UNKNOWN Platform: UNKNOWN Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: Apache Software License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Security acme-0.4.1/README.rst0000644000175000017500000000004712665157707013525 0ustar bmwbmw00000000000000ACME protocol implementation in Python