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