Browse Source

Revise makedesc.py: teach it how to emit ed signatures and crosscerts

Also, add a trivial ed25519-signed routerinfo to the tests.
Nick Mathewson 9 years ago
parent
commit
b600b68b20
3 changed files with 299 additions and 102 deletions
  1. 249 100
      scripts/codegen/makedesc.py
  2. 46 0
      src/test/failing_routerdescs.inc
  3. 4 2
      src/test/test_dir.c

+ 249 - 100
scripts/codegen/makedesc.py

@@ -14,6 +14,17 @@ import binascii
 import ctypes
 import ctypes.util
 import hashlib
+import optparse
+import os
+import struct
+import time
+import UserDict
+
+import slow_ed25519
+import slownacl_curve25519
+import ed25519_exts_ref
+
+# Pull in the openssl stuff we need.
 
 crypt = ctypes.CDLL(ctypes.util.find_library('crypto'))
 BIO_s_mem = crypt.BIO_s_mem
@@ -24,6 +35,15 @@ BIO_new = crypt.BIO_new
 BIO_new.argtypes = [ctypes.c_void_p]
 BIO_new.restype = ctypes.c_void_p
 
+crypt.BIO_free.argtypes = [ctypes.c_void_p]
+crypt.BIO_free.restype = ctypes.c_int
+
+crypt.BIO_ctrl.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_long, ctypes.c_void_p ]
+crypt.BIO_ctrl.restype = ctypes.c_long
+
+crypt.PEM_write_bio_RSAPublicKey.argtypes = [ ctypes.c_void_p, ctypes.c_void_p ]
+crypt.PEM_write_bio_RSAPublicKey.restype = ctypes.c_int
+
 RSA_generate_key = crypt.RSA_generate_key
 RSA_generate_key.argtypes = [ctypes.c_int, ctypes.c_ulong, ctypes.c_void_p, ctypes.c_void_p]
 RSA_generate_key.restype = ctypes.c_void_p
@@ -39,6 +59,14 @@ i2d_RSAPublicKey.argtypes = [
 ]
 i2d_RSAPublicKey.restype = ctypes.c_int
 
+
+def rsa_sign(msg, rsa):
+    buf = ctypes.create_string_buffer(1024)
+    n = RSA_private_encrypt(len(msg), msg, buf, rsa, 1)
+    if n <= 0:
+        raise Exception()
+    return buf.raw[:n]
+
 def b64(x):
     x = base64.b64encode(x)
     res = []
@@ -51,29 +79,188 @@ def bio_extract(bio):
     length = crypt.BIO_ctrl(bio, 3, 0, ctypes.byref(buf))
     return ctypes.string_at(buf, length)
 
-def make_key(e=65537):
+def make_rsa_key(e=65537):
     rsa = crypt.RSA_generate_key(1024, e, None, None)
     bio = BIO_new(BIO_s_mem())
     crypt.PEM_write_bio_RSAPublicKey(bio, rsa)
     pem = bio_extract(bio).rstrip()
     crypt.BIO_free(bio)
-
     buf = ctypes.create_string_buffer(1024)
     pBuf = ctypes.c_char_p(ctypes.addressof(buf))
     n = crypt.i2d_RSAPublicKey(rsa, ctypes.byref(pBuf))
     s = buf.raw[:n]
     digest = hashlib.sha1(s).digest()
-
     return (rsa,pem,digest)
 
+def makeEdSigningKeyCert(sk_master, pk_master, pk_signing, date,
+                         includeSigning=False, certType=1):
+    assert len(pk_signing) == len(pk_master) == 32
+    expiration = struct.pack("!L", date//3600)
+    if includeSigning:
+        extensions = "\x01\x00\x20\x04\x00%s"%(pk_master)
+    else:
+        extensions = "\x00"
+    signed = "\x01%s%s\x01%s%s" % (
+        chr(certType), expiration, pk_signing, extensions)
+    signature = ed25519_exts_ref.signatureWithESK(signed, sk_master, pk_master)
+    assert len(signature) == 64
+    return signed+signature
+
+def objwrap(identifier, body):
+    return ("-----BEGIN {0}-----\n"
+            "{1}"
+            "-----END {0}-----").format(identifier, body)
+
+MAGIC1 = "<<<<<<MAGIC>>>>>>"
+MAGIC2 = "<<<<<!#!#!#XYZZY#!#!#!>>>>>"
+
+class OnDemandKeys(object):
+    def __init__(self, certDate=None):
+        if certDate is None:
+            certDate = time.time() + 86400
+        self.certDate = certDate
+        self.rsa_id = None
+        self.rsa_onion_key = None
+        self.ed_id_sk = None
+        self.ntor_sk = None
+        self.ntor_crosscert = None
+        self.rsa_crosscert_ed = None
+        self.rsa_crosscert_noed = None
+
+    @property
+    def RSA_IDENTITY(self):
+        if self.rsa_id is None:
+            self.rsa_id, self.rsa_ident_pem, self.rsa_id_digest = make_rsa_key()
+
+        return self.rsa_ident_pem
+
+    @property
+    def RSA_ID_DIGEST(self):
+        self.RSA_IDENTITY
+        return self.rsa_id_digest
+
+    @property
+    def RSA_FINGERPRINT_NOSPACE(self):
+        return binascii.b2a_hex(self.RSA_ID_DIGEST).upper()
+
+    @property
+    def RSA_ONION_KEY(self):
+        if self.rsa_onion_key is None:
+            self.rsa_onion_key, self.rsa_onion_pem, _ = make_rsa_key()
+
+        return self.rsa_onion_pem
+
+    @property
+    def RSA_FINGERPRINT(self):
+        hexdigest = self.RSA_FINGERPRINT_NOSPACEK
+        return " ".join(hexdigest[i:i+4] for i in range(0,len(hexdigest),4))
+
+    @property
+    def RSA_SIGNATURE(self):
+        return MAGIC1
+
+    @property
+    def ED_SIGNATURE(self):
+        return MAGIC2
+
+    @property
+    def NTOR_ONION_KEY(self):
+        if self.ntor_sk is None:
+            self.ntor_sk = slownacl_curve25519.Private()
+            self.ntor_pk = self.ntor_sk.get_public()
+        return base64.b64encode(self.ntor_pk.serialize())
+
+    @property
+    def ED_CERT(self):
+        if self.ed_id_sk is None:
+            self.ed_id_sk = ed25519_exts_ref.expandSK(os.urandom(32))
+            self.ed_signing_sk = ed25519_exts_ref.expandSK(os.urandom(32))
+            self.ed_id_pk = ed25519_exts_ref.publickeyFromESK(self.ed_id_sk)
+            self.ed_signing_pk = ed25519_exts_ref.publickeyFromESK(self.ed_signing_sk)
+            self.ed_cert = makeEdSigningKeyCert(self.ed_id_sk, self.ed_id_pk, self.ed_signing_pk, self.certDate, includeSigning=True, certType=4)
+
+        return objwrap('ED25519 CERT', b64(self.ed_cert))
+
+    @property
+    def NTOR_CROSSCERT(self):
+        if self.ntor_crosscert is None:
+            self.ED_CERT
+            self.NTOR_ONION_KEY
+
+            ed_privkey = self.ntor_sk.serialize() + os.urandom(32)
+            ed_pub0 = ed25519_exts_ref.publickeyFromESK(ed_privkey)
+            sign = (ord(ed_pub0[31]) & 255) >> 7
+
+            self.ntor_crosscert = makeEdSigningKeyCert(self.ntor_sk.serialize() + os.urandom(32), ed_pub0, self.ed_id_pk, self.certDate, certType=10)
+            self.ntor_crosscert_sign = sign
+
+        return objwrap('ED25519 CERT', b64(self.ntor_crosscert))
+
+    @property
+    def NTOR_CROSSCERT_SIGN(self):
+        self.NTOR_CROSSCERT
+        return self.ntor_crosscert_sign
+
+    @property
+    def RSA_CROSSCERT_NOED(self):
+        if self.rsa_crosscert_noed is None:
+            self.RSA_ONION_KEY
+            signed = self.RSA_ID_DIGEST
+            self.rsa_crosscert_noed = rsa_sign(signed, self.rsa_onion_key)
+        return objwrap("CROSSCERT",b64(self.rsa_crosscert_noed))
+
+    @property
+    def RSA_CROSSCERT_ED(self):
+        if self.rsa_crosscert_ed is None:
+            self.RSA_ONION_KEY
+            self.ED_CERT
+            signed = self.RSA_ID_DIGEST + self.ed_id_pk
+            self.rsa_crosscert_ed = rsa_sign(signed, self.rsa_onion_key)
+        return objwrap("CROSSCERT",b64(self.rsa_crosscert_ed))
+
+    def sign_desc(self, body):
+        idx = body.rfind("\nrouter-sig-ed25519 ")
+        if idx >= 0:
+            self.ED_CERT
+            signed_part = body[:idx+len("\nrouter-sig-ed25519 ")]
+            signed_part = "Tor router descriptor signature v1" + signed_part
+            digest = hashlib.sha256(signed_part).digest()
+            ed_sig = ed25519_exts_ref.signatureWithESK(digest,
+                                      self.ed_signing_sk, self.ed_signing_pk)
+
+            body = body.replace(MAGIC2, base64.b64encode(ed_sig).replace("=",""))
+
+        idx = body.rindex("\nrouter-signature")
+        end_of_sig = body.index("\n", idx+1)
+
+        signed_part = body[:end_of_sig+1]
+
+        digest = hashlib.sha1(signed_part).digest()
+        assert len(digest) == 20
+
+        rsasig = rsa_sign(digest, self.rsa_id)
+
+        body = body.replace(MAGIC1, objwrap("SIGNATURE", b64(rsasig)))
+
+        return body
+
+
 def signdesc(body, args_out=None):
     rsa, ident_pem, id_digest = make_key()
     _, onion_pem, _ = make_key()
 
+    need_ed = '{ED25519-CERT}' in body or '{ED25519-SIGNATURE}' in body
+    if need_ed:
+        sk_master = os.urandom(32)
+        sk_signing = os.urandom(32)
+        pk_master = slow_ed25519.pubkey(sk_master)
+        pk_signing = slow_ed25519.pubkey(sk_signing)
+
     hexdigest = binascii.b2a_hex(id_digest).upper()
     fingerprint = " ".join(hexdigest[i:i+4] for i in range(0,len(hexdigest),4))
 
     MAGIC = "<<<<<<MAGIC>>>>>>"
+    MORE_MAGIC = "<<<<<!#!#!#XYZZY#!#!#!>>>>>"
     args = {
         "RSA-IDENTITY" : ident_pem,
         "ONION-KEY" : onion_pem,
@@ -81,6 +268,11 @@ def signdesc(body, args_out=None):
         "FINGERPRINT-NOSPACE" : hexdigest,
         "RSA-SIGNATURE" : MAGIC
     }
+    if need_ed:
+        args['ED25519-CERT'] = makeEdSigningKeyCert(
+            sk_master, pk_master, pk_signing)
+        args['ED25519-SIGNATURE'] = MORE_MAGIC
+
     if args_out:
         args_out.update(args)
     body = body.format(**args)
@@ -104,115 +296,72 @@ def signdesc(body, args_out=None):
 
     return body.rstrip()
 
-def emit_ri(name, body, args_out=None):
-    print "const char %s[] ="%name
-    body = "\n".join(line.rstrip() for line in body.split("\n"))+"\n"
-    b = signdesc(body, args_out)
-    for line in b.split("\n"):
-        print '  "%s\\n"'%line
+def print_c_string(ident, body):
+    print "static const char EX %s[] =" % ident
+    for line in body.split("\n"):
+        print '  "%s\\n"' %(line)
     print "  ;"
 
+def emit_ri(name, body):
+    info = OnDemandKeys()
+    body = body.format(d=info)
+    body = info.sign_desc(body)
+    print_c_string("EX_RI_%s"%name.upper(), body)
+
 def emit_ei(name, body):
-    args = { 'NAME' : name }
-    emit_ri(name, body, args)
-    args['key'] = "\n".join(
-        '  "%s\\n"'%line for line in args['RSA-IDENTITY'].split("\n"))
-    print """
-const char {NAME}_fp[] = "{FINGERPRINT-NOSPACE}";
-const char {NAME}_key[] =
-{key};""".format(**args)
+    info = OnDemandKeys()
+    body = body.format(d=info)
+    body = info.sign_desc(body)
+    print_c_string("EX_EI_%s"%name.upper(), body)
 
-if 0:
-    emit_ri("minimal",
-     """\
-router fred 127.0.0.1 9001 0 9002
-signing-key
-{RSA-IDENTITY}
-onion-key
-{ONION-KEY}
-published 2014-10-05 12:00:00
-bandwidth 1000 1000 1000
-reject *:*
-router-signature
-{RSA-SIGNATURE}
-""")
+    print 'const char {NAME}_FP[] = "{d.RSA_FINGERPRINT_NOSPACE}";'.format(
+        d=info, NAME=name.upper())
+    prnit_c_string("%s_KEY"%name.upper(), d.RSA_IDENTITY)
+
+def analyze(s):
+    fields = {}
+    while s.startswith(":::"):
+        first,s=s.split("\n", 1)
+        m = re.match(r'^:::(\w+)=(.*)',first)
+        if not m:
+            raise ValueError(first)
+        k,v = m.groups()
+        fields[k] = v
+    return fields, s
+
+def process_file(s):
+    try:
+        name = fields['name']
+        tp = fields['type']
+    except KeyError:
+        raise ValueError("missing required field")
+
+    if tp == 'ei':
+        emit_ei(name, s)
+    elif tp == 'ri':
+        emit_ri(name, s)
+    else:
+        raise ValueError("unrecognized type")
 
 if 0:
-    emit_ri("maximal",
+    emit_ri("minimal_ed",
      """\
 router fred 127.0.0.1 9001 0 9002
+identity-ed25519
+{d.ED_CERT}
 signing-key
-{RSA-IDENTITY}
+{d.RSA_IDENTITY}
 onion-key
-{ONION-KEY}
+{d.RSA_ONION_KEY}
+ntor-onion-key {d.NTOR_ONION_KEY}
+ntor-onion-key-crosscert {d.NTOR_CROSSCERT_SIGN}
+{d.NTOR_CROSSCERT}
+onion-key-crosscert
+{d.RSA_CROSSCERT_ED}
 published 2014-10-05 12:00:00
 bandwidth 1000 1000 1000
-reject 127.0.0.1:*
-accept *:80
 reject *:*
-ipv6-policy accept 80,100,101
-ntor-onion-key s7rSohmz9SXn8WWh1EefTHIsWePthsEntQi0WL+ScVw
-uptime 1000
-hibernating 0
-unrecognized-keywords are just dandy in this format
-platform Tor 0.2.4.23 on a Banana PC Jr 6000 Series
-contact O.W.Jones
-fingerprint {FINGERPRINT}
-read-history 900 1,2,3,4
-write-history 900 1,2,3,4
-extra-info-digest AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-hidden-service-dir
-allow-single-hop-exits
-family $AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA $BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
-caches-extra-info
-or-address [::1:2:3:4]:9999
-or-address 127.0.0.99:10000
-opt fred is a fine router
-router-signature
-{RSA-SIGNATURE}
-""")
-
-if 0:
-    emit_ei("maximal",
-"""\
-extra-info bob {FINGERPRINT-NOSPACE}
-published 2014-10-05 20:07:00
-opt foobarbaz
-read-history 900 1,2,3
-write-history 900 1,2,3
-dirreq-v2-ips 1
-dirreq-v3-ips 100
-dirreq-v3-reqs blahblah
-dirreq-v2-share blahblah
-dirreq-v3-share blahblah
-dirreq-v2-resp djfkdj
-dirreq-v3-resp djfkdj
-dirreq-v2-direct-dl djfkdj
-dirreq-v3-direct-dl djfkdj
-dirreq-v2-tunneled-dl djfkdj
-dirreq-v3-tunneled-dl djfkdj
-dirreq-stats-end foobar
-entry-ips jfsdfds
-entry-stats-end ksdflkjfdkf
-cell-stats-end FOO
-cell-processed-cells FOO
-cell-queued-cells FOO
-cell-time-in-queue FOO
-cell-circuits-per-decile FOO
-exit-stats-end FOO
-exit-kibibytes-written FOO
-exit-kibibytes-read FOO
-exit-streams-opened FOO
-router-signature
-{RSA-SIGNATURE}
-""")
-
-if 0:
-    emit_ei("minimal",
-"""\
-extra-info bob {FINGERPRINT-NOSPACE}
-published 2014-10-05 20:07:00
+router-sig-ed25519 {d.ED_SIGNATURE}
 router-signature
-{RSA-SIGNATURE}
+{d.RSA_SIGNATURE}
 """)
-

+ 46 - 0
src/test/failing_routerdescs.inc

@@ -666,3 +666,49 @@ static const char EX_RI_ZERO_ORPORT[] =
   "wgFKhHI/49NHyWHX5IMQpeicg0T7Qa6qwnUvspH62p8=\n"
   "-----END SIGNATURE-----\n"
   ;
+
+static const char EX_RI_MINIMAL_ED[] =
+  "router fred 127.0.0.1 9001 0 9002\n"
+  "identity-ed25519\n"
+  "-----BEGIN ED25519 CERT-----\n"
+  "AQQABf5iAa+2yD5ryD5kXaWbpmzaTyuTjRfjMTFleDuFGkHe26wrAQAgBABFTAHm\n"
+  "hdZriC+6BRCCMYu48cYc9tUN1adfEROqSHZN3HHP4k/fYgncoxrS3OYDX1x8Ysm/\n"
+  "sqxAXBY4NhCMswWvuDYgtQpro9YaFohiorJkHjyLQXjUeZikCfDrlxyR8AM=\n"
+  "-----END ED25519 CERT-----\n"
+  "signing-key\n"
+  "-----BEGIN RSA PUBLIC KEY-----\n"
+  "MIGJAoGBAOsjlHgM/lPQgjJyfrq0y+cR+iipcAeS2HAU8CK9SATETOTZYrxoL5vH\n"
+  "1BNteT+JxAxpjva+j7r7XZV41xPDx7alVr8G3zQsjqkAt5NnleTfUREUbg0+OSMV\n"
+  "10gU+DgcZJTMehfGYJnuJsF4eQHio/ZTdJLaZML7qwq0iWg3sZfBAgMBAAE=\n"
+  "-----END RSA PUBLIC KEY-----\n"
+  "onion-key\n"
+  "-----BEGIN RSA PUBLIC KEY-----\n"
+  "MIGJAoGBAK9NjRY7GtAZnlxrAZlImChXmGzml0uk2KlCugvju+eIsjSA/zW3LuqW\n"
+  "wqp7Kh488Ak5nUFSlCaV9GjAexT134pynst8P0m/ofrejwlzl5DHd6sFbR33Fkzl\n"
+  "H48zic0QDY+8tKXI732dA4GveEwZDlxxy8sPcvUDaVyTsuZLHR4zAgMBAAE=\n"
+  "-----END RSA PUBLIC KEY-----\n"
+  "ntor-onion-key 71DgscFrk4i58O5GuTerI9g3JL0kz+6QaCstAllz9xw=\n"
+  "ntor-onion-key-crosscert 1\n"
+  "-----BEGIN ED25519 CERT-----\n"
+  "AQoABf5iAUVMAeaF1muIL7oFEIIxi7jxxhz21Q3Vp18RE6pIdk3cAH5ijeKqa+LM\n"
+  "T5Nb0I42Io4Z7BVjXG7sYVSxrospCOI4dqkl2ln3BKNuEFFT42xJwt+XGz3aMyK2\n"
+  "Cpp8w8I8nwU=\n"
+  "-----END ED25519 CERT-----\n"
+  "onion-key-crosscert\n"
+  "-----BEGIN CROSSCERT-----\n"
+  "lAZwD6YVic61NvJ0Iy62cSPuzJl5hJOFYNh9iSG/vn4/lVfnnCik+Gqi2v9pwItC\n"
+  "acwmutCSrMprmmFAW1dgzoU7GzUtdbxaGaOJdg8WwtO4JjFSzScTDB8R6sp0SCAI\n"
+  "PdbzAzJyiMqYcynyyCTiL77iwhUOBPzs2fXlivMtW2E=\n"
+  "-----END CROSSCERT-----\n"
+  "published 2014-10-05 12:00:00\n"
+  "bandwidth 1000 1000 1000\n"
+  "reject *:*\n"
+  "router-sig-ed25519 Oyo/eES+/wsgse1f+YSiJDGatBDaiB4fASf7vJ7GxFeD4OfLbB7OYa4hYNEo5NBssNt/PA55AQVSL8hvzBE3Cg\n"
+  "router-signature\n"
+  "-----BEGIN SIGNATURE-----\n"
+  "wdk26ZtS1H81IxcUThyirANLoszrnYYhOMP57YRAUDEzUr88X6yNDZ5S0tLl+FoT\n"
+  "9XlEVrpN7Z3k4N9WloWb0o/zVVidPMRVwt8YQakSgR8axzMQg6QhQ6zXTiYhiXa4\n"
+  "mawlwYFXsaVDSIIqYA2CudIyF3UBRZuTbw0CFZElMWc=\n"
+  "-----END SIGNATURE-----\n"
+  "\n"
+  ;

+ 4 - 2
src/test/test_dir.c

@@ -352,7 +352,7 @@ test_dir_formats(void *arg)
 #include "failing_routerdescs.inc"
 
 static void
-test_dir_routerparse_bad(void *arg)
+test_dir_routerinfo_parsing(void *arg)
 {
   (void) arg;
 
@@ -377,6 +377,8 @@ test_dir_routerparse_bad(void *arg)
   CHECK_OK(EX_RI_MINIMAL);
   CHECK_OK(EX_RI_MAXIMAL);
 
+  CHECK_OK(EX_RI_MINIMAL_ED);
+
   /* good annotations prepended */
   routerinfo_free(ri);
   ri = router_parse_entry_from_string(EX_RI_MINIMAL, NULL, 0, 0,
@@ -3168,7 +3170,7 @@ test_dir_packages(void *arg)
 struct testcase_t dir_tests[] = {
   DIR_LEGACY(nicknames),
   DIR_LEGACY(formats),
-  DIR(routerparse_bad, 0),
+  DIR(routerinfo_parsing, 0),
   DIR(extrainfo_parsing, 0),
   DIR(parse_router_list, TT_FORK),
   DIR(load_routers, TT_FORK),