commit ee2330f349d038e9961d7f9afa528dd6c890bf86 Author: Damian Johnson atagar@torproject.org Date: Wed Mar 29 19:43:57 2017 +0200
Unit tests for ed25519 crypto validation
A server descriptor unit test indirectly provides some coverage, but adding three tests that specifically focus on the validate() method. Also adding some quick docs. --- stem/descriptor/certificate.py | 3 ++ test/unit/descriptor/certificate.py | 96 ++++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 2 deletions(-)
diff --git a/stem/descriptor/certificate.py b/stem/descriptor/certificate.py index bab0a44..d49df50 100644 --- a/stem/descriptor/certificate.py +++ b/stem/descriptor/certificate.py @@ -231,6 +231,9 @@ class Ed25519CertificateV1(Ed25519Certificate):
# ed25519 signature validates descriptor content up until the signature itself
+ if b'router-sig-ed25519 ' not in descriptor_content: + raise ValueError("Descriptor doesn't have a router-sig-ed25519 entry.") + signed_content = descriptor_content[:descriptor_content.index(b'router-sig-ed25519 ') + 19] descriptor_sha256_digest = hashlib.sha256(ED25519_ROUTER_SIGNATURE_PREFIX + signed_content).digest()
diff --git a/test/unit/descriptor/certificate.py b/test/unit/descriptor/certificate.py index f46965a..9226f30 100644 --- a/test/unit/descriptor/certificate.py +++ b/test/unit/descriptor/certificate.py @@ -12,6 +12,7 @@ import stem.prereq import test.runner
from stem.descriptor.certificate import ED25519_SIGNATURE_LENGTH, CertType, ExtensionType, ExtensionFlag, Ed25519Certificate, Ed25519CertificateV1, Ed25519Extension +from test.unit.descriptor import get_resource
ED25519_CERT = """ AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR @@ -25,6 +26,14 @@ EXPECTED_SIGNATURE = '\xc6\x8e\xd3\xae\x0b?\xedJ6\xe2\xef\x95\xcf,\x18o%N<u\x83\
def certificate(version = 1, cert_type = 4, extension_data = []): + """ + Provides base64 encoded Ed25519 certifificate content. + + :param int version: certificate version + :param int cert_type: certificate type + :param list extension_data: extensions to embed within the certificate + """ + return base64.b64encode(''.join([ chr(version), chr(cert_type), @@ -41,6 +50,10 @@ class TestEd25519Certificate(unittest.TestCase): self.assertRaisesRegexp(ValueError, re.escape(exc_msg), Ed25519Certificate.parse, parse_arg)
def test_basic_parsing(self): + """ + Parse a basic test certificate. + """ + signing_key = b'\x11' * 32 cert_bytes = certificate(extension_data = [b'\x00\x20\x04\x07' + signing_key, b'\x00\x00\x05\x04']) cert = Ed25519Certificate.parse(cert_bytes) @@ -63,6 +76,10 @@ class TestEd25519Certificate(unittest.TestCase): self.assertTrue(cert.is_expired())
def test_with_real_cert(self): + """ + Parse a certificate from a real server descriptor. + """ + cert = Ed25519Certificate.parse(ED25519_CERT)
self.assertEqual(Ed25519CertificateV1, type(cert)) @@ -76,29 +93,104 @@ class TestEd25519Certificate(unittest.TestCase): self.assertEqual(EXPECTED_SIGNATURE, cert.signature)
def test_non_base64(self): + """ + Parse data that isn't base64 encoded. + """ + self.assert_raises('\x02\x0323\x04', "Ed25519 certificate wasn't propoerly base64 encoded (Incorrect padding):")
def test_too_short(self): + """ + Parse data that's too short to be a valid certificate. + """ + self.assert_raises('', "Ed25519 certificate wasn't propoerly base64 encoded (empty):") self.assert_raises('AQQABhtZAaW2GoBED1IjY3A6', 'Ed25519 certificate was 18 bytes, but should be at least 104')
def test_with_invalid_version(self): + """ + We cannot support other certificate versions until they're documented. + Assert we raise if we don't handle a cert version yet. + """ + self.assert_raises(certificate(version = 2), 'Ed25519 certificate is version 2. Parser presently only supports version 1.')
def test_with_invalid_cert_type(self): + """ + Provide an invalid certificate version. Tor specifies a couple ranges that + are reserved. + """ + self.assert_raises(certificate(cert_type = 0), 'Ed25519 certificate cannot have a type of 0. This is reserved to avoid conflicts with tor CERTS cells.') self.assert_raises(certificate(cert_type = 7), 'Ed25519 certificate cannot have a type of 7. This is reserved for RSA identity cross-certification.')
def test_truncated_extension(self): + """ + Include an extension without as much data as it specifies. + """ + self.assert_raises(certificate(extension_data = [b'']), 'Ed25519 extension is missing header field data') self.assert_raises(certificate(extension_data = [b'\x50\x00\x00\x00\x15\x12']), "Ed25519 extension is truncated. It should have 20480 bytes of data but there's only 2.")
+ def test_extra_extension_data(self): + """ + Include an extension with more data than it specifies. + """ + + self.assert_raises(certificate(extension_data = [b'\x00\x01\x00\x00\x15\x12']), "Ed25519 certificate had 1 bytes of unused extension data") + def test_truncated_signing_key(self): + """ + Include an extension with an incorrect signing key size. + """ + self.assert_raises(certificate(extension_data = [b'\x00\x02\x04\x07\11\12']), "Ed25519 HAS_SIGNING_KEY extension must be 32 bytes, but was 2.")
- def test_extra_extension_data(self): - self.assert_raises(certificate(extension_data = [b'\x00\x01\x00\x00\x15\x12']), "Ed25519 certificate had 1 bytes of unused extension data") + def test_validation_with_descriptor_key(self): + """ + Validate a descriptor signature using the ed25519 master key within the + descriptor. + """ + + if not stem.prereq._is_pynacl_available(): + test.runner.skip(self, '(requires pynacl module)') + return + + with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, validate = False)) + + desc.certificate.validate(desc) + + def test_validation_with_embedded_key(self): + """ + Validate a descriptor signature using the signing key within the ed25519 + certificate. + """ + + if not stem.prereq._is_pynacl_available(): + test.runner.skip(self, '(requires pynacl module)') + return + + with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, validate = False)) + + desc.ed25519_master_key = None + desc.certificate.validate(desc) + + def test_validation_with_invalid_descriptor(self): + """ + Validate a descriptor without a valid signature. + """ + + if not stem.prereq._is_pynacl_available(): + test.runner.skip(self, '(requires pynacl module)') + return + + with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file: + desc = next(stem.descriptor.parse_file(descriptor_file, validate = False))
+ cert = Ed25519Certificate.parse(certificate()) + self.assertRaisesRegexp(ValueError, re.escape('Ed25519KeyCertificate signing key is invalid (Signature was forged or corrupt)'), cert.validate, desc)
class TestCertificate(unittest.TestCase):