[tor-commits] [stem/master] Support for ed25519 descriptor fields

atagar at torproject.org atagar at torproject.org
Sun Aug 23 21:58:43 UTC 2015


commit 353200c26399e5727a6865ddcd83b1820f3e1b6f
Author: Damian Johnson <atagar at torproject.org>
Date:   Sun Aug 23 14:55:55 2015 -0700

    Support for ed25519 descriptor fields
    
    The descriptor fields for ed25519 finally made it into the spec...
    
      https://trac.torproject.org/projects/tor/ticket/16235
      https://gitweb.torproject.org/torspec.git/commit/?id=5a79d67a45454ab5b7413478702acb93dfa867e2
    
    This included fields in four descriptor types: server, extrainfo,
    microdescriptors, and router status entries. One leftover bit though is to
    support bridge sanitization...
    
      https://trac.torproject.org/projects/tor/ticket/16359
      https://collector.torproject.org/formats.html#bridge-descriptors
    
    These descriptions aren't quite enough for me to be sure what's up so gonna
    check with Karsten for clarification.
---
 docs/change_log.rst                                |    1 +
 stem/descriptor/extrainfo_descriptor.py            |   14 +++
 stem/descriptor/router_status_entry.py             |   39 ++++++-
 stem/descriptor/server_descriptor.py               |   40 +++++++
 .../data/extrainfo_descriptor_with_ed25519         |   26 +++++
 .../descriptor/data/server_descriptor_with_ed25519 |   80 +++++++++-----
 test/unit/descriptor/extrainfo_descriptor.py       |   14 +++
 test/unit/descriptor/router_status_entry.py        |  112 ++++++++++++++++++--
 test/unit/descriptor/server_descriptor.py          |   60 +++++++----
 9 files changed, 327 insertions(+), 59 deletions(-)

diff --git a/docs/change_log.rst b/docs/change_log.rst
index 5eade67..a52359e 100644
--- a/docs/change_log.rst
+++ b/docs/change_log.rst
@@ -53,6 +53,7 @@ The following are only available within Stem's `git repository
 
  * **Descriptors**
 
+  * Support for ed25519 descriptor fields (:spec:`5a79d67`)
   * Server descriptor validation fails with 'extra-info-digest line had an invalid value' from additions in proposal 228 (:trac:`16227`)
 
  * **Website**
diff --git a/stem/descriptor/extrainfo_descriptor.py b/stem/descriptor/extrainfo_descriptor.py
index 56c042a..7373620 100644
--- a/stem/descriptor/extrainfo_descriptor.py
+++ b/stem/descriptor/extrainfo_descriptor.py
@@ -83,6 +83,7 @@ from stem.descriptor import (
   _get_descriptor_components,
   _value,
   _values,
+  _parse_simple_line,
   _parse_timestamp_line,
   _parse_forty_character_hex,
   _parse_key_block,
@@ -533,6 +534,7 @@ def _parse_hs_stats(keyword, stat_attribute, extra_attribute, descriptor, entrie
   setattr(descriptor, extra_attribute, extra)
 
 
+_parse_identity_ed25519_line = _parse_key_block('identity-ed25519', 'ed25519_certificate', 'ED25519 CERT')
 _parse_geoip_db_digest_line = _parse_forty_character_hex('geoip-db-digest', 'geoip_db_digest')
 _parse_geoip6_db_digest_line = _parse_forty_character_hex('geoip6-db-digest', 'geoip6_db_digest')
 _parse_dirreq_v2_resp_line = functools.partial(_parse_dirreq_line, 'dirreq-v2-resp', 'dir_v2_responses', 'dir_v2_responses_unknown')
@@ -570,6 +572,7 @@ _parse_dirreq_v3_reqs_line = functools.partial(_parse_geoip_to_count_line, 'dirr
 _parse_geoip_client_origins_line = functools.partial(_parse_geoip_to_count_line, 'geoip-client-origins', 'geoip_client_origins')
 _parse_entry_ips_line = functools.partial(_parse_geoip_to_count_line, 'entry-ips', 'entry_ips')
 _parse_bridge_ips_line = functools.partial(_parse_geoip_to_count_line, 'bridge-ips', 'bridge_ips')
+_parse_router_sig_ed25519_line = _parse_simple_line('router-sig-ed25519', 'ed25519_signature')
 _parse_router_digest_line = _parse_forty_character_hex('router-digest', '_digest')
 _parse_router_signature_line = _parse_key_block('router-signature', 'signature', 'SIGNATURE')
 
@@ -587,6 +590,9 @@ class ExtraInfoDescriptor(Descriptor):
     port, args) tuple, these usually appear on bridges in which case all of
     those are **None**
 
+  :var ed25519_certificate str: base64 encoded ed25519 certificate
+  :var ed25519_signature str: signature of this document using ed25519
+
   **Bi-directional connection usage:**
 
   :var datetime conn_bi_direct_end: end of the sampling interval
@@ -689,6 +695,9 @@ class ExtraInfoDescriptor(Descriptor):
   .. versionchanged:: 1.4.0
      Added the hs_stats_end, hs_rend_cells, hs_rend_cells_attr,
      hs_dir_onions_seen, and hs_dir_onions_seen_attr attributes.
+
+  .. versionchanged:: 1.5.0
+     Added the ed25519_certificate and ed25519_signature attributes.
   """
 
   ATTRIBUTES = {
@@ -699,6 +708,9 @@ class ExtraInfoDescriptor(Descriptor):
     'geoip6_db_digest': (None, _parse_geoip6_db_digest_line),
     'transport': ({}, _parse_transport_line),
 
+    'ed25519_certificate': (None, _parse_identity_ed25519_line),
+    'ed25519_signature': (None, _parse_router_sig_ed25519_line),
+
     'conn_bi_direct_end': (None, _parse_conn_bi_direct_line),
     'conn_bi_direct_interval': (None, _parse_conn_bi_direct_line),
     'conn_bi_direct_below': (None, _parse_conn_bi_direct_line),
@@ -778,6 +790,8 @@ class ExtraInfoDescriptor(Descriptor):
 
   PARSER_FOR_LINE = {
     'extra-info': _parse_extra_info_line,
+    'identity-ed25519': _parse_identity_ed25519_line,
+    'router-sig-ed25519': _parse_router_sig_ed25519_line,
     'geoip-db-digest': _parse_geoip_db_digest_line,
     'geoip6-db-digest': _parse_geoip6_db_digest_line,
     'transport': _parse_transport_line,
diff --git a/stem/descriptor/router_status_entry.py b/stem/descriptor/router_status_entry.py
index c33baa3..1d0305a 100644
--- a/stem/descriptor/router_status_entry.py
+++ b/stem/descriptor/router_status_entry.py
@@ -265,8 +265,11 @@ def _parse_w_line(descriptor, entries):
 
 def _parse_p_line(descriptor, entries):
   # "p" ("accept" / "reject") PortList
-  # p reject 1-65535
-  # example: p accept 80,110,143,443,993,995,6660-6669,6697,7000-7001
+  #
+  # examples:
+  #
+  #   p accept 80,110,143,443,993,995,6660-6669,6697,7000-7001
+  #   p reject 1-65535
 
   value = _value('p', entries)
 
@@ -276,6 +279,30 @@ def _parse_p_line(descriptor, entries):
     raise ValueError('%s exit policy is malformed (%s): p %s' % (descriptor._name(), exc, value))
 
 
+def _parse_id_line(descriptor, entries):
+  # "id" "ed25519" ed25519-identity
+  #
+  # examples:
+  #
+  #   id ed25519 none
+  #   id ed25519 8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8
+
+  value = _value('id', entries)
+
+  if value:
+    if not (descriptor.document and descriptor.document.is_vote):
+      vote_status = 'vote' if descriptor.document else '<undefined document>'
+      raise ValueError("%s 'id' line should only appear in votes (appeared in a %s): id %s" % (descriptor._name(), vote_status, value))
+
+    value_comp = value.split()
+
+    if len(value_comp) >= 2:
+      descriptor.identifier_type = value_comp[0]
+      descriptor.identifier = value_comp[1]
+    else:
+      raise ValueError("'id' lines should contain both the key type and digest: id %s" % value)
+
+
 def _parse_m_line(descriptor, entries):
   # "m" methods 1*(algorithm "=" digest)
   # example: m 8,9,10,11,12 sha256=g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs
@@ -512,6 +539,8 @@ class RouterStatusEntryV3(RouterStatusEntry):
 
   :var list or_addresses: **\*** relay's OR addresses, this is a tuple listing
     of the form (address (**str**), port (**int**), is_ipv6 (**bool**))
+  :var str identifier_type: identity digest key type
+  :var str identifier: base64 encoded identity digest
   :var str digest: **\*** router's upper-case hex digest
 
   :var int bandwidth: bandwidth claimed by the relay (in kb/s)
@@ -531,11 +560,16 @@ class RouterStatusEntryV3(RouterStatusEntry):
 
   **\*** attribute is either required when we're parsed with validation or has
   a default value, others are left as **None** if undefined
+
+  .. versionchanged:: 1.5.0
+     Added the identifier and identifier_type attributes.
   """
 
   ATTRIBUTES = dict(RouterStatusEntry.ATTRIBUTES, **{
     'digest': (None, _parse_r_line),
     'or_addresses': ([], _parse_a_line),
+    'identifier_type': (None, _parse_id_line),
+    'identifier': (None, _parse_id_line),
 
     'bandwidth': (None, _parse_w_line),
     'measured': (None, _parse_w_line),
@@ -550,6 +584,7 @@ class RouterStatusEntryV3(RouterStatusEntry):
     'a': _parse_a_line,
     'w': _parse_w_line,
     'p': _parse_p_line,
+    'id': _parse_id_line,
     'm': _parse_m_line,
   })
 
diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
index 22e6f40..872806b 100644
--- a/stem/descriptor/server_descriptor.py
+++ b/stem/descriptor/server_descriptor.py
@@ -78,6 +78,8 @@ REQUIRED_FIELDS = (
 
 # optional entries that can appear at most once
 SINGLE_FIELDS = (
+  'identity-ed25519',
+  'master-key-ed25519',
   'platform',
   'fingerprint',
   'hibernating',
@@ -92,7 +94,10 @@ SINGLE_FIELDS = (
   'hidden-service-dir',
   'protocols',
   'allow-single-hop-exits',
+  'onion-key-crosscert',
   'ntor-onion-key',
+  'ntor-onion-key-crosscert',
+  'router-sig-ed25519',
 )
 
 DEFAULT_IPV6_EXIT_POLICY = stem.exit_policy.MicroExitPolicy('reject 1-65535')
@@ -382,6 +387,8 @@ def _parse_exit_policy(descriptor, entries):
     del descriptor._unparsed_exit_policy
 
 
+_parse_identity_ed25519_line = _parse_key_block('identity-ed25519', 'ed25519_certificate', 'ED25519 CERT')
+_parse_master_key_ed25519_line = _parse_simple_line('master-key-ed25519', 'ed25519_master_key')
 _parse_contact_line = _parse_bytes_line('contact', 'contact')
 _parse_published_line = _parse_timestamp_line('published', 'published')
 _parse_read_history_line = functools.partial(_parse_history_line, 'read-history', 'read_history_end', 'read_history_interval', 'read_history_values')
@@ -392,9 +399,12 @@ _parse_caches_extra_info_line = lambda descriptor, entries: setattr(descriptor,
 _parse_family_line = lambda descriptor, entries: setattr(descriptor, 'family', set(_value('family', entries).split(' ')))
 _parse_eventdns_line = lambda descriptor, entries: setattr(descriptor, 'eventdns', _value('eventdns', entries) == '1')
 _parse_onion_key_line = _parse_key_block('onion-key', 'onion_key', 'RSA PUBLIC KEY')
+_parse_onion_key_crosscert_line = _parse_key_block('onion-key-crosscert', 'onion_key_crosscert', 'CROSSCERT')
 _parse_signing_key_line = _parse_key_block('signing-key', 'signing_key', 'RSA PUBLIC KEY')
 _parse_router_signature_line = _parse_key_block('router-signature', 'signature', 'SIGNATURE')
 _parse_ntor_onion_key_line = _parse_simple_line('ntor-onion-key', 'ntor_onion_key')
+_parse_ntor_onion_key_crosscert_line = _parse_key_block('ntor-onion-key-crosscert', 'ntor_onion_key_crosscert', 'ED25519 CERT', 'ntor_onion_key_crosscert_sign')
+_parse_router_sig_ed25519_line = _parse_simple_line('router-sig-ed25519', 'ed25519_signature')
 _parse_router_digest_line = _parse_forty_character_hex('router-digest', '_digest')
 
 
@@ -411,6 +421,10 @@ class ServerDescriptor(Descriptor):
   :var int socks_port: **\*** port used as client (deprecated, always **None**)
   :var int dir_port: **\*** port used for descriptor mirroring
 
+  :var ed25519_certificate str: base64 encoded ed25519 certificate
+  :var ed25519_master_key str: base64 encoded master key for our ed25519 certificate
+  :var ed25519_signature str: signature of this document using ed25519
+
   :var bytes platform: line with operating system and tor version
   :var stem.version.Version tor_version: version of tor
   :var str operating_system: operating system
@@ -434,6 +448,9 @@ class ServerDescriptor(Descriptor):
   :var list or_addresses: **\*** alternative for our address/or_port
     attributes, each entry is a tuple of the form (address (**str**), port
     (**int**), is_ipv6 (**bool**))
+  :var str onion_key_crosscert: signature generated using the onion_key
+  :var str ntor_onion_key_crosscert: signature generated using the ntor-onion-key
+  :var str ntor_onion_key_crosscert_sign: sign of the corresponding ed25519 public key
 
   Deprecated, moved to extra-info descriptor...
 
@@ -447,6 +464,11 @@ class ServerDescriptor(Descriptor):
 
   **\*** attribute is either required when we're parsed with validation or has
   a default value, others are left as **None** if undefined
+
+  .. versionchanged:: 1.5.0
+     Added the ed25519_certificate, ed25519_master_key, ed25519_signature,
+     onion_key_crosscert, ntor_onion_key_crosscert, and
+     ntor_onion_key_crosscert_sign attributes.
   """
 
   ATTRIBUTES = {
@@ -461,6 +483,10 @@ class ServerDescriptor(Descriptor):
     'socks_port': (None, _parse_router_line),
     'dir_port': (None, _parse_router_line),
 
+    'ed25519_certificate': (None, _parse_identity_ed25519_line),
+    'ed25519_master_key': (None, _parse_master_key_ed25519_line),
+    'ed25519_signature': (None, _parse_router_sig_ed25519_line),
+
     'platform': (None, _parse_platform_line),
     'tor_version': (None, _parse_platform_line),
     'operating_system': (None, _parse_platform_line),
@@ -481,6 +507,9 @@ class ServerDescriptor(Descriptor):
     'hidden_service_dir': (None, _parse_hidden_service_dir_line),
     'eventdns': (None, _parse_eventdns_line),
     'or_addresses': ([], _parse_or_address_line),
+    'onion_key_crosscert': (None, _parse_onion_key_crosscert_line),
+    'ntor_onion_key_crosscert': (None, _parse_ntor_onion_key_crosscert_line),
+    'ntor_onion_key_crosscert_sign': (None, _parse_ntor_onion_key_crosscert_line),
 
     'read_history_end': (None, _parse_read_history_line),
     'read_history_interval': (None, _parse_read_history_line),
@@ -493,6 +522,9 @@ class ServerDescriptor(Descriptor):
 
   PARSER_FOR_LINE = {
     'router': _parse_router_line,
+    'identity-ed25519': _parse_identity_ed25519_line,
+    'master-key-ed25519': _parse_master_key_ed25519_line,
+    'router-sig-ed25519': _parse_router_sig_ed25519_line,
     'bandwidth': _parse_bandwidth_line,
     'platform': _parse_platform_line,
     'published': _parse_published_line,
@@ -504,6 +536,8 @@ class ServerDescriptor(Descriptor):
     'uptime': _parse_uptime_line,
     'protocols': _parse_protocols_line,
     'or-address': _parse_or_address_line,
+    'onion-key-crosscert': _parse_onion_key_crosscert_line,
+    'ntor-onion-key-crosscert': _parse_ntor_onion_key_crosscert_line,
     'read-history': _parse_read_history_line,
     'write-history': _parse_write_history_line,
     'ipv6-policy': _parse_ipv6_policy_line,
@@ -636,6 +670,12 @@ class ServerDescriptor(Descriptor):
     if not self.exit_policy:
       raise ValueError("Descriptor must have at least one 'accept' or 'reject' entry")
 
+    if self.ed25519_certificate:
+      if not self.onion_key_crosscert:
+        raise ValueError("Descriptor must have a 'onion-key-crosscert' when identity-ed25519 is present")
+      elif not self.ed25519_signature:
+        raise ValueError("Descriptor must have a 'router-sig-ed25519' when identity-ed25519 is present")
+
   # Constraints that the descriptor must meet to be valid. These can be None if
   # not applicable.
 
diff --git a/test/unit/descriptor/data/extrainfo_descriptor_with_ed25519 b/test/unit/descriptor/data/extrainfo_descriptor_with_ed25519
new file mode 100644
index 0000000..d49ff7d
--- /dev/null
+++ b/test/unit/descriptor/data/extrainfo_descriptor_with_ed25519
@@ -0,0 +1,26 @@
+ at type extra-info 1.0
+extra-info silverfoxden 4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E
+identity-ed25519
+-----BEGIN ED25519 CERT-----
+AQQABhz0AQFcf5tGWLvPvr1sktoezBB95j6tAWSECa3Eo2ZuBtRNAQAgBABFAwSN
+GcRlGIte4I1giLvQSTcXefT93rvx2PZ8wEDewxWdy6tzcLouPfE3Beu/eUyg8ntt
+YuVlzi50WXzGlGnPmeounGLo0EDHTGzcLucFWpe0g/0ia6UDqgQiAySMBwI=
+-----END ED25519 CERT-----
+published 2015-08-22 19:21:12
+write-history 2015-08-22 19:20:44 (14400 s) 14409728,23076864,7756800,6234112,7446528,12290048
+read-history 2015-08-22 19:20:44 (14400 s) 20449280,23888896,9099264,7185408,8880128,13230080
+geoip-db-digest 6882B8663F74C23E26E3C2274C24CAB2E82D67A2
+geoip6-db-digest F063BD5247EB9829E6B9E586393D7036656DAF44
+dirreq-stats-end 2015-08-22 11:58:30 (86400 s)
+dirreq-v3-ips 
+dirreq-v3-reqs 
+dirreq-v3-resp ok=0,not-enough-sigs=0,unavailable=0,not-found=0,not-modified=0,busy=0
+dirreq-v3-direct-dl complete=0,timeout=0,running=0
+dirreq-v3-tunneled-dl complete=0,timeout=0,running=0
+router-sig-ed25519 g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw
+router-signature
+-----BEGIN SIGNATURE-----
+R7kNaIWZrg3n3FWFBRMlEK2cbnha7gUIs8ToksLe+SF0dgoZiLyV3GKrnzdE/K6D
+qdiOMN7eK04MOZVlgxkA5ayi61FTYVveK1HrDbJ+sEUwsviVGdif6kk/9DXOiyIJ
+7wP/tofgHj/aCbFZb1PGU0zrEVLa72hVJ6cCW8w/t1s=
+-----END SIGNATURE-----
diff --git a/test/unit/descriptor/data/server_descriptor_with_ed25519 b/test/unit/descriptor/data/server_descriptor_with_ed25519
index 2f43ced..87d1d24 100644
--- a/test/unit/descriptor/data/server_descriptor_with_ed25519
+++ b/test/unit/descriptor/data/server_descriptor_with_ed25519
@@ -1,50 +1,72 @@
 @type server-descriptor 1.0
-router Truie 198.50.156.78 9001 0 9030
+router destiny 94.242.246.23 9001 0 443
 identity-ed25519
 -----BEGIN ED25519 CERT-----
-AQQABhWIAZTz0r0KRagr6X9SHfm4oiIuMLVhJQQmNchtkBuR5SuFAQAgBAAVkw7m
-0YJgO/A8VMioco097sIOutDiM7UqqPvoIyKErk1akOm3f6VAO/juOzxEeAgzgfA7
-DiRsSjeVjp0xUdE43bXhK/8Uh+SPMwYKj47drjgTHGgzjTmlY9B/jFJ1Wgs=
+AQQABhtZAaW2GoBED1IjY3A6f6GNqBEl5A83fD2Za9upGke51JGqAQAgBABnprVR
+ptIr43bWPo2fIzo3uOywfoMrryprpbm4HhCkZMaO064LP+1KNuLvlc8sGG8lTjx1
+g4k3ELuWYgHYWU5rAia7nl4gUfBZOEfHAfKES7l3d63dBEjEX98Ljhdp2w4=
 -----END ED25519 CERT-----
-platform Tor 0.2.7.1-alpha-dev on Linux
+master-key-ed25519 Z6a1UabSK+N21j6NnyM6N7jssH6DK68qa6W5uB4QpGQ
+or-address [2a01:608:ffff:ff07::1:23]:9003
+platform Tor 0.2.7.2-alpha-dev on Linux
 protocols Link 1 2 Circuit 1
-published 2015-05-28 15:44:47
-fingerprint A692 21A7 EC74 98D2 F88A 0FB7 9526 1013 FA36 CAAE
-uptime 61
-bandwidth 1073741824 1073741824 9506816
-extra-info-digest 0879DB7B765218D7B3AE7557669D20307BB21CAA V609l+N6ActBveebfNbH5lQ6wHDNstDkFgyqEhBHwtA
+published 2015-08-22 15:21:45
+fingerprint F65E 0196 C94D FFF4 8AFB F2F5 F9E3 E19A AE58 3FD0
+uptime 1362680
+bandwidth 149715200 1048576000 51867731
+extra-info-digest 44E9B679AF0B4EB09296985BAF4066AE9CA5BB93 r+roMxhsjd1GPpn5knQoBvtE9Rhsv8zQHCqiYL6u2CA
 onion-key
 -----BEGIN RSA PUBLIC KEY-----
-MIGJAoGBALbTpnPvhaGET+2ACtLdG6jhQXN8uVJ0iF9RwMh2hwu351yp3eVPt7os
-ditUF6w7KV+6emkvLu9EBpNN7vWrpDAhRNOGTOZhZKLnGFaxp+eGNX6+5AhmiWYt
-/+w+f6dvVKEjsaX3XZsMqcTBjw2hzVpHxh/AjgDx/b9mJKC85vENAgMBAAE=
+MIGJAoGBAKpPOeBPFBZhH32k0CmIVsXMi4mbbkpEAYpZD0Z3/zLc9k05qAvhE55h
++LXqG6C6k23JnR7H1a4EtFU0UQVWxUa4xUL9pi/0tj3Zsu842Z18K3sL8hYWDw6x
+b6afVdSKIcY6guG5fevmobUd/6437oSwM7IeXrWy28s0PtWKHhQzAgMBAAE=
 -----END RSA PUBLIC KEY-----
 signing-key
 -----BEGIN RSA PUBLIC KEY-----
-MIGJAoGBALDSt2G+Zjl20a59HZsuag913ONdnnNa/uVMRbsZZkbnNRONf2aXBGgu
-wrW7XtPLeAKl+d0d5g9XnePVvefcEdKvoKNCFv6s8s3S2KB/CEkeyE7Lxx1Pc6Qx
-f/jgS3T3TFHUlvtZvHLZ/3WaXMyuTTRlGadpzDkQx5oWR6aNn065AgMBAAE=
+MIGJAoGBAOUS7xm+1d/FAk7VHx2SaYzjYoGpNaCHHWXlmDz2+iWEqcDRjjnVFekV
+sfAPysNnB0a/lHdrqzyKjCkzAoeut5Ts3bj6eMrF3psFian2IqdlqsFaAcBov7fo
+J6ipwr8lP72LOMHlB2AwP3BEWtHZX7nmARV7ekbPs21R06lEhzLLAgMBAAE=
 -----END RSA PUBLIC KEY-----
 onion-key-crosscert
 -----BEGIN CROSSCERT-----
-TCcCIv38fGcSzUO+DKxudFme2XBRuDkf5FjEr+6UbtDyuDjvjJDFYagN+zMJf/4K
-RyBScjyKYK6MVMxAmf25QjAGx3KHV00ozVSzlN3WDAS2iicuKYvBsehG9g/tr6mI
-luS5EoSKJIlmM2jOhN1QyR+Rpi37z/E6VTksk/bd69A=
+iW8BqwH5VKqZaiMgPcuHIQFpiQnRsd2b1zc+PXVN3AFT0cQx6J4rZhIdxiqHeNqj
+fVEoi4+iHkbksGABZKlB/x7Kv2Kvbj3ZH46m22KEASkRL+i9EhCYdf3Ju7czIi/7
+U/jQTwhn7+o8LCLsLhw3aV/v/sXEtbxePhMbCMHI7hE=
 -----END CROSSCERT-----
 ntor-onion-key-crosscert 0
 -----BEGIN ED25519 CERT-----
-AQoABhNgARWTDubRgmA78DxUyKhyjT3uwg660OIztSqo++gjIoSuAEW8gwMcFUSD
-mfkijKN6KyZxHloENGcgJMeJsR9kvfYp/u7O+VoPQ1kTxaw1lajTrnGQF+PV1MlK
-niid4Nq5ZgM=
+AQoABhtwAWemtVGm0ivjdtY+jZ8jOje47LB+gyuvKmulubgeEKRkAHj4IPqm+osx
+vbKfvRHeZ0uaghFPZr76UVPYwuK4N+VcW75yq2vuFSsFTCJqamPB3PIdSz6rbx4U
+4F3iroztLAQ=
 -----END ED25519 CERT-----
+family $379FB450010D17078B3766C2273303C358C3A442 $3EB46C1D8D8B1C0BBCB6E4F08301EF68B7F5308D $B0279A521375F3CB2AE210BDBFC645FDD2E1973A $EC116BCB80565A408CE67F8EC3FE3B0B02C3A065
 hidden-service-dir
-contact 0x11F48D36 David Goulet <dgoulet AT ev0ke dot net>
-ntor-onion-key qDcuoDpDD36bIapIbXBVhkIoiuMIXD9jNfjF1+7Vaks=
-reject *:*
-router-sig-ed25519 AxqrLz7QL/e+xGhhihs/rNzWsBW0Qla7Cwru1q88A5i+pcQBgfzfECiecptqYbDAsUPXMtwFsLp7Ls2BMOzvCQ
+contact 0x02225522 Frenn vun der Enn (FVDE) <info AT enn DOT lu>
+ntor-onion-key JCj8BOqk0Khfp1hfoJaDbSTzNgeA/u2pSAXnaR3vhl0=
+reject 0.0.0.0/8:*
+reject 169.254.0.0/16:*
+reject 127.0.0.0/8:*
+reject 192.168.0.0/16:*
+reject 10.0.0.0/8:*
+reject 172.16.0.0/12:*
+reject 94.242.246.23:*
+reject *:25
+reject *:587
+reject *:465
+reject 176.67.160.187:*
+reject 185.35.77.160:*
+reject 185.35.77.250:*
+reject *:10000
+reject *:14464
+reject 94.100.180.202:*
+reject 217.69.139.215:*
+reject 217.69.140.233:*
+accept *:*
+ipv6-policy reject 25,465,587,10000,14464
+router-sig-ed25519 w+cKNZTlL7vz/4WgYdFUblzJy3VdTw0mfFK4N3SPFCt20fNKt9SgiZ5V/2ai3kgGsc6oCsyUesSiYtPcTXMLCw
 router-signature
 -----BEGIN SIGNATURE-----
-mSkveaqx79vzXLc6yC2+x8yZMQPe74ihw9tZJDdSOK5VqhzZOKHFM+JoD12noxQd
-wgxa+IX0RG65KlguYE7NEZ7M6JOwr6r0zK/pWSZE8ZeHyt7FDx9ygc3k2ybQ6RWE
-Hd7QXPiyVgs9cIgnvGFVt/5vzjMV+BELpOtehBrUJbs=
+y72z1dZOYxVQVLRMvEJOn9lOFxBsjojpwiYxw+3vWFHnhkOdGqolxJ6gTLhiIXNu
+ckBPqxjbpFbmt6qgk0oeivwyLo9o4nZT737d3tx1EuBmxo+gqzNtukXWzJzZFIj5
+xE0eo9e/zKPSCF/LK6zv0FSefdBpnEkYYFuGN0BCrZo=
 -----END SIGNATURE-----
diff --git a/test/unit/descriptor/extrainfo_descriptor.py b/test/unit/descriptor/extrainfo_descriptor.py
index 5cb3205..b4bc34f 100644
--- a/test/unit/descriptor/extrainfo_descriptor.py
+++ b/test/unit/descriptor/extrainfo_descriptor.py
@@ -137,6 +137,20 @@ k0d2aofcVbHr4fPQOSST0LXDrhFl5Fqo5um296zpJGvRUeO6S44U/EfJAGShtqWw
     self.assertEqual('478B4CB438302981DE9AAF246F48DBE57F69050A', desc_list[4].fingerprint)
     self.assertEqual('25D9D52A0350B42E69C8AB7CE945DB1CA38DA0CF', desc_list[5].fingerprint)
 
+  def test_with_ed25519(self):
+    """
+    Parses a descriptor with a ed25519 identity key.
+    """
+
+    with open(get_resource('extrainfo_descriptor_with_ed25519'), 'rb') as descriptor_file:
+      desc = next(stem.descriptor.parse_file(descriptor_file, 'extra-info 1.0', validate = True))
+
+    self.assertEqual('silverfoxden', desc.nickname)
+    self.assertEqual('4970B1DC3DBC8D82D7F1E43FF44B28DBF4765A4E', desc.fingerprint)
+    self.assertTrue('AQQABhz0AQFcf5tGWLvPvr' in desc.ed25519_certificate)
+    self.assertEqual('g6Zg7Er8K7C1etmt7p20INE1ExIvMRPvhwt6sjbLqEK+EtQq8hT+86hQ1xu7cnz6bHee+Zhhmcc4JamV4eiMAw', desc.ed25519_signature)
+    self.assertEqual([], desc.get_unrecognized_lines())
+
   def test_minimal_extrainfo_descriptor(self):
     """
     Basic sanity check that we can parse an extrainfo descriptor with minimal
diff --git a/test/unit/descriptor/router_status_entry.py b/test/unit/descriptor/router_status_entry.py
index 14df828..774a3e4 100644
--- a/test/unit/descriptor/router_status_entry.py
+++ b/test/unit/descriptor/router_status_entry.py
@@ -17,6 +17,39 @@ from test.mocking import (
   ROUTER_STATUS_ENTRY_V3_HEADER,
 )
 
+ENTRY_WITHOUT_ED25519 = """\
+r seele AAoQ1DAR6kkoo19hBAX5K0QztNw m0ynPuwzSextzsiXYJYA0Hce+Cs 2015-08-23 00:26:35 73.15.150.172 9001 0
+s Running Stable Valid
+v Tor 0.2.6.10
+w Bandwidth=102 Measured=31
+p reject 1-65535
+id ed25519 none
+m 13,14,15 sha256=uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc
+m 16,17 sha256=G6FmPe/ehgfb6tsRzFKDCwvvae+RICeP1MaP0vWDGyI
+m 18,19,20,21 sha256=/XhIMOnhElo2UiKjL2S10uRka/fhg1CFfNd+9wgUwEE
+"""
+
+ENTRY_WITH_ED25519 = """\
+r PDrelay1 AAFJ5u9xAqrKlpDW6N0pMhJLlKs yrJ6b/73pmHBiwsREgw+inf8WFw 2015-08-23 16:52:37 95.215.44.189 8080 0
+s Fast Running Stable Valid
+v Tor 0.2.7.2-alpha-dev
+w Bandwidth=608 Measured=472
+p reject 1-65535
+id ed25519 8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8
+m 13 sha256=PTSHzE7RKnRGZMRmBddSzDiZio254FUhv9+V4F5zq8s
+m 14,15 sha256=0wsEwBbxJ8RtPmGYwilHQTVEw2pWzUBEVlSgEO77OyU
+m 16,17 sha256=JK2xhYr/VsCF60px+LsT990BCpfKfQTeMxRbD63o2vE
+m 18,19,20 sha256=AkZH3gIvz3wunsroqh5izBJizdYuR7kn2oVbsvqgML8
+m 21 sha256=AVp41YVxKEJCaoEf0+77Cdvyw5YgpyDXdob0+LSv/pE
+"""
+
+
+def vote_document():
+  mock_document = lambda x: x  # just need anything with a __dict__
+  setattr(mock_document, 'is_vote', True)
+  setattr(mock_document, 'is_consensus', False)
+  return mock_document
+
 
 class TestRouterStatusEntry(unittest.TestCase):
   def test_fingerprint_decoding(self):
@@ -86,6 +119,8 @@ class TestRouterStatusEntry(unittest.TestCase):
     self.assertEqual([], entry.unrecognized_bandwidth_entries)
     self.assertEqual(None, entry.exit_policy)
     self.assertEqual([], entry.microdescriptor_hashes)
+    self.assertEqual(None, entry.identifier_type)
+    self.assertEqual(None, entry.identifier)
     self.assertEqual([], entry.get_unrecognized_lines())
 
   def test_minimal_micro_v3(self):
@@ -109,6 +144,72 @@ class TestRouterStatusEntry(unittest.TestCase):
     self.assertEqual('6A252497006BB9AF36A1B1B902C4D7FA2129923400DBE0101F167B1B031F63BD', entry.digest)
     self.assertEqual([], entry.get_unrecognized_lines())
 
+  def test_without_ed25519(self):
+    """
+    Parses a router status entry without a ed25519 value.
+    """
+
+    microdescriptor_hashes = [
+      ([13, 14, 15], {'sha256': 'uaAYTOVuYRqUwJpNfP2WizjzO0FiNQB4U97xSQu+vMc'}),
+      ([16, 17], {'sha256': 'G6FmPe/ehgfb6tsRzFKDCwvvae+RICeP1MaP0vWDGyI'}),
+      ([18, 19, 20, 21], {'sha256': '/XhIMOnhElo2UiKjL2S10uRka/fhg1CFfNd+9wgUwEE'}),
+    ]
+
+    entry = RouterStatusEntryV3(ENTRY_WITHOUT_ED25519, document = vote_document(), validate = True)
+    self.assertEqual('seele', entry.nickname)
+    self.assertEqual('000A10D43011EA4928A35F610405F92B4433B4DC', entry.fingerprint)
+    self.assertEqual(datetime.datetime(2015, 8, 23, 0, 26, 35), entry.published)
+    self.assertEqual('73.15.150.172', entry.address)
+    self.assertEqual(9001, entry.or_port)
+    self.assertEqual(None, entry.dir_port)
+    self.assertEqual(set([Flag.RUNNING, Flag.STABLE, Flag.VALID]), set(entry.flags))
+    self.assertEqual('Tor 0.2.6.10', entry.version_line)
+    self.assertEqual(Version('0.2.6.10'), entry.version)
+    self.assertEqual(102, entry.bandwidth)
+    self.assertEqual(31, entry.measured)
+    self.assertEqual(False, entry.is_unmeasured)
+    self.assertEqual([], entry.unrecognized_bandwidth_entries)
+    self.assertEqual(MicroExitPolicy('reject 1-65535'), entry.exit_policy)
+    self.assertEqual(microdescriptor_hashes, entry.microdescriptor_hashes)
+    self.assertEqual('ed25519', entry.identifier_type)
+    self.assertEqual('none', entry.identifier)
+    self.assertEqual('9B4CA73EEC3349EC6DCEC897609600D0771EF82B', entry.digest)
+    self.assertEqual([], entry.get_unrecognized_lines())
+
+  def test_with_ed25519(self):
+    """
+    Parses a router status entry with a ed25519 value.
+    """
+
+    microdescriptor_hashes = [
+      ([13], {'sha256': 'PTSHzE7RKnRGZMRmBddSzDiZio254FUhv9+V4F5zq8s'}),
+      ([14, 15], {'sha256': '0wsEwBbxJ8RtPmGYwilHQTVEw2pWzUBEVlSgEO77OyU'}),
+      ([16, 17], {'sha256': 'JK2xhYr/VsCF60px+LsT990BCpfKfQTeMxRbD63o2vE'}),
+      ([18, 19, 20], {'sha256': 'AkZH3gIvz3wunsroqh5izBJizdYuR7kn2oVbsvqgML8'}),
+      ([21], {'sha256': 'AVp41YVxKEJCaoEf0+77Cdvyw5YgpyDXdob0+LSv/pE'}),
+    ]
+
+    entry = RouterStatusEntryV3(ENTRY_WITH_ED25519, document = vote_document(), validate = True)
+    self.assertEqual('PDrelay1', entry.nickname)
+    self.assertEqual('000149E6EF7102AACA9690D6E8DD2932124B94AB', entry.fingerprint)
+    self.assertEqual(datetime.datetime(2015, 8, 23, 16, 52, 37), entry.published)
+    self.assertEqual('95.215.44.189', entry.address)
+    self.assertEqual(8080, entry.or_port)
+    self.assertEqual(None, entry.dir_port)
+    self.assertEqual(set([Flag.FAST, Flag.RUNNING, Flag.STABLE, Flag.VALID]), set(entry.flags))
+    self.assertEqual('Tor 0.2.7.2-alpha-dev', entry.version_line)
+    self.assertEqual(Version('0.2.7.2-alpha-dev'), entry.version)
+    self.assertEqual(608, entry.bandwidth)
+    self.assertEqual(472, entry.measured)
+    self.assertEqual(False, entry.is_unmeasured)
+    self.assertEqual([], entry.unrecognized_bandwidth_entries)
+    self.assertEqual(MicroExitPolicy('reject 1-65535'), entry.exit_policy)
+    self.assertEqual(microdescriptor_hashes, entry.microdescriptor_hashes)
+    self.assertEqual('ed25519', entry.identifier_type)
+    self.assertEqual('8RH34kO07Pp+XYwzdoATVyCibIvmbslUjRkAm7J4IA8', entry.identifier)
+    self.assertEqual('CAB27A6FFEF7A661C18B0B11120C3E8A77FC585C', entry.digest)
+    self.assertEqual([], entry.get_unrecognized_lines())
+
   def test_missing_fields(self):
     """
     Parses a router status entry that's missing fields.
@@ -478,14 +579,9 @@ class TestRouterStatusEntry(unittest.TestCase):
         [([8, 9, 10, 11, 12], {'sha256': 'g1vx9si329muxV', 'md5': '3tquWIXXySNOIwRGMeAESKs/v4DWs'})],
     }
 
-    # we need a document that's a vote
-    mock_document = lambda x: x  # just need anything with a __dict__
-    setattr(mock_document, 'is_vote', True)
-    setattr(mock_document, 'is_consensus', False)
-
     for m_line, expected in test_values.items():
       content = get_router_status_entry_v3({'m': m_line}, content = True)
-      entry = RouterStatusEntryV3(content, document = mock_document)
+      entry = RouterStatusEntryV3(content, document = vote_document())
       self.assertEqual(expected, entry.microdescriptor_hashes)
 
     # try with multiple 'm' lines
@@ -499,7 +595,7 @@ class TestRouterStatusEntry(unittest.TestCase):
       ([31, 32], {'sha512': 'g1vx9si329muxV3tquWIXXySNOIwRGMeAESKs/v4DWs'}),
     ]
 
-    entry = RouterStatusEntryV3(content, document = mock_document)
+    entry = RouterStatusEntryV3(content, document = vote_document())
     self.assertEqual(expected, entry.microdescriptor_hashes)
 
     # try without a document
@@ -515,7 +611,7 @@ class TestRouterStatusEntry(unittest.TestCase):
 
     for m_line in test_values:
       content = get_router_status_entry_v3({'m': m_line}, content = True)
-      self.assertRaises(ValueError, RouterStatusEntryV3, content, True, mock_document)
+      self.assertRaises(ValueError, RouterStatusEntryV3, content, True, vote_document())
 
   def _expect_invalid_attr(self, content, attr = None, expected_value = None):
     """
diff --git a/test/unit/descriptor/server_descriptor.py b/test/unit/descriptor/server_descriptor.py
index 4ee60f3..1218bee 100644
--- a/test/unit/descriptor/server_descriptor.py
+++ b/test/unit/descriptor/server_descriptor.py
@@ -109,6 +109,9 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     self.assertEqual(9001, desc.or_port)
     self.assertEqual(None, desc.socks_port)
     self.assertEqual(None, desc.dir_port)
+    self.assertEqual(None, desc.ed25519_certificate)
+    self.assertEqual(None, desc.ed25519_master_key)
+    self.assertEqual(None, desc.ed25519_signature)
     self.assertEqual(b'Tor 0.2.1.30 on Linux x86_64', desc.platform)
     self.assertEqual(stem.version.Version('0.2.1.30'), desc.tor_version)
     self.assertEqual('Linux x86_64', desc.operating_system)
@@ -128,6 +131,9 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     self.assertEqual(104590, desc.observed_bandwidth)
     self.assertEqual(stem.exit_policy.ExitPolicy('reject *:*'), desc.exit_policy)
     self.assertEqual(expected_onion_key, desc.onion_key)
+    self.assertEqual(None, desc.onion_key_crosscert)
+    self.assertEqual(None, desc.ntor_onion_key_crosscert)
+    self.assertEqual(None, desc.onion_key_crosscert)
     self.assertEqual(expected_signing_key, desc.signing_key)
     self.assertEqual(expected_signature, desc.signature)
     self.assertEqual([], desc.get_unrecognized_lines())
@@ -245,35 +251,49 @@ Qlx9HNCqCY877ztFRC624ja2ql6A2hBcuoYMbkHjcQ4=
     with open(get_resource('server_descriptor_with_ed25519'), 'rb') as descriptor_file:
       desc = next(stem.descriptor.parse_file(descriptor_file, 'server-descriptor 1.0', validate = True))
 
-    self.assertEqual('Truie', desc.nickname)
-    self.assertEqual('A69221A7EC7498D2F88A0FB795261013FA36CAAE', desc.fingerprint)
-    self.assertEqual('198.50.156.78', desc.address)
+    family = set([
+      '$379FB450010D17078B3766C2273303C358C3A442',
+      '$3EB46C1D8D8B1C0BBCB6E4F08301EF68B7F5308D',
+      '$B0279A521375F3CB2AE210BDBFC645FDD2E1973A',
+      '$EC116BCB80565A408CE67F8EC3FE3B0B02C3A065',
+    ])
+
+    self.assertEqual('destiny', desc.nickname)
+    self.assertEqual('F65E0196C94DFFF48AFBF2F5F9E3E19AAE583FD0', desc.fingerprint)
+    self.assertEqual('94.242.246.23', desc.address)
     self.assertEqual(9001, desc.or_port)
     self.assertEqual(None, desc.socks_port)
-    self.assertEqual(9030, desc.dir_port)
-    self.assertEqual(b'Tor 0.2.7.1-alpha-dev on Linux', desc.platform)
-    self.assertEqual(stem.version.Version('0.2.7.1-alpha-dev'), desc.tor_version)
+    self.assertEqual(443, desc.dir_port)
+    self.assertTrue('bWPo2fIzo3uOywfoM' in desc.ed25519_certificate)
+    self.assertEqual('Z6a1UabSK+N21j6NnyM6N7jssH6DK68qa6W5uB4QpGQ', desc.ed25519_master_key)
+    self.assertEqual('w+cKNZTlL7vz/4WgYdFUblzJy3VdTw0mfFK4N3SPFCt20fNKt9SgiZ5V/2ai3kgGsc6oCsyUesSiYtPcTXMLCw', desc.ed25519_signature)
+    self.assertEqual(b'Tor 0.2.7.2-alpha-dev on Linux', desc.platform)
+    self.assertEqual(stem.version.Version('0.2.7.2-alpha-dev'), desc.tor_version)
     self.assertEqual('Linux', desc.operating_system)
-    self.assertEqual(61, desc.uptime)
-    self.assertEqual(datetime.datetime(2015, 5, 28, 15, 44, 47), desc.published)
-    self.assertEqual(b'0x11F48D36 David Goulet <dgoulet AT ev0ke dot net>', desc.contact)
+    self.assertEqual(1362680, desc.uptime)
+    self.assertEqual(datetime.datetime(2015, 8, 22, 15, 21, 45), desc.published)
+    self.assertEqual(b'0x02225522 Frenn vun der Enn (FVDE) <info AT enn DOT lu>', desc.contact)
     self.assertEqual(['1', '2'], desc.link_protocols)
     self.assertEqual(['1'], desc.circuit_protocols)
     self.assertEqual(False, desc.hibernating)
     self.assertEqual(False, desc.allow_single_hop_exits)
     self.assertEqual(False, desc.extra_info_cache)
-    self.assertEqual('0879DB7B765218D7B3AE7557669D20307BB21CAA', desc.extra_info_digest)
+    self.assertEqual('44E9B679AF0B4EB09296985BAF4066AE9CA5BB93', desc.extra_info_digest)
     self.assertEqual(['2'], desc.hidden_service_dir)
-    self.assertEqual(set(), desc.family)
-    self.assertEqual(1073741824, desc.average_bandwidth)
-    self.assertEqual(1073741824, desc.burst_bandwidth)
-    self.assertEqual(9506816, desc.observed_bandwidth)
-    self.assertEqual(stem.exit_policy.ExitPolicy('reject *:*'), desc.exit_policy)
-    self.assertTrue('MIGJAoGBALbTpn' in desc.onion_key)
-    self.assertTrue('MIGJAoGBALDSt2' in desc.signing_key)
-    self.assertTrue('mSkveaqx79vzX' in desc.signature)
-    self.assertEqual(4, len(desc.get_unrecognized_lines()))
-    self.assertEqual('B0445BC590F004B8FD3BE922EB19EC490DBA9077', desc.digest())
+    self.assertEqual(family, desc.family)
+    self.assertEqual(149715200, desc.average_bandwidth)
+    self.assertEqual(1048576000, desc.burst_bandwidth)
+    self.assertEqual(51867731, desc.observed_bandwidth)
+    self.assertTrue(desc.exit_policy is not None)
+    self.assertEqual(stem.exit_policy.MicroExitPolicy('reject 25,465,587,10000,14464'), desc.exit_policy_v6)
+    self.assertTrue('MIGJAoGBAKpPOe' in desc.onion_key)
+    self.assertTrue('iW8BqwH5VKqZai' in desc.onion_key_crosscert)
+    self.assertTrue('AQoABhtwAWemtV' in desc.ntor_onion_key_crosscert)
+    self.assertEqual('0', desc.ntor_onion_key_crosscert_sign)
+    self.assertTrue('MIGJAoGBAOUS7x' in desc.signing_key)
+    self.assertTrue('y72z1dZOYxVQVL' in desc.signature)
+    self.assertEqual([], desc.get_unrecognized_lines())
+    self.assertEqual('B5E441051D139CCD84BC765D130B01E44DAC29AD', desc.digest())
 
   def test_cr_in_contact_line(self):
     """



More information about the tor-commits mailing list