diff --git a/README.md b/README.md index 3bafe15..3ec7ae5 100644 --- a/README.md +++ b/README.md @@ -262,10 +262,13 @@ The file `customization.py` also contains OEM strings that can be safely changed In order to fully protect the AES key that is needed to support NPBackup, one can compile the program with Nuitka. Compiling needs restic binary for the target platform in `RESTIC_SOURCE_FILES` folder, files must be named `restic_{version}_{platform}_{arch}[.extension]` like provided by restic.net or [github](https://github.com/restic/restic) -Linux binaries need to be made executable in the `RESTIC_SOURCE_FILES` folder. +Linux binaries need to be made executable in the `RESTIC_SOURCE_FILES` folder. +You can use `update_restic.sh` script in `RESTIC_SOURCE_FILES` to download binaries. You'll need to change the default AES key in `secrets.py`, see the documentation in the file itself. +One you're setup, you need to install compilation tools, see `requirements-compilation.txt` file. + Compile options are available in `compile.py`. We maintain a special 32 bit binary for Windows 7 which allows to backup those old machines until they get replaced. @@ -282,7 +285,7 @@ Official binaries for Windows provided by NetInvent are signed with a certificat Also, official binaries are compiled using Nuitka Commercial grade, which is more secure in storing secrets. Pre-compiled builds for Windows have been code signed with NetInvent's EV certificate, using [windows_tools.signtool](github.com/netinvent/windows_tools) -Signing on a Windows machine with Windows SDK installed can be done via `bin\sign_windows.py` script. +Signing on a Windows machine with Windows SDK installed can be done via `windows\sign_windows.py` script. Alternatively, you can sign executables via: ``` from windows_tools.signtool import SignTool diff --git a/bin/compile.py b/bin/compile.py index 7d025e1..660b28e 100644 --- a/bin/compile.py +++ b/bin/compile.py @@ -7,8 +7,8 @@ __intname__ = "npbackup.compile" __author__ = "Orsiris de Jong" __copyright__ = "Copyright (C) 2023-2024 NetInvent" __license__ = "GPL-3.0-only" -__build__ = "2024090301" -__version__ = "2.0.1" +__build__ = "2024090801" +__version__ = "2.1.0" """ @@ -18,6 +18,9 @@ Nuitka compilation script tested for - Linux i386 - Linux i686 - Linux armv71 + + Also optionally signs windows executables + """ @@ -28,6 +31,8 @@ import argparse import atexit from command_runner import command_runner from ofunctions.platform import python_arch, get_os +if os.name == "nt": + from npbackup.windows.sign_windows import sign AUDIENCES = ["public", "private"] BUILD_TYPES = ["cli", "gui", "viewer"] @@ -177,7 +182,7 @@ def have_nuitka_commercial(): return False -def compile(arch: str, audience: str, build_type: str, onefile: bool, create_tar_only: bool): +def compile(arch: str, audience: str, build_type: str, onefile: bool, create_tar_only: bool, ev_cert_data: str = None): if build_type not in BUILD_TYPES: print("CANNOT BUILD BOGUS BUILD TYPE") sys.exit(1) @@ -327,6 +332,9 @@ def compile(arch: str, audience: str, build_type: str, onefile: bool, create_tar fh.write(npbackup_version) print(f"COMPILED {'WITH SUCCESS' if not errors else 'WITH ERRORS'}") + if os.name == "nt" and ev_cert_data: + sign(ev_cert_data=ev_cert_data, dry_run=args.dry_run) + if not onefile: if not create_archive( platform=platform, @@ -417,6 +425,15 @@ if __name__ == "__main__": help="Build single file executable (more prone to AV detection)", ) + parser.add_argument( + "--sign", + type=str, + dest="ev_cert_data", + default=False, + required=False, + help="Digitally sign windows executables", + ) + parser.add_argument( "--create-tar-only", action="store_true", @@ -472,7 +489,8 @@ if __name__ == "__main__": audience=audience, build_type=build_type, onefile=args.onefile, - create_tar_only=create_tar_only + create_tar_only=create_tar_only, + ev_cert_data=args.ev_cert_data ) if not create_tar_only: audience_build = "private" if private_build else "public" diff --git a/bin/sign_windows.py b/bin/sign_windows.py deleted file mode 100644 index 3995b7e..0000000 --- a/bin/sign_windows.py +++ /dev/null @@ -1,45 +0,0 @@ -#! /usr/bin/env python -# -*- coding: utf-8 -*- -# -# This file is part of npbackup - -__intname__ = "npbackup.sign_windows" -__author__ = "Orsiris de Jong" -__copyright__ = "Copyright (C) 2023-2024 NetInvent" -__license__ = "GPL-3.0-only" -__build__ = "2024060401" -__version__ = "1.1.2" - - -import os - -try: - from windows_tools.signtool import SignTool -except ImportError: - print("This tool needs windows_tools.signtool >= 0.3.1") - - -basepath = r"C:\GIT\npbackup\BUILDS" -audiences = ["private", "public"] -arches = ["x86", "x64"] -binaries = ["npbackup-cli", "npbackup-gui", "npbackup-viewer"] - -signer = SignTool() - -for audience in audiences: - for arch in arches: - for binary in binaries: - one_file_exe_path = exe_path = os.path.join( - basepath, audience, "windows", arch, binary + f"-{arch}.exe" - ) - standalone_exe_path = os.path.join( - basepath, audience, "windows", arch, binary + ".dist", binary + f".exe" - ) - for exe_file in (one_file_exe_path, standalone_exe_path): - if os.path.isfile(exe_file): - print(f"Signing {exe_file}") - result = signer.sign(exe_file, bitness=arch) - if not result: - raise EnvironmentError( - "Could not sign executable ! Is the PKI key connected ?" - ) diff --git a/npbackup/requirements-compilation.txt b/npbackup/requirements-compilation.txt new file mode 100644 index 0000000..aa84cfe --- /dev/null +++ b/npbackup/requirements-compilation.txt @@ -0,0 +1,2 @@ +nuitka>=2.4.8 +windows_tools.signtool>0.5.0 \ No newline at end of file diff --git a/npbackup/windows/sign_windows.py b/npbackup/windows/sign_windows.py new file mode 100644 index 0000000..9892493 --- /dev/null +++ b/npbackup/windows/sign_windows.py @@ -0,0 +1,132 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- +# +# This file is part of npbackup + +__intname__ = "npbackup.sign_windows" +__author__ = "Orsiris de Jong" +__copyright__ = "Copyright (C) 2023-2024 NetInvent" +__license__ = "GPL-3.0-only" +__build__ = "2024090801" +__version__ = "1.2.0" + + +import os +import sys +import argparse +from cryptidy.symmetric_encryption import decrypt_message + +try: + from windows_tools.signtool import SignTool +except ImportError: + print("This tool needs windows_tools.signtool >= 0.4.0") + + +basepath = r"C:\GIT\npbackup\BUILDS" +audiences = ["private", "public"] +arches = ["x86", "x64"] +binaries = ["npbackup-cli", "npbackup-gui", "npbackup-viewer"] + + +def check_private_ev(): + """ + Test if we have private ev data + """ + try: + from PRIVATE._private_ev_data import AES_EV_KEY + from PRIVATE._private_obfuscation import obfuscation + + print("We have private EV certifcate DATA") + return obfuscation(AES_EV_KEY) + except ImportError as exc: + print("ERROR: Cannot load private EV certificate DATA: {}".format(exc)) + sys.exit() + + +def get_ev_data(cert_data_path): + """ + This retrieves specific data for crypto env + """ + aes_key = check_private_ev() + with open(cert_data_path, "rb") as fp: + ev_cert_data = fp.read() + try: + timestamp, ev_cert = decrypt_message(ev_cert_data, aes_key=aes_key) + ( + pkcs12_certificate, + pkcs12_password, + container_name, + cryptographic_provider, + ) = ev_cert + except Exception as exc: + print("EV Cert data is corrupt") + sys.exit(1) + return pkcs12_certificate, pkcs12_password, container_name, cryptographic_provider + + +def sign(ev_cert_data: str = None, dry_run: bool = False): + if ev_cert_data: + ( + pkcs12_certificate, + pkcs12_password, + container_name, + cryptographic_provider, + ) = get_ev_data(ev_cert_data) + signer = SignTool( + certificate=pkcs12_certificate, + pkcs12_password=pkcs12_password, + container_name=container_name, + cryptographic_provider=cryptographic_provider, + ) + else: + signer = SignTool() + + for audience in audiences: + for arch in arches: + for binary in binaries: + one_file_exe_path = os.path.join( + basepath, audience, "windows", arch, binary + f"-{arch}.exe" + ) + standalone_exe_path = os.path.join( + basepath, + audience, + "windows", + arch, + binary + ".dist", + binary + f".exe", + ) + for exe_file in (one_file_exe_path, standalone_exe_path): + if os.path.isfile(exe_file): + print(f"Signing {exe_file}") + result = signer.sign(exe_file, bitness=arch, dry_run=dry_run) + if not result: + # IMPORTANT: If using an automated crypto USB EV token, we need to stop on error so we don't lock ourselves out of the token with bad password attempts + raise EnvironmentError( + "Could not sign executable ! Is the PKI key connected ?" + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + prog="npbackup sign_windows.py", + description="Windows executable signer for NPBackup", + ) + + parser.add_argument( + "--dry-run", + action="store_true", + default=False, + required=False, + help="Don't actually sign anything, just test command", + ) + parser.add_argument( + "--ev-cert-data", + type=str, + default=None, + required=False, + help="Path to EV certificate data", + ) + + args = parser.parse_args() + + sign(ev_cert_data=args.ev_cert_data, dry_run=args.dry_run)