9
9
import pytest
10
10
11
11
from hawkbit_mgmt import HawkbitMgmtTestClient , HawkbitError
12
- from helper import run_pexpect , available_port
12
+ from helper import run_pexpect , available_port , run
13
+ from mtls_conf import MtlsConfig
13
14
14
15
def pytest_addoption (parser ):
15
16
"""Register custom argparse-style options."""
@@ -103,8 +104,11 @@ def _adjust_config(options={'client': {}}, remove={}, add_trailing_space=False):
103
104
adjusted_config .set (section , key , value )
104
105
105
106
# remove
106
- for section , option in remove .items ():
107
- adjusted_config .remove_option (section , option )
107
+ for section , options in remove .items ():
108
+ if type (options ) is str :
109
+ options = [options ]
110
+ for option in options :
111
+ adjusted_config .remove_option (section , option )
108
112
109
113
# add trailing space
110
114
if add_trailing_space :
@@ -209,6 +213,24 @@ def rauc_dbus_install_success(rauc_bundle):
209
213
assert proc .terminate (force = True )
210
214
proc .expect (pexpect .EOF )
211
215
216
+ @pytest .fixture
217
+ def rauc_dbus_install_success_mtls (rauc_bundle ,tmp_path_factory ):
218
+ """
219
+ Creates a RAUC D-Bus dummy interface on the SessionBus mimicing a successful installation on
220
+ InstallBundle().
221
+ """
222
+ import pexpect
223
+
224
+ proc = run_pexpect (f'{ sys .executable } -m rauc_dbus_dummy --tmp-dir { tmp_path_factory .getbasetemp ()} --mtls { rauc_bundle } ' ,
225
+ cwd = os .path .dirname (__file__ ))
226
+ proc .expect ('Interface published' )
227
+
228
+ yield
229
+
230
+ assert proc .isalive ()
231
+ assert proc .terminate (force = True )
232
+ proc .expect (pexpect .EOF )
233
+
212
234
@pytest .fixture
213
235
def rauc_dbus_install_failure (rauc_bundle ):
214
236
"""
@@ -242,11 +264,26 @@ def nginx_config(tmp_path_factory):
242
264
243
265
http {{
244
266
access_log /dev/null;
267
+ map $ssl_client_s_dn $ssl_client_s_dn_cn {{
268
+ default "";
269
+ ~CN=(?<CN>[^,]+) $CN;
270
+ }}
245
271
272
+ {server}
273
+ }}
274
+ """
275
+ http_server = """
246
276
server {{
247
277
listen {port};
248
278
listen [::]:{port};
249
-
279
+
280
+ client_body_temp_path {tmp_dir};
281
+ proxy_temp_path {tmp_dir};
282
+ fastcgi_temp_path {tmp_dir};
283
+ uwsgi_temp_path {tmp_dir};
284
+ scgi_temp_path {tmp_dir};
285
+ {server_options}
286
+
250
287
location / {{
251
288
proxy_pass http://localhost:8080;
252
289
{location_options}
@@ -258,12 +295,63 @@ def nginx_config(tmp_path_factory):
258
295
sub_filter_once off;
259
296
}}
260
297
}}
261
- }}"""
262
-
263
- def _nginx_config (port , location_options ):
264
- proxy_config = tmp_path_factory .mktemp ('nginx' ) / 'nginx.conf'
265
- location_options = ( f'{ key } { value } ;' for key , value in location_options .items ())
266
- proxy_config_str = config_template .format (port = port , location_options = " " .join (location_options ))
298
+ """
299
+ mtls_server = """
300
+ server {{
301
+ listen {port} ssl;
302
+ listen [::]:{port} ssl;
303
+
304
+ client_body_temp_path {tmp_dir};
305
+ proxy_temp_path {tmp_dir};
306
+ fastcgi_temp_path {tmp_dir};
307
+ uwsgi_temp_path {tmp_dir};
308
+ scgi_temp_path {tmp_dir};
309
+ server_name localhost;
310
+ {server_options}
311
+
312
+
313
+ ssl_verify_client optional;
314
+ ssl_verify_depth 3;
315
+ # For devices that is using device integration API,
316
+ # Mutual TLS is required.
317
+ location ~*/.*/controller/ {{
318
+ if ($ssl_client_verify != SUCCESS) {{
319
+ return 403;
320
+ }}
321
+
322
+ proxy_pass http://localhost:8080;
323
+ proxy_set_header Host $http_host;
324
+ proxy_set_header X-Real-IP $remote_addr;
325
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
326
+ proxy_set_header X-Forwarded-Proto https;
327
+ proxy_set_header X-Forwarded-Port {port};
328
+ proxy_set_header X-Forwarded-Protocol https;
329
+
330
+ # Client certificate Common Name and Issuer Hash is required
331
+ # for auth in hawkbit.
332
+ proxy_set_header X-Ssl-Client-Cn $ssl_client_s_dn_cn;
333
+
334
+ # These are required for clients to upload and download software.
335
+ proxy_request_buffering off;
336
+ client_max_body_size 1000m;
337
+ {location_options}
338
+ }}
339
+ }}
340
+ """
341
+ def dict_to_nginx_option (option :dict ):
342
+ if not option :
343
+ return ""
344
+ option = ( f'{ key } { value } ;' for key , value in option .items ())
345
+ return " " .join (option )
346
+ def _nginx_config (port , location_options , server_options , mtls ):
347
+ nginx_tmp_dir = tmp_path_factory .mktemp ('nginx' )
348
+ server_config = mtls_server if mtls else http_server
349
+ server_config_str = server_config .format (port = port ,location_options = dict_to_nginx_option (location_options ),
350
+ server_options = dict_to_nginx_option (server_options ),
351
+ tmp_dir = nginx_tmp_dir )
352
+
353
+ proxy_config_str = config_template .format (port = port ,server = server_config_str , tmp_dir = nginx_tmp_dir )
354
+ proxy_config = nginx_tmp_dir / 'nginx.conf'
267
355
proxy_config .write_text (proxy_config_str )
268
356
return proxy_config
269
357
@@ -272,18 +360,16 @@ def _nginx_config(port, location_options):
272
360
@pytest .fixture (scope = 'session' )
273
361
def nginx_proxy (nginx_config ):
274
362
"""
275
- Runs an nginx rate liming proxy, limiting download speeds to 70 KB/s . HTTP requests are
363
+ Runs an nginx proxy. HTTP requests are
276
364
forwarded to port 8080 (default port of the docker hawkBit instance). Returns the port the
277
- proxy is running on. This port can be set in the rauc-hawkbit-updater config to rate limit its
278
- HTTP requests.
365
+ proxy is running on. This port can be set in the rauc-hawkbit-updater config.
279
366
"""
280
367
import pexpect
281
368
282
369
procs = []
283
-
284
- def _nginx_proxy (options ):
370
+ def _nginx_proxy (options , server_options = None , mtls = False ):
285
371
port = available_port ()
286
- proxy_config = nginx_config (port , options )
372
+ proxy_config = nginx_config (port , options , server_options , mtls )
287
373
288
374
try :
289
375
proc = run_pexpect (f'nginx -c { proxy_config } -p .' , timeout = None )
@@ -332,3 +418,40 @@ def partial_download_port(nginx_proxy):
332
418
'limit_rate' : '70k' ,
333
419
}
334
420
return nginx_proxy (location_options )
421
+
422
+ @pytest .fixture
423
+ def mtls_download_port (nginx_proxy , mtls_certificates ):
424
+ """
425
+ Runs an nginx proxy. HTTPS requests are forwarded to port 8080
426
+ (default port of the docker hawkBit instance). Returns the port the proxy is running on. This
427
+ port can be set in the rauc-hawkbit-updater config to test partial downloads.
428
+ """
429
+ mtls_cert = mtls_certificates ()
430
+ hash_issuer = mtls_cert .get_issuer_hash ()
431
+ location_options = {"proxy_set_header X-Ssl-Issuer-Hash-1" : hash_issuer }
432
+ server_options = {
433
+ "ssl_certificate" : mtls_cert .ca_cert ,
434
+ "ssl_certificate_key" : mtls_cert .ca_key ,
435
+ "ssl_client_certificate" : mtls_cert .ca_cert
436
+ }
437
+ return nginx_proxy (location_options , server_options , mtls = True )
438
+
439
+
440
+
441
+ @pytest .fixture
442
+ def mtls_config (tmp_path_factory ):
443
+ return MtlsConfig (tmp_path_factory .getbasetemp ())
444
+
445
+ @pytest .fixture
446
+ def mtls_certificates (mtls_config , tmp_path_factory , hawkbit_target_added ):
447
+ """
448
+ Generate CA cert and key if they don't exist and also generate specific client cert and key
449
+ for the Hawkbit controller id as Common Name.
450
+ """
451
+ def _mtls_certificates ():
452
+ out , err , exitcode = run (f'{ os .path .dirname (__file__ )} /gen_key.sh { mtls_config .certs_dir } { hawkbit_target_added } ' , timeout = 20 )
453
+ assert exitcode == 0
454
+ assert mtls_config .ca_cert_exist ()
455
+ assert mtls_config .client_cert_exist ()
456
+ return mtls_config
457
+ return _mtls_certificates
0 commit comments