Ticket #11045: 0001-Refactoring-for-checking-consensus-signatures.patch

File 0001-Refactoring-for-checking-consensus-signatures.patch, 12.0 KB (added by NickHopper, 5 years ago)

Code that checks consensus signatures

  • stem/descriptor/__init__.py

    From 5d95f2fc69f2c8c93b9812f9e29c9a75c8565011 Mon Sep 17 00:00:00 2001
    From: Nick Hopper <hopper@cs.umn.edu>
    Date: Tue, 15 Jul 2014 07:08:14 -0500
    Subject: [PATCH] Refactoring for checking consensus signatures
    
    ---
     stem/descriptor/__init__.py          |   74 ++++++++++++++++++++++++++++++-
     stem/descriptor/networkstatus.py     |   81 ++++++++++++++++++++++++++++++++++
     stem/descriptor/server_descriptor.py |   67 +++-------------------------
     3 files changed, 159 insertions(+), 63 deletions(-)
    
    diff --git a/stem/descriptor/__init__.py b/stem/descriptor/__init__.py
    index 9fe7235..a8dfe60 100644
    a b __all__ = [ 
    4747  'router_status_entry',
    4848  'tordnsel',
    4949  'parse_file',
    50   'Descriptor',
     50  'Descriptor'
    5151]
    5252
    5353import os
    5454import re
    5555import tarfile
     56import base64
    5657
    5758import stem.prereq
    5859import stem.util.enum
    class Descriptor(object): 
    372373    else:
    373374      return self._raw_contents
    374375
     376  def sig_tokens(self):
     377    raise NotImplementedError()
     378
     379  def signed_portion(self):
     380    raw_doc = str(self)
     381    start_tok, end_tok = self.sig_tokens()
     382    begin = raw_doc.find(start_tok)
     383    sig_begin = raw_doc.find(end_tok)
     384    doc_end = sig_begin + len(end_tok)
     385    if begin < 0 or sig_begin < begin:
     386        raise ValueError('unable to extract signable network status document')
     387    return raw_doc[begin:doc_end]
     388
     389  def nsdoc_digest(self, digest_func):
     390    sbytes = stem.util.str_tools._to_bytes(self.signed_portion())
     391    return digest_func(sbytes).hexdigest().upper()
     392
     393
     394     
    375395
    376396def _get_bytes_field(keyword, content):
    377397  """
    def _get_descriptor_components(raw_contents, validate, extra_keywords = ()): 
    591611  else:
    592612    return entries
    593613
     614
     615def _get_bytes(block_content):
     616    base64_object = ''.join(block_content.split('\n')[1:-1])
     617    return base64.b64decode(stem.util.str_tools._to_bytes(base64_object))
     618
     619def check_signature(signed_digest, sigstr, keystr):
     620    from Crypto.Util.number import bytes_to_long, long_to_bytes
     621    from Crypto.Util import asn1
     622
     623    seq = asn1.DerSequence()
     624    seq.decode(_get_bytes(keystr))
     625    modulus, exponent = seq[0], seq[1]
     626
     627    sig_as_bytes = _get_bytes(sigstr)
     628    sig_as_long = bytes_to_long(sig_as_bytes)
     629    decrypted_int = pow(sig_as_long, exponent, modulus)
     630    #    blocksize = 128/256
     631    blocksize = len(long_to_bytes(modulus))
     632    # convert the int to a byte array.
     633    decrypted_bytes = long_to_bytes(decrypted_int, blocksize)
     634    ############################################################################
     635    ## The decrypted bytes should have a structure exactly along these lines.
     636    ## 1 byte  - [null '\x00']
     637    ## 1 byte  - [block type identifier '\x01'] - Should always be 1
     638    ## N bytes - [padding '\xFF' ]
     639    ## 1 byte  - [separator '\x00' ]
     640    ## M bytes - [message]
     641    ## Total   - 128 bytes
     642    ## More info here http://www.ietf.org/rfc/rfc2313.txt
     643    ##                esp the Notes in section 8.1
     644    ############################################################################
     645    try:
     646      if decrypted_bytes.index('\x00\x01') != 0:
     647        raise ValueError("Verification failed, identifier missing")
     648    except ValueError:
     649      raise ValueError("Verification failed, Malformed data")
     650    try:
     651      identifier_offset = 2
     652      # find the separator
     653      separator_index = decrypted_bytes.index('\x00', identifier_offset)
     654    except ValueError:
     655      raise ValueError("Verification failed, separator not found")
     656    decrypted_digest = decrypted_bytes[separator_index + 1:]
     657    # The local digest is stored in uppercase hex;
     658    #  - so decode it from hex
     659    #  - and convert it to lower case
     660    local_digest = signed_digest.lower().decode('hex')
     661    if decrypted_digest != local_digest:
     662      raise ValueError("Decrypted digest does not match local digest. expected " + repr(local_digest) + " and found " + repr(decrypted_digest))
     663    return True
     664
     665
    594666# importing at the end to avoid circular dependencies on our Descriptor class
    595667
    596668import stem.descriptor.server_descriptor
  • stem/descriptor/networkstatus.py

    diff --git a/stem/descriptor/networkstatus.py b/stem/descriptor/networkstatus.py
    index 5af9d02..a986f4f 100644
    a b import stem.descriptor.router_status_entry 
    5656import stem.util.str_tools
    5757import stem.util.tor_tools
    5858import stem.version
     59import hashlib
    5960
    6061from stem.descriptor import (
    6162  PGP_BLOCK_END,
    from stem.descriptor import ( 
    6364  DocumentHandler,
    6465  _get_descriptor_components,
    6566  _read_until_keywords,
     67  _get_bytes,
     68  check_signature
    6669)
    6770
    6871# Version 2 network status document fields, tuples of the form...
    class NetworkStatusDocument(Descriptor): 
    273276  def get_unrecognized_lines(self):
    274277    return list(self._unrecognized_lines)
    275278
     279  def sig_tokens(self):
     280    return ('network-status-version', '\ndirectory-signature ')
     281
    276282
    277283class NetworkStatusDocumentV2(NetworkStatusDocument):
    278284  """
    class NetworkStatusDocumentV3(NetworkStatusDocument): 
    591597  def __le__(self, other):
    592598    return self._compare(other, lambda s, o: s <= o)
    593599
     600  def verify_consensus(self, certs, n_required):
     601    # a consensus is verified if at least n_required authority id fingerprints have signed it
     602    # under certs that were valid (correctly signed and cross-certified and not expired) at the time of the consensus
     603    sigs = self.signatures
     604    signing_authorities = set()
     605    for sig in sigs:
     606        doc_digest = self.nsdoc_digest(sig.sig_digest_func())
     607        # find cert corresponding to authority       
     608        # check signature and cert validity
     609        for c in certs:
     610            if sig.key_digest == c.signing_key_digest() and sig.identity == c.fingerprint:
     611                try:
     612                    if c.check_certificate(self.fresh_until) and \
     613                      check_signature(doc_digest, sig.signature, c.signing_key):
     614                        signing_authorities.add(c.fingerprint)
     615                except ValueError:
     616                    pass # it's ok for one signature to fail validation as long as n_required don't
     617    if len(signing_authorities) >= n_required:
     618        return True
     619    else:
     620        raise ValueError('consensus not signed by enough distinct, known directory authorities')
     621
     622
     623
     624 
    594625
    595626class _DocumentHeader(object):
    596627  def __init__(self, document_file, validate, default_params):
    class KeyCertificate(Descriptor): 
    14151446  def __le__(self, other):
    14161447    return self._compare(other, lambda s, o: s <= o)
    14171448
     1449  # For extensibiility - certificates may use sha256 for some signatures in the future
     1450  idkey_digest_func = hashlib.sha1
     1451  skey_digest_func = hashlib.sha1
     1452  cert_digest_func = hashlib.sha1
     1453
     1454  def sig_tokens(self):
     1455    return ('dir-key-certificate-version', '\ndir-key-certification\n')
     1456
     1457  def signing_key_digest(self):
     1458    signing_key_bytes = _get_bytes(self.signing_key)
     1459    signing_key_digest = KeyCertificate.skey_digest_func(signing_key_bytes).hexdigest().upper()
     1460    return signing_key_digest
     1461
     1462  def check_certificate(self, check_date=None):
     1463    # A certificate is valid if:
     1464    # - the fingerprint matches the digest of the identity key
     1465    # - the cross-certification is a valid signature on the identity key with the signing key, and
     1466    # - the key-certification is a valid signature on the document with the identity key, and
     1467    # - optionally, if the check_date is between the published and expires dates
     1468    # check fingerprint
     1469    id_key_bytes = _get_bytes(self.identity_key)
     1470    id_fingerprint = KeyCertificate.idkey_digest_func(id_key_bytes).hexdigest().upper()
     1471    if id_fingerprint != self.fingerprint:
     1472        raise ValueError('Certificate identity key does not match supplied fingerprint')
     1473    # check cross-certification
     1474    if not check_signature(self.fingerprint, self.crosscert, self.signing_key):
     1475        raise ValueError('Invalid cross-certification of identity key with signing key')
     1476    # check key certification
     1477    cert_digest = KeyCertificate.cert_digest_func(self.signed_portion()).hexdigest().upper()
     1478    if not check_signature(cert_digest, self.certification, self.identity_key):
     1479        raise ValueError('Invalid signature on identity certificate')
     1480    # check dates
     1481    if check_date is not None:
     1482        if check_date > self.expires:
     1483            raise ValueError('identity certificate was expired on ' + str(check_date))
     1484        if check_date < self.published:
     1485            raise ValueError('identity certificate was not yet published on ' + str(check_date))   
     1486    # no checks failed, the certificate passes
     1487    return True
     1488     
     1489
     1490
    14181491
    14191492class DocumentSignature(object):
    14201493  """
    class DocumentSignature(object): 
    14551528
    14561529    return method(True, True)  # we're equal
    14571530
     1531  def sig_digest_func(self):
     1532    if self.method == 'sha1':
     1533      return hashlib.sha1
     1534    elif self.method == 'sha256':
     1535      return hashlib.sha256
     1536    else:
     1537      raise ValueError('unrecognized Algorithm field in Document Signature')
     1538
    14581539  def __eq__(self, other):
    14591540    return self._compare(other, lambda s, o: s == o)
    14601541
  • stem/descriptor/server_descriptor.py

    diff --git a/stem/descriptor/server_descriptor.py b/stem/descriptor/server_descriptor.py
    index 064b287..e5e074d 100644
    a b class RelayDescriptor(ServerDescriptor): 
    726726
    727727    if not stem.prereq.is_crypto_available():
    728728      return
    729 
    730     from Crypto.Util import asn1
    731     from Crypto.Util.number import bytes_to_long, long_to_bytes
    732 
    733     # get the ASN.1 sequence
    734 
    735     seq = asn1.DerSequence()
    736     seq.decode(key_as_der)
    737     modulus = seq[0]
    738     public_exponent = seq[1]  # should always be 65537
    739 
    740     sig_as_bytes = RelayDescriptor._get_key_bytes(self.signature)
    741 
    742     # convert the descriptor signature to an int
    743 
    744     sig_as_long = bytes_to_long(sig_as_bytes)
    745 
    746     # use the public exponent[e] & the modulus[n] to decrypt the int
    747 
    748     decrypted_int = pow(sig_as_long, public_exponent, modulus)
    749 
    750     # block size will always be 128 for a 1024 bit key
    751 
    752     blocksize = 128
    753 
    754     # convert the int to a byte array.
    755 
    756     decrypted_bytes = long_to_bytes(decrypted_int, blocksize)
    757 
    758     ############################################################################
    759     ## The decrypted bytes should have a structure exactly along these lines.
    760     ## 1 byte  - [null '\x00']
    761     ## 1 byte  - [block type identifier '\x01'] - Should always be 1
    762     ## N bytes - [padding '\xFF' ]
    763     ## 1 byte  - [separator '\x00' ]
    764     ## M bytes - [message]
    765     ## Total   - 128 bytes
    766     ## More info here http://www.ietf.org/rfc/rfc2313.txt
    767     ##                esp the Notes in section 8.1
    768     ############################################################################
    769 
    770     try:
    771       if decrypted_bytes.index(b'\x00\x01') != 0:
    772         raise ValueError('Verification failed, identifier missing')
    773     except ValueError:
    774       raise ValueError('Verification failed, malformed data')
    775 
    776     try:
    777       identifier_offset = 2
    778 
    779       # find the separator
    780       seperator_index = decrypted_bytes.index(b'\x00', identifier_offset)
    781     except ValueError:
    782       raise ValueError('Verification failed, seperator not found')
    783 
    784     digest_hex = codecs.encode(decrypted_bytes[seperator_index + 1:], 'hex_codec')
    785     digest = stem.util.str_tools._to_unicode(digest_hex.upper())
    786 
    787     local_digest = self.digest()
    788 
    789     if digest != local_digest:
    790       raise ValueError('Decrypted digest does not match local digest (calculated: %s, local: %s)' % (digest, local_digest))
     729   
     730    stem.descriptor.check_signature(self.digest(),self.signature,key_as_der)
    791731
    792732  def _parse(self, entries, validate):
    793733    entries = dict(entries)  # shallow copy since we're destructive
    class RelayDescriptor(ServerDescriptor): 
    821761        del entries['router-signature']
    822762
    823763    ServerDescriptor._parse(self, entries, validate)
     764   
     765  def sig_tokens(self):
     766    return ('router ','\nrouter-signature\n')
    824767
    825768  def _compare(self, other, method):
    826769    if not isinstance(other, RelayDescriptor):