diff --git a/acme/client.py b/acme/client.py index 43c659bb8..f5a651652 100644 --- a/acme/client.py +++ b/acme/client.py @@ -540,21 +540,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 cert: `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 7f09f8bdf..d408f0564 100644 --- a/acme/client_test.py +++ b/acme/client_test.py @@ -509,8 +509,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 e355e9fd0..cd5a65f79 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 @@ -327,28 +329,22 @@ class CertificateResource(ResourceWithURI): 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 6749f55dc..9b3c03fbc 100644 --- a/acme/messages_test.py +++ b/acme/messages_test.py @@ -1,5 +1,4 @@ """Tests for acme.messages.""" -import datetime import os import pkg_resources import unittest @@ -7,7 +6,6 @@ import unittest from Crypto.PublicKey import RSA import M2Crypto import mock -import pytz from acme import challenges from acme import jose @@ -23,6 +21,9 @@ CSR = jose.ComparableX509(M2Crypto.X509.load_request_string( M2Crypto.X509.FORMAT_DER)) 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): @@ -318,27 +319,20 @@ class CertificateResourceTest(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/constants.py b/letsencrypt/constants.py index 47539615d..356b8ed14 100644 --- a/letsencrypt/constants.py +++ b/letsencrypt/constants.py @@ -81,7 +81,3 @@ ACCOUNT_KEYS_DIR = "keys" REC_TOKEN_DIR = "recovery_tokens" """Directory where all recovery tokens are saved (relative to IConfig.work_dir).""" - -NETSTAT = "/bin/netstat" -"""Location of netstat binary for checking whether a listener is already -running on the specified port (Linux-specific).""" diff --git a/letsencrypt/interfaces.py b/letsencrypt/interfaces.py index c0d44a134..d2a420d00 100644 --- a/letsencrypt/interfaces.py +++ b/letsencrypt/interfaces.py @@ -148,8 +148,7 @@ class IConfig(zope.interface.Interface): """ server = zope.interface.Attribute( - "CA hostname (and optionally :port). The server certificate must " - "be trusted in order to avoid further modifications to the client.") + "ACME new registration URI (including /acme/new-reg).") email = zope.interface.Attribute( "Email used for registration and recovery contact.") rsa_key_size = zope.interface.Attribute("Size of the RSA key.") 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.