Skip to content

Commit

Permalink
Update sys-firewall RPM key via GUI Updater
Browse files Browse the repository at this point in the history
If the GUI updater observes the RPM key in sys-firewall is different from the latest key, run the sys-firewall state to update the key. The reason we check is to prevent needless applications of salt state, given the requirement to boot a disp-mgmt vm each time.

Here, we rely on checksums to avoid using sudo privileges to read the apt keys provisioned by both the dev logic and rpm package, which are found in /srv/salt/sd/sd-workstation

Handling qvm-copy in sd-dom0-files.sls has several hidden complexities:
 * shell script and rc.local will need to be updated/run as well
 * copy to vm, copy to folder and apply permissions, for 3 files

It may also lead to several edgecases:
 * the file will need to also be moved to the proper
 * issues with idempotency if the file is not deleted from ~/QubesIncoming
 * race conditions between dom0 rpm provisioning and sys-firewall provisioning
  • Loading branch information
emkll committed May 28, 2020
1 parent 2c54112 commit cf43eed
Show file tree
Hide file tree
Showing 2 changed files with 268 additions and 4 deletions.
78 changes: 77 additions & 1 deletion launcher/sdw_updater_gui/Updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
FLAG_FILE_LAST_UPDATED_DOM0 = os.path.join(DEFAULT_HOME, "sdw-last-updated")
LOCK_FILE = "sdw-launcher.lock"
LOG_FILE = "launcher.log"
SYS_FIREWALL_SIGNING_PUBKEY_LOCATION = "/rw/config/RPM-GPG-KEY-securedrop-workstation"
PROD_SIGNING_PUBKEY_SHASUM = (
"609990581da1ed7edd91015c3e4c5f75a363ac5d4f9a653be7f27f0c7f7e9f8b"
)
TEST_SIGNING_PUBKEY_SHASUM = (
"b12d8e46ab9547433dc6463a87e1376488ff3f9609c78c4a75f5d62e8e3cddbb"
)

sdlog = logging.getLogger(__name__)

Expand Down Expand Up @@ -376,23 +383,92 @@ def overall_update_status(results):
return UpdateStatus.UPDATES_OK


def _sys_firewall_has_correct_key():
"""
insert description here
"""
current_pubkey_shasum = ""
sdlog.info("Checking sys-firewall config")

try:
current_pubkey_shasum = subprocess.check_output(
[
"qvm-run",
"--pass-io",
"sys-firewall",
"sha256sum {}".format(SYS_FIREWALL_SIGNING_PUBKEY_LOCATION),
]
).decode("utf-8")
except subprocess.CalledProcessError as e:
sdlog.error("Failed to check sys-firewall rpm key")
sdlog.error(str(e))
return False

# If the key is neither test nor prod key we return false to later apply
# the sys-firewall state to ensure the pubkey updated in sys-firewall
if PROD_SIGNING_PUBKEY_SHASUM in current_pubkey_shasum:
sdlog.info("sys-firewall RPM config is using current prod key")
return True
elif TEST_SIGNING_PUBKEY_SHASUM in current_pubkey_shasum:
sdlog.info("sys-firewall RPM config is using current test key")
return True
else:
sdlog.info("sys-firewall RPM config is not using current dev or prod key")
return False


def apply_dom0_state():
"""
Applies the dom0 state to ensure dom0 and AppVMs are properly
Configured. This will *not* enforce configuration inside the AppVMs.
Here, we call qubectl directly (instead of through securedrop-admin) to
ensure it is environment-specific.
We also check the GPG pubkey for RPM verification in sys-firewall, as it is
used by dom0 to fetch updates. If the public key is out of date, we
update it by applying sys-firewall state.
"""
sdlog.info("Applying dom0 state")
try:
subprocess.check_call(["sudo", "qubesctl", "--show-output", "state.highstate"])
sdlog.info("Dom0 state applied")
return UpdateStatus.UPDATES_OK
except subprocess.CalledProcessError as e:
sdlog.error("Failed to dom0 state")
sdlog.error(str(e))
return UpdateStatus.UPDATES_FAILED

if not _sys_firewall_has_correct_key():
return _apply_firewall_state()

return UpdateStatus.UPDATES_OK


def _apply_firewall_state():
"""
Applies the sys-firewall state which will copy the RPM key in place in
sys-firewall's config and update the rc.local script to ensure the key is
always populated on boot.
"""
sdlog.info("Applying sys-firewall state")
try:
subprocess.check_call(
[
"sudo",
"qubesctl",
"--show-output",
"--skip-dom0",
"--targets",
"sys-firewall",
"state.highstate",
]
)
sdlog.info("sys-firewall state applied")
except subprocess.CalledProcessError as e:
sdlog.error("Failed to apply sys-firewall state")
sdlog.error(str(e))
return UpdateStatus.UPDATES_FAILED

return UpdateStatus.UPDATES_OK


def shutdown_and_start_vms():
"""
Expand Down
194 changes: 191 additions & 3 deletions launcher/tests/test_updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import pytest
import subprocess
import Updater
from importlib.machinery import SourceFileLoader
from datetime import datetime, timedelta
from tempfile import TemporaryDirectory
Expand Down Expand Up @@ -983,15 +984,202 @@ def test_should_run_updater_invalid_status_value(mocked_write):
assert updater.should_launch_updater(TEST_INTERVAL) is True


@mock.patch(
"subprocess.check_output",
side_effect=[Updater.PROD_SIGNING_PUBKEY_SHASUM.encode("utf-8")],
)
@mock.patch("subprocess.check_call")
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_apply_dom0_state_success(mocked_info, mocked_error, mocked_subprocess):
def test_apply_dom0_state_success_without_key_update(
mocked_info, mocked_error, mocked_call, mocked_output
):
updater.apply_dom0_state()
log_call_list = [call("Applying dom0 state"), call("Dom0 state applied")]
mocked_subprocess.assert_called_once_with(
log_call_list = [
call("Applying dom0 state"),
call("Dom0 state applied"),
call("Checking sys-firewall config"),
call("sys-firewall RPM config is using current prod key"),
]
mocked_call.assert_called_once_with(
["sudo", "qubesctl", "--show-output", "state.highstate"]
)
mocked_output.assert_called_once_with(
[
"qvm-run",
"--pass-io",
"sys-firewall",
"sha256sum /rw/config/RPM-GPG-KEY-securedrop-workstation",
]
)
mocked_info.assert_has_calls(log_call_list)
assert not mocked_error.called


@mock.patch(
"subprocess.check_output",
side_effect=subprocess.CalledProcessError(1, "check_output"),
)
@mock.patch("subprocess.check_call")
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_apply_dom0_state_fails_on_checking_key_update(
mocked_info, mocked_error, mocked_call, mocked_output
):
updater.apply_dom0_state()
log_call_list = [
call("Applying dom0 state"),
call("Dom0 state applied"),
call("Checking sys-firewall config"),
call("Applying sys-firewall state"),
call("sys-firewall state applied"),
]
error_call_list = [
call("Failed to check sys-firewall rpm key"),
call("Command 'check_output' returned non-zero exit status 1."),
]
check_calls = [
call(["sudo", "qubesctl", "--show-output", "state.highstate"]),
call(
[
"sudo",
"qubesctl",
"--show-output",
"--skip-dom0",
"--targets",
"sys-firewall",
"state.highstate",
]
),
]
mocked_call.assert_has_calls(check_calls)
mocked_output.assert_called_once_with(
[
"qvm-run",
"--pass-io",
"sys-firewall",
"sha256sum /rw/config/RPM-GPG-KEY-securedrop-workstation",
]
)
mocked_info.assert_has_calls(log_call_list)
mocked_error.assert_has_calls(error_call_list)


@mock.patch("subprocess.check_call", side_effect="0")
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_apply_firewall_state(mocked_info, mocked_error, mocked_check):
updater._apply_firewall_state()
log_call_list = [
call("Applying sys-firewall state"),
call("sys-firewall state applied"),
]
mocked_check.assert_called_once_with(
[
"sudo",
"qubesctl",
"--show-output",
"--skip-dom0",
"--targets",
"sys-firewall",
"state.highstate",
]
)
mocked_info.assert_has_calls(log_call_list)
assert not mocked_error.called


@mock.patch(
"subprocess.check_call", side_effect=subprocess.CalledProcessError(1, "check_call")
)
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_apply_firewall_state_fails(mocked_info, mocked_error, mocked_check):
updater._apply_firewall_state()
error_call_list = [
call("Failed to apply sys-firewall state"),
call("Command 'check_call' returned non-zero exit status 1."),
]
mocked_check.assert_called_once_with(
[
"sudo",
"qubesctl",
"--show-output",
"--skip-dom0",
"--targets",
"sys-firewall",
"state.highstate",
]
)
mocked_info.assert_called_once_with("Applying sys-firewall state")
mocked_error.assert_has_calls(error_call_list)


@mock.patch(
"subprocess.check_output",
side_effect=[Updater.PROD_SIGNING_PUBKEY_SHASUM.encode("utf-8")],
)
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_sys_firewall_has_correct_key_prod(mocked_info, mocked_error, mocked_output):
updater._sys_firewall_has_correct_key()
log_call_list = [
call("Checking sys-firewall config"),
call("sys-firewall RPM config is using current prod key"),
]
mocked_output.assert_called_once_with(
[
"qvm-run",
"--pass-io",
"sys-firewall",
"sha256sum /rw/config/RPM-GPG-KEY-securedrop-workstation",
]
)
mocked_info.assert_has_calls(log_call_list)
assert not mocked_error.called


@mock.patch(
"subprocess.check_output",
side_effect=[Updater.TEST_SIGNING_PUBKEY_SHASUM.encode("utf-8")],
)
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_sys_firewall_has_correct_key_dev(mocked_info, mocked_error, mocked_output):
updater._sys_firewall_has_correct_key()
log_call_list = [
call("Checking sys-firewall config"),
call("sys-firewall RPM config is using current test key"),
]
mocked_output.assert_called_once_with(
[
"qvm-run",
"--pass-io",
"sys-firewall",
"sha256sum /rw/config/RPM-GPG-KEY-securedrop-workstation",
]
)
mocked_info.assert_has_calls(log_call_list)
assert not mocked_error.called


@mock.patch("subprocess.check_output", side_effect=["abc".encode("utf-8")])
@mock.patch("Updater.sdlog.error")
@mock.patch("Updater.sdlog.info")
def test_sys_firewall_has_other_key(mocked_info, mocked_error, mocked_output):
updater._sys_firewall_has_correct_key()
log_call_list = [
call("Checking sys-firewall config"),
call("sys-firewall RPM config is not using current dev or prod key"),
]
mocked_output.assert_called_once_with(
[
"qvm-run",
"--pass-io",
"sys-firewall",
"sha256sum /rw/config/RPM-GPG-KEY-securedrop-workstation",
]
)
mocked_info.assert_has_calls(log_call_list)
assert not mocked_error.called

Expand Down

0 comments on commit cf43eed

Please sign in to comment.