Skip to content

Commit

Permalink
Add backup validation
Browse files Browse the repository at this point in the history
This patch extends the backup and restore commands to validate the
backup file.  It also adds validate-backup and export-backup commands to
check the content of a backup file.

Fixes: #456
  • Loading branch information
robin-nitrokey committed Oct 27, 2023
1 parent 781d4b9 commit 7db3835
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 9 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Install required packages
run: |
apt update
apt install -y gcc libpcsclite-dev make swig
apt install -y gcc libpcsclite-dev make swig git
- name: Create virtual environment
run: make init
- name: Check code format
Expand All @@ -36,7 +36,7 @@ jobs:
- name: Install required packages
run: |
apt update
apt install -y gcc libpcsclite-dev make swig
apt install -y gcc libpcsclite-dev make swig git
- name: Create virtual environment
run: make init
- name: Check code import format
Expand All @@ -53,7 +53,7 @@ jobs:
- name: Install required packages
run: |
apt update
apt install -y gcc libpcsclite-dev make swig
apt install -y gcc libpcsclite-dev make swig git
- name: Create virtual environment
run: make init
- name: Check code style
Expand All @@ -70,7 +70,7 @@ jobs:
- name: Install required packages
run: |
apt update
apt install -y gcc libpcsclite-dev make swig
apt install -y gcc libpcsclite-dev make swig git
- name: Create virtual environment
run: make init
- name: Check code static typing
Expand All @@ -87,10 +87,10 @@ jobs:
- name: Install required packages
run: |
apt update
apt install -y gcc libpcsclite-dev make swig
apt install -y gcc libpcsclite-dev make swig git
- name: Create virtual environment
run: make init
- name: Check code static typing
run: |
. venv/bin/activate
make check-doctest
make check-doctest
108 changes: 106 additions & 2 deletions pynitrokey/cli/nethsm.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,19 @@
# http://opensource.org/licenses/MIT>, at your option. This file may not be
# copied, modified, or distributed except according to those terms.

import base64
import contextlib
import datetime
import json
import mimetypes
import os.path
import sys

import click
import nethsm as nethsm_sdk
from nethsm.backup import EncryptedBackup

from pynitrokey.cli.exceptions import CliException
from pynitrokey.helpers import prompt


Expand Down Expand Up @@ -1127,6 +1132,10 @@ def backup(ctx, filename):
with open(filename, "xb") as f:
f.write(data)
print(f"Backup for {nethsm.host} written to {filename}")
try:
EncryptedBackup.parse(data)
except ValueError as e:
raise CliException(f"Failed to validate backup: {e}", support_hint=False)


@nethsm.command()
Expand All @@ -1143,20 +1152,115 @@ def backup(ctx, filename):
type=DATETIME_TYPE,
help="The system time to set (default: the time of this system)",
)
@click.option(
"-f",
"--force",
is_flag=True,
help="Restore the backup even if validation fails",
)
@click.argument("filename")
@click.pass_context
def restore(ctx, backup_passphrase, system_time, filename):
def restore(ctx, backup_passphrase, system_time, force, filename):
"""Restore a backup of a NetHSM instance from a file.
If the system time is not set, the current system time is used."""
if not system_time:
system_time = datetime.datetime.now(datetime.timezone.utc)

with open(filename, "rb") as f:
data = f.read()
try:
EncryptedBackup.parse(data).decrypt(backup_passphrase)
except ValueError as e:
if force:
print(f"Failed to validate backup: {e}")
print("Backup is restored anyway as --force is set")
else:
raise CliException(
f"Failed to validate backup (use --force to restore anyway): {e}",
support_hint=False,
)

with connect(ctx, require_auth=False) as nethsm:
with open(filename, "rb") as f:
nethsm.restore(f, backup_passphrase, system_time)
nethsm.restore(data, backup_passphrase, system_time)
print(f"Backup restored on NetHSM {nethsm.host}")


@nethsm.command()
@click.option(
"-p",
"--backup-passphrase",
help="The backup passphrase for decryption (default: only the unencrypted metadata is validated)",
)
@click.argument("filename")
def validate_backup(backup_passphrase, filename):
"""Validate a NetHSM backup file.
Per default, only the metadata of the encrypted backup is validated. If
the backup passphrase is set, the backup is decrypted and the content is
also validated."""

with open(filename, "rb") as f:
data = f.read()
try:
encrypted = EncryptedBackup.parse(data)
except ValueError as e:
raise CliException(
f"Failed to validate backup metadata: {e}", support_hint=False
)

if backup_passphrase:
try:
encrypted.decrypt(backup_passphrase)
except ValueError as e:
raise CliException(
f"Failed to validate backup content: {e}", support_hint=False
)
print("Backup metadata and content are valid.")
else:
print("Backup metadata is valid.")


@nethsm.command()
@click.option(
"-p",
"--backup-passphrase",
hide_input=True,
prompt=True,
help="The backup passphrase",
)
@click.argument("filename")
def export_backup(backup_passphrase, filename):
"""Export the content a NetHSM backup file.
The key-value data stored in the backup file is printed to the standard
output as a JSON object using the base64 encoding for binary data.
Additionally, the .locked-domain-key and .version keys are set with the
domain key and version info extracted from the backup file."""

with open(filename, "rb") as f:
data = f.read()
try:
encrypted = EncryptedBackup.parse(data)
except ValueError as e:
raise CliException(f"Failed to parse backup metadata: {e}", support_hint=False)

try:
decrypted = encrypted.decrypt(backup_passphrase)
except ValueError as e:
raise CliException(f"Failed to decrypt backup content: {e}", support_hint=False)

data = {}
data[".locked-domain-key"] = base64.b64encode(decrypted.domain_key).decode()
data[".version"] = decrypted.version
for key, value in decrypted.data.items():
data[key] = base64.b64encode(value).decode()

json.dump(data, sys.stdout, indent=4)
print()


@nethsm.command()
@click.argument("filename")
@click.pass_context
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ dependencies = [
"protobuf >=3.17.3, < 4.0.0",
"click-aliases",
"semver",
"nethsm >= 0.3.1",
"nethsm @ git+https://github.com/Nitrokey/nethsm-sdk-py@validate-backup-pynitrokey",
]
dynamic = ["version", "description"]

Expand Down

0 comments on commit 7db3835

Please sign in to comment.