Skip to content

Commit 99cf549

Browse files
committed
Add possibility to check the certificate fingerprint of Threema Gateway Server
Update CLI
1 parent 948f235 commit 99cf549

3 files changed

Lines changed: 48 additions & 20 deletions

File tree

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import socket
22
import asyncio
3-
import threading
43
import copy
54

65
import pytest

threema-gateway

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,18 @@ def aio_run(func):
2626

2727

2828
@click.group()
29-
def cli():
29+
@click.option('-vf', '--verify-fingerprint', is_flag=True,
30+
help='Verify the certificate fingerprint.')
31+
@click.option('--fingerprint', type=str, help='A hex-encoded fingerprint.')
32+
@click.pass_context
33+
def cli(ctx, verify_fingerprint, fingerprint):
3034
"""
3135
Command Line Interface. Use --help for details.
3236
"""
37+
ctx.obj = {
38+
'verify_fingerprint': verify_fingerprint,
39+
'fingerprint': fingerprint
40+
}
3341

3442

3543
@cli.command(short_help='Show version information.', help="""
@@ -157,13 +165,14 @@ Prints the message ID on success.
157165
@click.argument('to')
158166
@click.argument('from')
159167
@click.argument('secret')
168+
@click.pass_context
160169
@aio_run
161-
def send_simple(**arguments):
170+
def send_simple(ctx, **arguments):
162171
# Read message from stdin
163172
text = click.get_text_stream('stdin').read().strip()
164173

165174
# Create connection
166-
with Connection(arguments['from'], arguments['secret']) as connection:
175+
with Connection(arguments['from'], arguments['secret'], **ctx.obj) as connection:
167176
# Create message
168177
message = simple.TextMessage(
169178
connection=connection,
@@ -188,8 +197,9 @@ Prints the message ID on success.
188197
@click.option('-k', '--public-key', help="""
189198
The public key of the recipient. Will be fetched automatically if not provided.
190199
""")
200+
@click.pass_context
191201
@aio_run
192-
def send_e2e(**arguments):
202+
def send_e2e(ctx, **arguments):
193203
# Get key instances
194204
private_key = util.read_key_or_key_file(arguments['private_key'], Key.Type.private)
195205
if arguments['public_key'] is not None:
@@ -204,7 +214,8 @@ def send_e2e(**arguments):
204214
connection = Connection(
205215
id=arguments['from'],
206216
secret=arguments['secret'],
207-
key=private_key
217+
key=private_key,
218+
**ctx.obj
208219
)
209220

210221
with connection:
@@ -235,8 +246,9 @@ Prints the message ID on success.
235246
@click.option('-k', '--public-key', help="""
236247
The public key of the recipient. Will be fetched automatically if not provided.
237248
""")
249+
@click.pass_context
238250
@aio_run
239-
def send_image(**arguments):
251+
def send_image(ctx, **arguments):
240252
# Get key instances
241253
private_key = util.read_key_or_key_file(arguments['private_key'], Key.Type.private)
242254
if arguments['public_key'] is not None:
@@ -248,7 +260,8 @@ def send_image(**arguments):
248260
connection = Connection(
249261
id=arguments['from'],
250262
secret=arguments['secret'],
251-
key=private_key
263+
key=private_key,
264+
**ctx.obj
252265
)
253266

254267
with connection:
@@ -281,8 +294,9 @@ The public key of the recipient. Will be fetched automatically if not provided.
281294
@click.option('-t', '--thumbnail-path', help="""
282295
The relative or absolute path to a thumbnail.
283296
""")
297+
@click.pass_context
284298
@aio_run
285-
def send_file(**arguments):
299+
def send_file(ctx, **arguments):
286300
# Get key instances
287301
private_key = util.read_key_or_key_file(arguments['private_key'], Key.Type.private)
288302
if arguments['public_key'] is not None:
@@ -294,7 +308,8 @@ def send_file(**arguments):
294308
connection = Connection(
295309
id=arguments['from'],
296310
secret=arguments['secret'],
297-
key=private_key
311+
key=private_key,
312+
**ctx.obj
298313
)
299314

300315
with connection:
@@ -321,8 +336,9 @@ FROM is the API identity and SECRET is the API secret.
321336
@click.option('-e', '--email', help='An email address.')
322337
@click.option('-p', '--phone', help='A phone number in E.164 format.')
323338
@click.option('-i', '--id', help='A Threema ID.')
339+
@click.pass_context
324340
@aio_run
325-
def lookup(**arguments):
341+
def lookup(ctx, **arguments):
326342
modes = ['email', 'phone', 'id']
327343
mode = {key: value for key, value in arguments.items()
328344
if key in modes and value is not None}
@@ -333,7 +349,8 @@ def lookup(**arguments):
333349
raise click.ClickException(error)
334350

335351
# Create connection
336-
with Connection(arguments['from'], secret=arguments['secret']) as connection:
352+
connection = Connection(arguments['from'], secret=arguments['secret'], **ctx.obj)
353+
with connection:
337354
# Do lookup
338355
if 'id' in mode:
339356
public_key = yield from connection.get_public_key(arguments['id'])
@@ -350,10 +367,11 @@ Prints a set of capabilities in alphabetical order on success.
350367
@click.argument('from')
351368
@click.argument('secret')
352369
@click.argument('id')
370+
@click.pass_context
353371
@aio_run
354-
def capabilities(**arguments):
372+
def capabilities(ctx, **arguments):
355373
# Create connection
356-
with Connection(arguments['from'], arguments['secret']) as connection:
374+
with Connection(arguments['from'], arguments['secret'], **ctx.obj) as connection:
357375
# Lookup and format returned capabilities
358376
coroutine = connection.get_reception_capabilities(arguments['id'])
359377
capabilities_ = yield from coroutine
@@ -367,10 +385,11 @@ FROM is the API identity and SECRET is the API secret.
367385
""")
368386
@click.argument('from')
369387
@click.argument('secret')
388+
@click.pass_context
370389
@aio_run
371-
def credits(**arguments):
390+
def credits(ctx, **arguments):
372391
# Create connection
373-
with Connection(arguments['from'], arguments['secret']) as connection:
392+
with Connection(arguments['from'], arguments['secret'], **ctx.obj) as connection:
374393
# Get and print credits
375394
click.echo((yield from connection.get_credits()))
376395

threema/gateway/__init__.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import asyncio
2626

2727
import aiohttp
28-
2928
import libnacl.public
3029
import libnacl.encode
3130

@@ -36,7 +35,7 @@
3635

3736
__author__ = 'Lennart Grahl <lennart.grahl@threema.ch>'
3837
__status__ = 'Production'
39-
__version__ = '2.0.3'
38+
__version__ = '2.1.3'
4039
__all__ = (
4140
'feature_level',
4241
'ReceptionCapability',
@@ -76,7 +75,14 @@ class Connection:
7675
end-to-end mode.
7776
- `key_file`: A file where the private key is stored in. Can
7877
be used instead of passing the key directly.
78+
- `verify_fingerprint`: Set to `True` if you want to verify the
79+
TLS certificate of the Threema Gateway Server by a
80+
fingerprint. (Recommended)
81+
- `fingerprint`: A hex-encoded fingerprint of an DER-encoded
82+
TLS certificate. Will fall back to a stored fingerprint which
83+
will be invalid as soon as the certificate expires.
7984
"""
85+
fingerprint = b'm\x7f\xa3\x1d\x80\xdcV\xf9\xc1\xed\x17\x98*\xd6\x01\x7f'
8086
urls = {
8187
'get_public_key': 'https://msgapi.threema.ch/pubkeys/{}',
8288
'get_id_by_phone': 'https://msgapi.threema.ch/lookup/phone/{}',
@@ -91,8 +97,12 @@ class Connection:
9197
'download_blob': 'https://msgapi.threema.ch/blobs/{}'
9298
}
9399

94-
def __init__(self, id, secret, key=None, key_file=None):
95-
self._session = aiohttp.ClientSession()
100+
def __init__(self, id, secret, key=None, key_file=None, fingerprint=None,
101+
verify_fingerprint=False):
102+
if fingerprint is None and verify_fingerprint:
103+
fingerprint = self.fingerprint
104+
connector = aiohttp.TCPConnector(fingerprint=fingerprint)
105+
self._session = aiohttp.ClientSession(connector=connector)
96106
self._key = None
97107
self._key_file = None
98108
self.id = id

0 commit comments

Comments
 (0)