1# -*- coding: utf-8 -*- 2import base64 3import textwrap 4import datetime 5 6import Cryptodome.Signature.pkcs1_15 7import Cryptodome.Hash.SHA 8 9import stem.descriptor.hidden_service_descriptor 10 11from onionbalance.hs_v2 import util 12 13from onionbalance.common import log 14from onionbalance.hs_v2 import config 15 16logger = log.get_logger() 17 18 19def generate_service_descriptor(permanent_key, introduction_point_list=None, 20 replica=0, timestamp=None, deviation=0): 21 """ 22 High-level interface for generating a signed HS descriptor 23 """ 24 25 if not timestamp: 26 timestamp = datetime.datetime.utcnow() 27 unix_timestamp = int(timestamp.strftime("%s")) 28 29 permanent_key_block = make_public_key_block(permanent_key) 30 permanent_id = util.calc_permanent_id(permanent_key) 31 32 # Calculate the current secret-id-part for this hidden service 33 # Deviation allows the generation of a descriptor for a different time 34 # period. 35 time_period = (util.get_time_period(unix_timestamp, permanent_id) + int(deviation)) 36 37 secret_id_part = util.calc_secret_id_part(time_period, None, replica) 38 descriptor_id = util.calc_descriptor_id(permanent_id, secret_id_part) 39 40 if not introduction_point_list: 41 onion_address = util.calc_onion_address(permanent_key) 42 raise ValueError("No introduction points for service %s.onion." % 43 onion_address) 44 45 # Generate the introduction point section of the descriptor 46 intro_section = make_introduction_points_part( 47 introduction_point_list 48 ) 49 50 unsigned_descriptor = generate_hs_descriptor_raw( 51 desc_id_base32=util.base32_encode_str(descriptor_id), 52 permanent_key_block=permanent_key_block, 53 secret_id_part_base32=util.base32_encode_str(secret_id_part), 54 publication_time=util.rounded_timestamp(timestamp), 55 introduction_points_part=intro_section 56 ) 57 58 signed_descriptor = sign_descriptor(unsigned_descriptor, permanent_key) 59 return signed_descriptor 60 61 62def generate_hs_descriptor_raw(desc_id_base32, permanent_key_block, 63 secret_id_part_base32, publication_time, 64 introduction_points_part): 65 """ 66 Generate hidden service descriptor string 67 """ 68 doc = [ 69 "rendezvous-service-descriptor {}".format(desc_id_base32), 70 "version 2", 71 "permanent-key", 72 permanent_key_block, 73 "secret-id-part {}".format(secret_id_part_base32), 74 "publication-time {}".format(publication_time), 75 "protocol-versions 2,3", 76 "introduction-points", 77 introduction_points_part, 78 "signature\n", 79 ] 80 81 unsigned_descriptor = '\n'.join(doc) 82 return unsigned_descriptor 83 84 85def make_introduction_points_part(introduction_point_list=None): 86 """ 87 Make introduction point block from list of IntroductionPoint objects 88 """ 89 90 # If no intro points were specified, we should create an empty list 91 if not introduction_point_list: 92 introduction_point_list = [] 93 94 intro = [] 95 for intro_point in introduction_point_list: 96 intro.append("introduction-point {}".format(intro_point.identifier)) 97 intro.append("ip-address {}".format(intro_point.address)) 98 intro.append("onion-port {}".format(intro_point.port)) 99 intro.append("onion-key") 100 intro.append(intro_point.onion_key) 101 intro.append("service-key") 102 intro.append(intro_point.service_key) 103 104 intro_section = '\n'.join(intro).encode('utf-8') 105 intro_section_base64 = base64.b64encode(intro_section).decode('utf-8') 106 intro_section_base64 = textwrap.fill(intro_section_base64, 64) 107 108 # Add the header and footer: 109 intro_points_with_headers = '\n'.join([ 110 '-----BEGIN MESSAGE-----', 111 intro_section_base64, 112 '-----END MESSAGE-----']) 113 return intro_points_with_headers 114 115 116def make_public_key_block(key): 117 """ 118 Get ASN.1 representation of public key, base64 and add headers 119 """ 120 asn1_pub = util.get_asn1_sequence(key) 121 pub_base64 = base64.b64encode(asn1_pub).decode('utf-8') 122 pub_base64 = textwrap.fill(pub_base64, 64) 123 124 # Add the header and footer: 125 pub_with_headers = '\n'.join([ 126 '-----BEGIN RSA PUBLIC KEY-----', 127 pub_base64, 128 '-----END RSA PUBLIC KEY-----']) 129 return pub_with_headers 130 131def pad_msg_with_tor_pkcs(msg_hash, emLen, with_hash_parameters=True): 132 """ 133 Tor requires PKCS#1 1.5 padding for descriptor signatures but does not 134 include the algorithmIdentifier as specified in RFC3447. 135 136 Unfortunately, most crypto libraries add that algorithmIdentifier by force 137 and hence we need this function for monkey patching. 138 """ 139 digestInfo = msg_hash.digest() 140 PS = b'\xFF' * (emLen - len(digestInfo) - 3) 141 return b'\x00\x01' + PS + b'\x00' + digestInfo 142 143def sign(body, private_key): 144 """ 145 Sign, base64 encode, wrap and add Tor signature headers 146 147 The message digest is PKCS1 padded without the optional 148 algorithmIdentifier section. 149 """ 150 # First monkey-patch cryptodome to do the Tor PKCS#1 padding 151 Cryptodome.Signature.pkcs1_15._EMSA_PKCS1_V1_5_ENCODE = pad_msg_with_tor_pkcs 152 153 # The RSA signing API requires a hasher as input 154 hasher = Cryptodome.Hash.SHA1.new(body) 155 # Now do the signing 156 signer = Cryptodome.Signature.pkcs1_15.new(private_key) 157 signature_bytes = signer.sign(hasher) 158 159 # Convert the signature to the base64 format that our descriptors like 160 signature_base64 = base64.b64encode(signature_bytes).decode('utf-8') 161 signature_base64 = textwrap.fill(signature_base64, 64) 162 163 # Add the header and footer: 164 signature_with_headers = '\n'.join([ 165 '-----BEGIN SIGNATURE-----', 166 signature_base64, 167 '-----END SIGNATURE-----']) 168 return signature_with_headers 169 170 171def sign_descriptor(descriptor, service_privkey): 172 """ 173 Sign or resign a provided hidden service descriptor 174 """ 175 token_descriptor_signature = '\nsignature\n' 176 177 # Remove signature block if it exists 178 if token_descriptor_signature in descriptor: 179 descriptor = descriptor[:descriptor.find(token_descriptor_signature) + len(token_descriptor_signature)] 180 else: 181 descriptor = descriptor.strip() + token_descriptor_signature 182 183 signature_with_headers = sign(descriptor.encode('utf-8'), service_privkey) 184 return descriptor + signature_with_headers 185 186def descriptor_received(descriptor_content): 187 """ 188 Process onion service descriptors retrieved from the HSDir system or 189 received directly over the metadata channel. 190 """ 191 192 try: 193 parsed_descriptor = stem.descriptor.hidden_service_descriptor.\ 194 HiddenServiceDescriptor(descriptor_content, validate=True) 195 except ValueError: 196 logger.exception("Received an invalid service descriptor.") 197 return None 198 199 # Ensure the received descriptor matches the requested descriptor 200 permanent_key = Cryptodome.PublicKey.RSA.importKey( 201 parsed_descriptor.permanent_key) 202 descriptor_onion_address = util.calc_onion_address(permanent_key) 203 204 known_descriptor, instance_changed = False, False 205 for instance in [instance for service in config.services for 206 instance in service.instances]: 207 if instance.onion_address == descriptor_onion_address: 208 instance_changed |= instance.update_descriptor(parsed_descriptor) 209 known_descriptor = True 210 211 if instance_changed: 212 logger.info("The introduction point set has changed for instance " 213 "%s.onion.", descriptor_onion_address) 214 215 if not known_descriptor: 216 # No matching service instance was found for the descriptor 217 logger.debug("Received a descriptor for an unknown service:\n%s", 218 descriptor_content.decode('utf-8')) 219 logger.warning("Received a descriptor with address %s.onion that " 220 "did not match any configured service instances.", 221 descriptor_onion_address) 222 223 return None 224