From 52d6e9b67435ee1d597c5a11474aba566e0aaaf3 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Wed, 17 Jun 2015 16:13:26 +0000 Subject: [PATCH 1/2] acme-spec#118 revoke. --- acme/client.py | 14 +++++--------- acme/client_test.py | 5 +++-- acme/messages.py | 34 +++++++++++++++------------------- acme/messages_test.py | 31 +++++++++++++------------------ letsencrypt/revoker.py | 2 +- 5 files changed, 37 insertions(+), 49 deletions(-) diff --git a/acme/client.py b/acme/client.py index 629048d03..73c962581 100644 --- a/acme/client.py +++ b/acme/client.py @@ -539,21 +539,17 @@ class Client(object): # pylint: disable=too-many-instance-attributes else: return None - def revoke(self, certr, when=messages.Revocation.NOW): + def revoke(self, cert): """Revoke certificate. - :param certr: Certificate Resource - :type certr: `.CertificateResource` - - :param when: When should the revocation take place? Takes - the same values as `.messages.Revocation.revoke`. + :param .ComparableX509 body: `M2Crypto.X509.X509` wrapped in + `.ComparableX509` :raises .ClientError: If revocation is unsuccessful. """ - rev = messages.Revocation(revoke=when, authorizations=tuple( - authzr.uri for authzr in certr.authzrs)) - response = self._post(certr.uri, rev) + response = self._post(messages.Revocation.url(self.new_reg_uri), + messages.Revocation(certificate=cert)) if response.status_code != httplib.OK: raise errors.ClientError( 'Successful revocation must return HTTP OK status') diff --git a/acme/client_test.py b/acme/client_test.py index 5e4cc1720..dfa8d7607 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -517,8 +517,9 @@ class ClientTest(unittest.TestCase): def test_revoke(self): self._mock_post_get() - self.net.revoke(self.certr, when=messages.Revocation.NOW) - self.post.assert_called_once_with(self.certr.uri, mock.ANY) + self.net.revoke(self.certr.body) + self.post.assert_called_once_with(messages.Revocation.url( + self.net.new_reg_uri), mock.ANY) def test_revoke_bad_status_raises_error(self): self.response.status_code = httplib.METHOD_NOT_ALLOWED diff --git a/acme/messages.py b/acme/messages.py index aa041caed..bfc452a70 100644 --- a/acme/messages.py +++ b/acme/messages.py @@ -1,4 +1,6 @@ """ACME protocol messages.""" +import urlparse + from acme import challenges from acme import fields from acme import jose @@ -271,28 +273,22 @@ class CertificateResource(Resource): class Revocation(jose.JSONObjectWithFields): """Revocation message. - :ivar revoke: Either a `datetime.datetime` or `Revocation.NOW`. - :ivar tuple authorizations: Same as `CertificateRequest.authorizations` + :ivar .ComparableX509 certificate: `M2Crypto.X509.X509` wrapped in + `.ComparableX509` """ + certificate = jose.Field( + 'certificate', decoder=jose.decode_cert, encoder=jose.encode_cert) - NOW = 'now' - """A possible value for `revoke`, denoting that certificate should - be revoked now.""" + # TODO: acme-spec#138, this allows only one ACME server instance per domain + PATH = '/acme/revoke-cert' + """Path to revocation URL, see `url`""" - revoke = jose.Field('revoke') - authorizations = CertificateRequest._fields['authorizations'] + @classmethod + def url(cls, base): + """Get revocation URL. - @revoke.decoder - def revoke(value): # pylint: disable=missing-docstring,no-self-argument - if value == Revocation.NOW: - return value - else: - return fields.RFC3339Field.default_decoder(value) + :param str base: New Registration Resource or server (root) URL. - @revoke.encoder - def revoke(value): # pylint: disable=missing-docstring,no-self-argument - if value == Revocation.NOW: - return value - else: - return fields.RFC3339Field.default_encoder(value) + """ + return urlparse.urljoin(base, cls.PATH) diff --git a/acme/messages_test.py b/acme/messages_test.py index 4f86d7809..65c080ee7 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -1,11 +1,10 @@ """Tests for acme.messages.""" -import datetime import os import pkg_resources import unittest +import M2Crypto.X509 import mock -import pytz from Crypto.PublicKey import RSA from acme import challenges @@ -14,6 +13,9 @@ from acme import jose KEY = jose.util.HashableRSAKey(RSA.importKey(pkg_resources.resource_string( 'acme.jose', os.path.join('testdata', 'rsa512_key.pem')))) +CERT = jose.ComparableX509(M2Crypto.X509.load_cert( + format=M2Crypto.X509.FORMAT_DER, file=pkg_resources.resource_filename( + 'acme.jose', os.path.join('testdata', 'cert.der')))) class ErrorTest(unittest.TestCase): @@ -223,27 +225,20 @@ class AuthorizationTest(unittest.TestCase): class RevocationTest(unittest.TestCase): """Tests for acme.messages.RevocationTest.""" + def test_url(self): + from acme.messages import Revocation + url = 'https://letsencrypt-demo.org/acme/revoke-cert' + self.assertEqual(url, Revocation.url('https://letsencrypt-demo.org')) + self.assertEqual( + url, Revocation.url('https://letsencrypt-demo.org/acme/new-reg')) + def setUp(self): from acme.messages import Revocation - self.rev_now = Revocation(authorizations=(), revoke=Revocation.NOW) - self.rev_date = Revocation(authorizations=(), revoke=datetime.datetime( - 2015, 3, 27, tzinfo=pytz.utc)) - self.jobj_now = {'authorizations': (), 'revoke': Revocation.NOW} - self.jobj_date = {'authorizations': (), - 'revoke': '2015-03-27T00:00:00Z'} - - def test_revoke_decoder(self): - from acme.messages import Revocation - self.assertEqual(self.rev_now, Revocation.from_json(self.jobj_now)) - self.assertEqual(self.rev_date, Revocation.from_json(self.jobj_date)) - - def test_revoke_encoder(self): - self.assertEqual(self.jobj_now, self.rev_now.to_partial_json()) - self.assertEqual(self.jobj_date, self.rev_date.to_partial_json()) + self.rev = Revocation(certificate=CERT) def test_from_json_hashable(self): from acme.messages import Revocation - hash(Revocation.from_json(self.rev_now.to_json())) + hash(Revocation.from_json(self.rev.to_json())) if __name__ == '__main__': diff --git a/letsencrypt/revoker.py b/letsencrypt/revoker.py index 0d3bd8e79..402157721 100644 --- a/letsencrypt/revoker.py +++ b/letsencrypt/revoker.py @@ -253,7 +253,7 @@ class Revoker(object): raise errors.LetsEncryptRevokerError( "Corrupted backup key file: %s" % cert.backup_key_path) - return self.network.revoke(certr=None) # XXX + return self.network.revoke(cert=None) # XXX def _remove_certs_keys(self, cert_list): # pylint: disable=no-self-use """Remove certificate and key. From d970987b79c1f370ac1400ae9a31f01ee6f2722a Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Mon, 22 Jun 2015 20:30:17 +0000 Subject: [PATCH 2/2] Fix comment typo --- acme/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme/client.py b/acme/client.py index 73c962581..1c0975849 100644 --- a/acme/client.py +++ b/acme/client.py @@ -542,7 +542,7 @@ class Client(object): # pylint: disable=too-many-instance-attributes def revoke(self, cert): """Revoke certificate. - :param .ComparableX509 body: `M2Crypto.X509.X509` wrapped in + :param .ComparableX509 cert: `M2Crypto.X509.X509` wrapped in `.ComparableX509` :raises .ClientError: If revocation is unsuccessful.