Ticket #28912: reproduce.py

File reproduce.py, 7.2 KB (added by plcp`, 5 months ago)
Line 
1import cryptography
2import hashlib
3import socket
4import ssl
5
6from cryptography.hazmat.backends import default_backend
7from cryptography.hazmat.primitives.ciphers import algorithms, modes, Cipher
8
9import stem
10import stem.client.cell
11import stem.client.datatype
12
13'''Using identity, we get 58 RELAY_DATA cells before hanging, 17 with deflate.
14
15    Affected:
16        Tor version 0.3.4.0-alpha-dev (git-3463b4e0652bacca).
17
18    Not affected:
19        Tor version 0.3.3.5-rc-dev (git-3ee4c9b1fae9d535).
20'''
21
22compression = b'identity' # or b'deflate'
23
24'''Issue reproduced with the code below, originally bisected on [1].
25
26        From tag:tor-0.3.4.1-alpha                      Affected?
27
28        deb8970a29ef7427c3d42182d3bacc31ab602c03        yes
29        2d7b5c6fe5dc46b7e7cd040e6723e25d12015985        yes
30        3fa66f97996c179388fa91176b9a82fb9b5b31d8        no
31        306563ac68250872791350bda1ba7a7acff5eb63        no
32        3ee4c9b1fae9d53556b3e3be852f12e9abe51e14        no
33        c32108ee0fea851ced14f71d842390992f762393        yes
34        22845df2a7503ed73ed325c3a98916f289918caa        no
35        c7d3de216c60c090fddb4926a739da038bb5d5fe        yes
36        9ef4c05df8323850b5894782f435da15810d6189        no
37        5e0fbd7006993a4e402f2eee49f6f86074923197        no
38        c5899d5cf3a761f4049c1d6f05232731edcfeb57        no
39        3463b4e0652bacca51fecd2c256e3e9d61ce920e        yes
40
41    [1] Unpublished (yet) python client, no link here, sorry :(
42
43    Usage:
44        virtualenv venv
45        source venv/bin/activate
46        pip install -r stem cryptography
47        tor PublishServerDescriptor 0 AssumeReachable 1 ExitRelay 0 ProtocolWarnings 1 SafeLogging 0 LogTimeGranularity 1 PidFile '$(mktemp)' SOCKSPort 0 ContactInfo none@example.com DataDirectory '$(mktemp -d)' ORPort 9050 DirPort 9051 Log 'err stderr' &
48
49        wget <url>
50        python stem_26060_issue.py
51'''
52
53host = '127.0.0.1'
54port = 9050
55version = 5
56header_size = 5
57
58def _recv_all(peer, size):
59    buf = b''
60    while len(buf) < size:
61        buf += peer.recv(size - len(buf))
62    return buf
63
64def recv_cell(peer, version=version):
65    hsize = header_size
66    if version < 4:
67        hsize = 3
68
69    header = _recv_all(peer, hsize)
70    length = 509
71
72    # Variable size
73    if bool(header[-1] & 0x80) or header[-1] == 7:
74        header += _recv_all(peer, 2)
75        length = int.from_bytes(header[-2:], byteorder='big')
76
77    return header + _recv_all(peer, length)
78
79def init_state(key_material):
80    # Build the running sums
81    fw_digest = hashlib.sha1(key_material.forward_digest)
82    bk_digest = hashlib.sha1(key_material.backward_digest)
83
84    # Build the forward encryptor
85    zeroed_ctr = modes.CTR(b'\x00' * (algorithms.AES.block_size // 8))
86    fw_encrypt = Cipher(algorithms.AES(key_material.forward_key), zeroed_ctr,
87        default_backend()).encryptor()
88    bk_decrypt = Cipher(algorithms.AES(key_material.backward_key), zeroed_ctr,
89        default_backend()).decryptor()
90
91    return (fw_digest, fw_encrypt, bk_digest, bk_decrypt)
92
93def relay_cell(state, circ_id, command, data=b'', stream_id=0):
94    fw_digest, fw_encrypt, bk_digest, bk_decrypt = state
95
96    # Build the cell
97    raw_cell = stem.client.cell.RelayCell(
98            circ_id=circ_id,
99            command=command,
100            data=data,
101            digest=0,
102            stream_id=stream_id,
103            recognized=0)
104
105    # Compute forward digest (with a zeroed digest field)
106    fw_digest.update(raw_cell.pack(version)[header_size:])
107
108    # Pack the cell afterwards (with an updated digest field)
109    raw_cell.digest = int.from_bytes(fw_digest.digest()[:4], byteorder='big')
110    raw_data = raw_cell.pack(version)
111
112    # Encrypt the cell
113    ciphertext = fw_encrypt.update(raw_data[header_size:])
114
115    # Return (the state and) the final cell
116    payload = raw_data[:header_size] + ciphertext
117    return (fw_digest, fw_encrypt, bk_digest, bk_decrypt), payload
118
119def relay_recv(state, peer, circ_id, stream_id):
120    fw_digest, fw_encrypt, bk_digest, bk_decrypt = state
121
122    # Retrieve a cell
123    raw_data = recv_cell(peer)
124
125    # Validate the cell header
126    head = raw_data[:header_size]
127    assert head[-1] == 3 # (relay_cell)
128    assert circ_id == int.from_bytes(head[:4], byteorder='big')
129
130    # Validate the relay header
131    tail = bk_decrypt.update(raw_data[header_size:])
132    assert 0 == sum(tail[1:3]) # (recognized)
133    assert stream_id == int.from_bytes(tail[3:5], byteorder='big')
134
135    # Validate the digest
136    digest = tail[5:9]
137    bk_digest.update(tail[:5] + b'\x00' * 4 + tail[9:])
138    assert digest == bk_digest.digest()[:4]
139
140    # Finalize
141    cell, _ = stem.client.cell.Cell.pop(head + tail, version)
142    return (fw_digest, fw_encrypt, bk_digest, bk_decrypt), cell
143
144if __name__ == '__main__':
145    # Setup context
146    peer = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
147    ctxt = ssl.SSLContext(ssl.PROTOCOL_TLS)
148
149    # https://trac.torproject.org/projects/tor/ticket/28616
150    if '1.1.1a' in ssl.OPENSSL_VERSION:
151        ctxt.options |= ssl.OP_NO_TLSv1_3
152
153    # Establish connection
154    peer = ctxt.wrap_socket(peer)
155    peer.connect((host, port))
156
157    # Establish v5 link
158    peer.sendall(stem.client.cell.VersionsCell([version]).pack(2))
159    recv_cell(peer, version=2)
160
161    # (sendall NETINFO)
162    address = stem.client.datatype.Address(host)
163    netinfo = stem.client.cell.NetinfoCell(address, [])
164    peer.sendall(netinfo.pack(version))
165
166    # (skip cells)
167    certs, _ = stem.client.cell.Cell.pop(recv_cell(peer), version)
168    auth, _ = stem.client.cell.Cell.pop(recv_cell(peer), version)
169    netinfo, _ = stem.client.cell.Cell.pop(recv_cell(peer), version)
170
171    # Create a new circuit
172    circ_id = 0x80000000
173    create = stem.client.cell.CreateFastCell(circ_id)
174
175    # (retrieve OR's answer)
176    peer.sendall(create.pack(version))
177    created, _ = stem.client.cell.Cell.pop(recv_cell(peer), version)
178
179    # (retrieve derived key_material)
180    key_material = stem.client.datatype.KDF.from_value(
181        create.key_material + created.key_material)
182    assert key_material.key_hash == created.derivative_key
183
184    # (build the forward crypto state)
185    state = init_state(key_material)
186
187    # Create a new stream
188    state, cell = relay_cell(state, circ_id, 'RELAY_BEGIN_DIR', stream_id=1)
189    peer.sendall(cell)
190
191    # (skip cell)
192    state, cell = relay_recv(state, peer, circ_id, stream_id=1)
193    assert cell.command == 'RELAY_CONNECTED'
194
195    # Query directory
196    data = b'\r\n'.join((b'GET /tor/status-vote/current/consensus HTTP/1.0',
197        b'Accept-Encoding: ' + compression)) + b'\r\n\r\n'
198    state, cell = relay_cell(state, circ_id, 'RELAY_DATA', data, stream_id=1)
199    peer.sendall(cell)
200
201    # Main loop (expect 1000+ cells, get 18 (deflate) or 58 (identity)
202    count = 0
203    while count == 0 or cell.command != 'RELAY_END':
204        state, cell = relay_recv(state, peer, circ_id, stream_id=1)
205        count += 1
206
207        print('{:4} - {}'.format(count, cell.command))
208
209        if count % 100 == 0:
210            print('sendme circuit')
211            state, sendme = relay_cell(state, circ_id, 'RELAY_SENDME',
212                                       stream_id=0)
213            peer.sendall(sendme)
214
215        if count % 50 == 0:
216            print('sendme stream')
217            state, sendme = relay_cell(state, circ_id, 'RELAY_SENDME',
218                                       stream_id=1)
219            peer.sendall(sendme)