99import pytest
1010
1111from 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
1314
1415def pytest_addoption (parser ):
1516 """Register custom argparse-style options."""
@@ -103,8 +104,11 @@ def _adjust_config(options={'client': {}}, remove={}, add_trailing_space=False):
103104 adjusted_config .set (section , key , value )
104105
105106 # 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 )
108112
109113 # add trailing space
110114 if add_trailing_space :
@@ -209,6 +213,24 @@ def rauc_dbus_install_success(rauc_bundle):
209213 assert proc .terminate (force = True )
210214 proc .expect (pexpect .EOF )
211215
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+
212234@pytest .fixture
213235def rauc_dbus_install_failure (rauc_bundle ):
214236 """
@@ -242,11 +264,26 @@ def nginx_config(tmp_path_factory):
242264
243265http {{
244266 access_log /dev/null;
267+ map $ssl_client_s_dn $ssl_client_s_dn_cn {{
268+ default "";
269+ ~CN=(?<CN>[^,]+) $CN;
270+ }}
245271
272+ {server}
273+ }}
274+ """
275+ http_server = """
246276 server {{
247277 listen {port};
248278 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+
250287 location / {{
251288 proxy_pass http://localhost:8080;
252289 {location_options}
@@ -258,12 +295,63 @@ def nginx_config(tmp_path_factory):
258295 sub_filter_once off;
259296 }}
260297 }}
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'
267355 proxy_config .write_text (proxy_config_str )
268356 return proxy_config
269357
@@ -272,18 +360,16 @@ def _nginx_config(port, location_options):
272360@pytest .fixture (scope = 'session' )
273361def nginx_proxy (nginx_config ):
274362 """
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
276364 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.
279366 """
280367 import pexpect
281368
282369 procs = []
283-
284- def _nginx_proxy (options ):
370+ def _nginx_proxy (options , server_options = None , mtls = False ):
285371 port = available_port ()
286- proxy_config = nginx_config (port , options )
372+ proxy_config = nginx_config (port , options , server_options , mtls )
287373
288374 try :
289375 proc = run_pexpect (f'nginx -c { proxy_config } -p .' , timeout = None )
@@ -332,3 +418,40 @@ def partial_download_port(nginx_proxy):
332418 'limit_rate' : '70k' ,
333419 }
334420 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