From 5b0efa2e6449a93b99e8efa8b8ff9df92f1f7c45 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 18 Apr 2015 05:48:32 +0000 Subject: [PATCH 1/4] Add test_from_json_hashable --- letsencrypt/acme/challenges_test.py | 52 +++++++++++++++++++++++++++++ letsencrypt/acme/jose/jwk_test.py | 8 +++++ letsencrypt/acme/jose/jws_test.py | 4 +++ letsencrypt/acme/messages2_test.py | 21 +++++++++++- 4 files changed, 84 insertions(+), 1 deletion(-) diff --git a/letsencrypt/acme/challenges_test.py b/letsencrypt/acme/challenges_test.py index f1507c7fd..2e6fbd372 100644 --- a/letsencrypt/acme/challenges_test.py +++ b/letsencrypt/acme/challenges_test.py @@ -37,6 +37,10 @@ class SimpleHTTPSTest(unittest.TestCase): from letsencrypt.acme.challenges import SimpleHTTPS self.assertEqual(self.msg, SimpleHTTPS.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import SimpleHTTPS + hash(SimpleHTTPS.from_json(self.jmsg)) + class SimpleHTTPSResponseTest(unittest.TestCase): @@ -60,6 +64,10 @@ class SimpleHTTPSResponseTest(unittest.TestCase): self.assertEqual( self.msg, SimpleHTTPSResponse.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import SimpleHTTPSResponse + hash(SimpleHTTPSResponse.from_json(self.jmsg)) + class DVSNITest(unittest.TestCase): @@ -86,6 +94,10 @@ class DVSNITest(unittest.TestCase): from letsencrypt.acme.challenges import DVSNI self.assertEqual(self.msg, DVSNI.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import DVSNI + hash(DVSNI.from_json(self.jmsg)) + def test_from_json_invalid_r_length(self): from letsencrypt.acme.challenges import DVSNI self.jmsg['r'] = 'abcd' @@ -131,6 +143,10 @@ class DVSNIResponseTest(unittest.TestCase): from letsencrypt.acme.challenges import DVSNIResponse self.assertEqual(self.msg, DVSNIResponse.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import DVSNIResponse + hash(DVSNIResponse.from_json(self.jmsg)) + class RecoveryContactTest(unittest.TestCase): @@ -154,6 +170,10 @@ class RecoveryContactTest(unittest.TestCase): from letsencrypt.acme.challenges import RecoveryContact self.assertEqual(self.msg, RecoveryContact.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import RecoveryContact + hash(RecoveryContact.from_json(self.jmsg)) + def test_json_without_optionals(self): del self.jmsg['activationURL'] del self.jmsg['successURL'] @@ -183,6 +203,10 @@ class RecoveryContactResponseTest(unittest.TestCase): self.assertEqual( self.msg, RecoveryContactResponse.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import RecoveryContactResponse + hash(RecoveryContactResponse.from_json(self.jmsg)) + def test_json_without_optionals(self): del self.jmsg['token'] @@ -207,6 +231,10 @@ class RecoveryTokenTest(unittest.TestCase): from letsencrypt.acme.challenges import RecoveryToken self.assertEqual(self.msg, RecoveryToken.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import RecoveryToken + hash(RecoveryToken.from_json(self.jmsg)) + class RecoveryTokenResponseTest(unittest.TestCase): @@ -223,6 +251,10 @@ class RecoveryTokenResponseTest(unittest.TestCase): self.assertEqual( self.msg, RecoveryTokenResponse.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import RecoveryTokenResponse + hash(RecoveryTokenResponse.from_json(self.jmsg)) + def test_json_without_optionals(self): del self.jmsg['token'] @@ -276,6 +308,10 @@ class ProofOfPossessionHintsTest(unittest.TestCase): self.assertEqual( self.msg, ProofOfPossession.Hints.from_json(self.jmsg_from)) + def test_from_json_hashable(self): + from letsencrypt.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']: @@ -328,6 +364,10 @@ class ProofOfPossessionTest(unittest.TestCase): self.assertEqual( self.msg, ProofOfPossession.from_json(self.jmsg_from)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import ProofOfPossession + hash(ProofOfPossession.from_json(self.jmsg_from)) + class ProofOfPossessionResponseTest(unittest.TestCase): @@ -371,6 +411,10 @@ class ProofOfPossessionResponseTest(unittest.TestCase): self.assertEqual( self.msg, ProofOfPossessionResponse.from_json(self.jmsg_from)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import ProofOfPossessionResponse + hash(ProofOfPossessionResponse.from_json(self.jmsg_from)) + class DNSTest(unittest.TestCase): @@ -386,6 +430,10 @@ class DNSTest(unittest.TestCase): from letsencrypt.acme.challenges import DNS self.assertEqual(self.msg, DNS.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import DNS + hash(DNS.from_json(self.jmsg)) + class DNSResponseTest(unittest.TestCase): @@ -401,6 +449,10 @@ class DNSResponseTest(unittest.TestCase): from letsencrypt.acme.challenges import DNSResponse self.assertEqual(self.msg, DNSResponse.from_json(self.jmsg)) + def test_from_json_hashable(self): + from letsencrypt.acme.challenges import DNSResponse + hash(DNSResponse.from_json(self.jmsg)) + if __name__ == '__main__': unittest.main() diff --git a/letsencrypt/acme/jose/jwk_test.py b/letsencrypt/acme/jose/jwk_test.py index b75d3e1ce..8bee88e72 100644 --- a/letsencrypt/acme/jose/jwk_test.py +++ b/letsencrypt/acme/jose/jwk_test.py @@ -29,6 +29,10 @@ class JWKOctTest(unittest.TestCase): from letsencrypt.acme.jose.jwk import JWKOct self.assertEqual(self.jwk, JWKOct.from_json(self.jobj)) + def test_from_json_hashable(self): + from letsencrypt.acme.jose.jwk import JWKOct + hash(JWKOct.from_json(self.jobj)) + def test_load(self): from letsencrypt.acme.jose.jwk import JWKOct self.assertEqual(self.jwk, JWKOct.load('foo')) @@ -86,6 +90,10 @@ class JWKRSATest(unittest.TestCase): # TODO: fix schemata to allow RSA512 #self.assertEqual(self.jwk512, JWK.from_json(self.jwk512json)) + def test_from_json_hashable(self): + from letsencrypt.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 letsencrypt.acme.jose.jwk import JWK diff --git a/letsencrypt/acme/jose/jws_test.py b/letsencrypt/acme/jose/jws_test.py index 215960e15..96a9c2070 100644 --- a/letsencrypt/acme/jose/jws_test.py +++ b/letsencrypt/acme/jose/jws_test.py @@ -196,6 +196,10 @@ class JWSTest(unittest.TestCase): self.assertRaises(errors.DeserializationError, JWS.from_json, {'signatures': (), 'signature': 'foo'}) + def test_from_json_hashable(self): + from letsencrypt.acme.jose.jws import JWS + hash(JWS.from_json(self.mixed.fully_serialize())) + class CLITest(unittest.TestCase): diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py index 5297d6362..cd9bc7c8b 100644 --- a/letsencrypt/acme/messages2_test.py +++ b/letsencrypt/acme/messages2_test.py @@ -39,6 +39,10 @@ class ErrorTest(unittest.TestCase): self.assertEqual( 'The request message was malformed', self.error.description) + def test_from_json_hashable(self): + from letsencrypt.acme.messages2 import Error + hash(Error.from_json(self.error.fully_serialize())) + class ConstantTest(unittest.TestCase): """Tests for letsencrypt.acme.messages2._Constant.""" @@ -61,6 +65,9 @@ class ConstantTest(unittest.TestCase): 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)) @@ -99,10 +106,14 @@ class ChallengeBodyTest(unittest.TestCase): def test_to_json(self): self.assertEqual(self.jobj_to, self.challb.to_json()) - def test_fields_from_json(self): + def test_from_json(self): from letsencrypt.acme.messages2 import ChallengeBody self.assertEqual(self.challb, ChallengeBody.from_json(self.jobj_from)) + def test_from_json_hashable(self): + from letsencrypt.acme.messages2 import ChallengeBody + hash(ChallengeBody.from_json(self.jobj_from)) + class AuthorizationTest(unittest.TestCase): """Tests for letsencrypt.acme.messages2.Authorization.""" @@ -139,6 +150,10 @@ class AuthorizationTest(unittest.TestCase): from letsencrypt.acme.messages2 import Authorization Authorization.from_json(self.jobj_from) + def test_from_json_hashable(self): + from letsencrypt.acme.messages2 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]), @@ -167,6 +182,10 @@ class RevocationTest(unittest.TestCase): self.assertEqual(self.jobj_now, self.rev_now.to_json()) self.assertEqual(self.jobj_date, self.rev_date.to_json()) + def test_from_json_hashable(self): + from letsencrypt.acme.messages2 import Revocation + hash(Revocation.from_json(self.rev_now.fully_serialize())) + if __name__ == '__main__': unittest.main() From 82dded912805b8ebe78f70b317c1832583a0c692 Mon Sep 17 00:00:00 2001 From: James Kasten Date: Fri, 17 Apr 2015 15:09:19 -0700 Subject: [PATCH 2/4] Add Registration encoding/fix hashable JWKRSA --- letsencrypt/acme/jose/jwk.py | 7 +++-- letsencrypt/acme/jose/jwk_test.py | 16 ++++++----- letsencrypt/acme/messages2.py | 3 +- letsencrypt/acme/messages2_test.py | 45 ++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 11 deletions(-) diff --git a/letsencrypt/acme/jose/jwk.py b/letsencrypt/acme/jose/jwk.py index 1b7e00e56..2e70ac66b 100644 --- a/letsencrypt/acme/jose/jwk.py +++ b/letsencrypt/acme/jose/jwk.py @@ -126,9 +126,10 @@ class JWKRSA(JWK): @classmethod def fields_from_json(cls, jobj): - return cls(key=Crypto.PublicKey.RSA.construct( - (cls._decode_param(jobj['n']), - cls._decode_param(jobj['e'])))) + return cls(key=util.HashableRSAKey( + Crypto.PublicKey.RSA.construct( + (cls._decode_param(jobj['n']), + cls._decode_param(jobj['e']))))) def fields_to_json(self): return { diff --git a/letsencrypt/acme/jose/jwk_test.py b/letsencrypt/acme/jose/jwk_test.py index 8bee88e72..4e7c4e596 100644 --- a/letsencrypt/acme/jose/jwk_test.py +++ b/letsencrypt/acme/jose/jwk_test.py @@ -6,6 +6,7 @@ import unittest from Crypto.PublicKey import RSA from letsencrypt.acme.jose import errors +from letsencrypt.acme.jose import util RSA256_KEY = RSA.importKey(pkg_resources.resource_string( @@ -46,15 +47,15 @@ class JWKRSATest(unittest.TestCase): def setUp(self): from letsencrypt.acme.jose.jwk import JWKRSA - self.jwk256 = JWKRSA(key=RSA256_KEY.publickey()) - self.jwk256_private = JWKRSA(key=RSA256_KEY) + self.jwk256 = JWKRSA(key=util.HashableRSAKey(RSA256_KEY.publickey())) + self.jwk256_private = JWKRSA(key=util.HashableRSAKey(RSA256_KEY)) self.jwk256json = { 'kty': 'RSA', 'e': 'AQAB', 'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5' '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q', } - self.jwk512 = JWKRSA(key=RSA512_KEY.publickey()) + self.jwk512 = JWKRSA(key=util.HashableRSAKey(RSA512_KEY.publickey())) self.jwk512json = { 'kty': 'RSA', 'e': 'AQAB', @@ -72,10 +73,11 @@ class JWKRSATest(unittest.TestCase): def test_load(self): from letsencrypt.acme.jose.jwk import JWKRSA - self.assertEqual(JWKRSA(key=RSA256_KEY), JWKRSA.load( - pkg_resources.resource_string( - 'letsencrypt.client.tests', - os.path.join('testdata', 'rsa256_key.pem')))) + self.assertEqual( + JWKRSA(key=util.HashableRSAKey(RSA256_KEY)), JWKRSA.load( + pkg_resources.resource_string( + 'letsencrypt.client.tests', + os.path.join('testdata', 'rsa256_key.pem')))) def test_public(self): self.assertEqual(self.jwk256, self.jwk256_private.public()) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index f4c1e9dce..7f4050c24 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -136,7 +136,8 @@ class Registration(ResourceBody): # 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) + key = jose.Field('key', omitempty=True, + decoder=jose.JWK.from_json, encoder=jose.JWK.to_json) contact = jose.Field('contact', omitempty=True, default=()) recovery_token = jose.Field('recoveryToken', omitempty=True) agreement = jose.Field('agreement', omitempty=True) diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py index cd9bc7c8b..e5a4eeb18 100644 --- a/letsencrypt/acme/messages2_test.py +++ b/letsencrypt/acme/messages2_test.py @@ -1,9 +1,12 @@ """Tests for letsencrypt.acme.messages2.""" import datetime +import os +import pkg_resources import unittest import mock import pytz +from Crypto.PublicKey import RSA from letsencrypt.acme import challenges from letsencrypt.acme import jose @@ -73,6 +76,48 @@ class ConstantTest(unittest.TestCase): self.assertEqual('MockConstant(b)', repr(self.const_b)) +class RegistrationTest(unittest.TestCase): + """Tests for letsencrypt.acme.messages2.Registration.""" + + def setUp(self): + from letsencrypt.acme.messages2 import Registration + + rsa_key = RSA.importKey(pkg_resources.resource_string( + 'letsencrypt.client.tests', os.path.join( + 'testdata', 'rsa256_key.pem'))) + + self.key = jose.jwk.JWKRSA(key=jose.util.HashableRSAKey( + rsa_key.publickey())) + + self.contact = ("mailto:letsencrypt-client@letsencrypt.org",) + self.recovery_token = "XYZ" + self.agreement = "https://letsencrypt.org/terms" + self.reg = Registration( + key=self.key, contact=self.contact, + recovery_token=self.recovery_token, agreement=self.agreement) + + self.json_key = { + 'kty': 'RSA', + 'e': 'AQAB', + 'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5' + '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q', + } + + self.json_reg = { + "contact": self.contact, + "recoveryToken": self.recovery_token, + "agreement": self.agreement, + "key": self.json_key, + } + + def test_to_json(self): + self.assertEqual(self.reg.to_json(), self.json_reg) + + def test_from_json(self): + from letsencrypt.acme.messages2 import Registration + + self.assertEqual(Registration.from_json(self.json_reg), self.reg) + class ChallengeResourceTest(unittest.TestCase): """Tests for letsencrypt.acme.messages2.ChallengeResource.""" From aca82c1771904162752a7807e87a36862cb93593 Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 18 Apr 2015 06:01:21 +0000 Subject: [PATCH 3/4] lint, style, test registration hashable --- letsencrypt/acme/jose/jwk.py | 2 +- letsencrypt/acme/messages2_test.py | 48 +++++++++++++----------------- 2 files changed, 22 insertions(+), 28 deletions(-) diff --git a/letsencrypt/acme/jose/jwk.py b/letsencrypt/acme/jose/jwk.py index 2e70ac66b..58cc89dad 100644 --- a/letsencrypt/acme/jose/jwk.py +++ b/letsencrypt/acme/jose/jwk.py @@ -129,7 +129,7 @@ class JWKRSA(JWK): return cls(key=util.HashableRSAKey( Crypto.PublicKey.RSA.construct( (cls._decode_param(jobj['n']), - cls._decode_param(jobj['e']))))) + cls._decode_param(jobj['e']))))) def fields_to_json(self): return { diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py index e5a4eeb18..e162af1d0 100644 --- a/letsencrypt/acme/messages2_test.py +++ b/letsencrypt/acme/messages2_test.py @@ -80,43 +80,37 @@ class RegistrationTest(unittest.TestCase): """Tests for letsencrypt.acme.messages2.Registration.""" def setUp(self): + key = jose.jwk.JWKRSA(key=jose.util.HashableRSAKey( + RSA.importKey(pkg_resources.resource_string( + 'letsencrypt.client.tests', os.path.join( + 'testdata', 'rsa256_key.pem'))).publickey())) + contact = ('mailto:letsencrypt-client@letsencrypt.org',) + recovery_token = 'XYZ' + agreement = 'https://letsencrypt.org/terms' + from letsencrypt.acme.messages2 import Registration - - rsa_key = RSA.importKey(pkg_resources.resource_string( - 'letsencrypt.client.tests', os.path.join( - 'testdata', 'rsa256_key.pem'))) - - self.key = jose.jwk.JWKRSA(key=jose.util.HashableRSAKey( - rsa_key.publickey())) - - self.contact = ("mailto:letsencrypt-client@letsencrypt.org",) - self.recovery_token = "XYZ" - self.agreement = "https://letsencrypt.org/terms" self.reg = Registration( - key=self.key, contact=self.contact, - recovery_token=self.recovery_token, agreement=self.agreement) + key=key, contact=contact, recovery_token=recovery_token, + agreement=agreement) - self.json_key = { - 'kty': 'RSA', - 'e': 'AQAB', - 'n': 'rHVztFHtH92ucFJD_N_HW9AsdRsUuHUBBBDlHwNlRd3fp5' - '80rv2-6QWE30cWgdmJS86ObRz6lUTor4R0T-3C5Q', - } - - self.json_reg = { - "contact": self.contact, - "recoveryToken": self.recovery_token, - "agreement": self.agreement, - "key": self.json_key, + self.jobj = { + 'contact': contact, + 'recoveryToken': recovery_token, + 'agreement': agreement, + 'key': key.fully_serialize(), } def test_to_json(self): - self.assertEqual(self.reg.to_json(), self.json_reg) + self.assertEqual(self.jobj, self.reg.to_json()) def test_from_json(self): from letsencrypt.acme.messages2 import Registration + self.assertEqual(self.reg, Registration.from_json(self.jobj)) + + def test_from_json_hashable(self): + from letsencrypt.acme.messages2 import Registration + hash(Registration.from_json(self.jobj)) - self.assertEqual(Registration.from_json(self.json_reg), self.reg) class ChallengeResourceTest(unittest.TestCase): """Tests for letsencrypt.acme.messages2.ChallengeResource.""" From 33ba8b9dacef932319c405d23e7dd51425d5644b Mon Sep 17 00:00:00 2001 From: Jakub Warmuz Date: Sat, 18 Apr 2015 06:19:54 +0000 Subject: [PATCH 4/4] Remove explicit Registration.key.encoder --- letsencrypt/acme/messages2.py | 3 +-- letsencrypt/acme/messages2_test.py | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/letsencrypt/acme/messages2.py b/letsencrypt/acme/messages2.py index 7f4050c24..f4c1e9dce 100644 --- a/letsencrypt/acme/messages2.py +++ b/letsencrypt/acme/messages2.py @@ -136,8 +136,7 @@ class Registration(ResourceBody): # 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, encoder=jose.JWK.to_json) + key = jose.Field('key', omitempty=True, decoder=jose.JWK.from_json) contact = jose.Field('contact', omitempty=True, default=()) recovery_token = jose.Field('recoveryToken', omitempty=True) agreement = jose.Field('agreement', omitempty=True) diff --git a/letsencrypt/acme/messages2_test.py b/letsencrypt/acme/messages2_test.py index e162af1d0..bebed64fa 100644 --- a/letsencrypt/acme/messages2_test.py +++ b/letsencrypt/acme/messages2_test.py @@ -93,23 +93,25 @@ class RegistrationTest(unittest.TestCase): key=key, contact=contact, recovery_token=recovery_token, agreement=agreement) - self.jobj = { + self.jobj_to = { 'contact': contact, 'recoveryToken': recovery_token, 'agreement': agreement, - 'key': key.fully_serialize(), + 'key': key, } + self.jobj_from = self.jobj_to.copy() + self.jobj_from['key'] = key.fully_serialize() def test_to_json(self): - self.assertEqual(self.jobj, self.reg.to_json()) + self.assertEqual(self.jobj_to, self.reg.to_json()) def test_from_json(self): from letsencrypt.acme.messages2 import Registration - self.assertEqual(self.reg, Registration.from_json(self.jobj)) + self.assertEqual(self.reg, Registration.from_json(self.jobj_from)) def test_from_json_hashable(self): from letsencrypt.acme.messages2 import Registration - hash(Registration.from_json(self.jobj)) + hash(Registration.from_json(self.jobj_from)) class ChallengeResourceTest(unittest.TestCase):