Whonix Windows Installer - Design Documentation

From Whonix
< Dev
Jump to navigation Jump to search

Work in progress.

Design / Features[edit]

WhonixStarter(.exe):

  • new implementation of whonix.exe in lazarus (without NET framework)
  • platform independent ( later linux/mac version possible )
  • ui consists of two forms ( main & error )
  • main form has two buttons for start/stop and manage Whonix VMs
  • error form pops up if virtualbox is missing

WhonixStarterSetup.msi:

  • installs windows version of WhonixStarter
  • adds start menu entry
  • adds desktop shortcut
  • uninstall over Windows "Programs and Features" tool

WhonixSetup(.exe):

  • ui consists of a main form with several pages guiding the user through the installation process
  • platform independent ( later linux/mac version possible )
  • installs VirtualBox and WhonixOVA
  • executes WhonixStarterSetup.msi (Windows only)
  • checks installed and only reinstall missing components
  • does not uninstall or delete any component

Challenges:

  • Whonix .ova is bigger than 2 GB.
  • Windows .cab files have a hardcoded 2 GB maximum file size.

Requirements:

  • cross compile on Debian (source) for Windows (target)
  • building does not require Windows

Build limitations:

  • needs Debian bookworm or above because of minimal wixl and lazarus version

flow chart[edit]

(1) Whonix-Starter:

  • lazbuildWhonixStarter.lprWhonixStarter.exe
  • wixlWhonixStarterSetup.wxsWhonixStarter.exe, WhonixStarterSetup.wxsWhonixStarterSetup.msi

(2) Whonix-Installer:

  • lazbuildWhonixSetup.lprWhonixSetup.exe
  • WhonixSetup.exe + append + Whonix.ovaWhonixSetup-Xfce.exe

misc[edit]

Notes[edit]

UseVersionInfo WhonixSetup.lpi[edit]

Can the following variables be removed if not essential?

OriginalFilename="WhonixInstaller-XYZ-1.2.3.4.exe"
      <UseVersionInfo Value="True"/>
      <MajorVersionNr Value="1"/>

The variables aren't essential for functionality.

For reasons of end user confidence it's recommended to keep the version and file information (<UseVersionInfo Value="True"/>).

"OriginalFilename" could just be "WhonixInstaller.exe". ( but I like to set the same as the final filename displayed in file browser )

"MajorVersionNr", "MinorVersionNr", "RevisionNr", "BuildNr" is summarized as "FileVersion" by Lazarus. If we delete these values the file version will be 0.0.0.0

I'm not sure in this case if the version refers to the installer or the software being installed.

CI[edit]

Whonix-Starter:

Whonix-Installer:

code signing[edit]

Introduction[edit]

EV (extended validation) certificate required to avoid Microsoft SmartScreen Filter warning message.

requirements[edit]

  • EV code signing for Windows authenticode to avoid Microsoft SmartScreen Filter warning message.
  • cross signing
  • build scripts running on Debian Linux
  • build result (program) running on Windows 64 bit
  • avoid running proprietary closed source software on local build machine
  • can be fully automated using build scripts
  • avoid hardware token (compatibility, hassle)
  • avoid proprietary closed source device drivers
  • ideally avoid non-mainline Linux kernel drivers
  • supports signing big files

providers[edit]

thalesgroup:

  • asked. does not have Linux tools.

Certum:

sectigo:

certerassl:

ssl.com:

Google Cloud HSM;

Verification of Code Signing Process[edit]

Verification of VirtualBox[edit]

VirtualBox for Windows is signed using Microsoft Authenticode signatures ("signtool").

VirtualBox's digital signature can be verified on the Linux platform using osslsigncode. Example:

osslsigncode verify -in VirtualBox-*.exe

A script verifyarchive.org has been added to the virtualbox-windows-installer-binary repository as a reminder and example how to verify the digital software signatures on the Linux platform.

Signing Whonix Windows Installer[edit]

This document is based on a case where an executable (hello.exe) is signed using GitHub Actions (CI) and CodeSigner.

For more details on the test, please refer to this GitHub repository: https://github.com/adrelanos/codesigner-testarchive.org

When a file (hello.exe) is signed, creating hello_signed.exe, the goal is to ensure that no additional modifications, beyond the signing process, have occurred. These could potentially include the insertion of malicious code.

Overview:

  • Original file: hello.exe
  • Signed file: hello_signed.exe
  • Extracted signature file: hello_signature.pem
  • File with reattached signature: hello_with_signature.exe
  • File with reattached signature and reset PE header: hello_with_signature_reset_PE.exe
  • File with removed signature: hello_without_signature.exe
  • File with removed signature and reset PE header: hello_without_signature_reset_PE.exe
  • pe-header-to-zeroarchive.org

To achieve this, the original file is compared with the signed file in various stages and through different methods. The key stages are as follows:

1. Extract the signature from the signed file using osslsigncode:

osslsigncode extract-signature -in hello_signed.exe -out hello_signature.pem

2. The original file (hello.exe) is then re-signed using this extracted signature to create a new file (hello_with_signature.exe):

osslsigncode attach-signature -sigin hello_signature.pem -in hello.exe -out hello_with_signature.exe

At this point, one would expect hello_signed.exe to be identical to hello_with_signature.exe. However, it was discovered that the signing process (osslsigncode attach-signature) modified the PE header of the file by adding a PE checksum, thus resulting in a difference between these two files.

To analyze and understand these differences, a set of tools were used, including diff, vbindiff, diffoscope, and readpe. These comparisons brought to light the change in the PE checksum.

3. In order to make a direct comparison, the PE checksum in hello_with_signature.exe was reset to 0, mirroring its original state in hello.exe and hello_signed.exe. This was achieved using a Python script named pe-header-to-zero:

pe-header-to-zero hello_with_signature.exe hello_with_signature_reset_PE.exe

After running this script, the newly created file hello_with_signature_reset_PE.exe was found to be an exact match to hello_signed.exe.

4. The script pe-header-to-zero was also used on hello_without_signature.exe to create hello_without_signature_reset_PE.exe:

pe-header-to-zero hello_without_signature.exe hello_without_signature_reset_PE.exe

It was found that hello_without_signature_reset_PE.exe was an exact match to the original hello.exe, further validating the process.

Following this thorough examination, it can be reasonably stated that the signing process did not introduce any unwanted or malicious modifications to the original executable file.

All operations were performed using the osslsigncode tool.

To install and examine PE headers, the pev tool was used:

sudo apt install pev

To view the PE checksum, the readpe utility was used:

readpe hello_signed.exe

readpe hello_with_signature_reset_PE.exe

readpe hello_without_signature_reset_PE.exe

Archived Tasks[edit]

WhonixStarter.msi execution[edit]

Happening on Windows 10.

Step 9 / 9 : Installing Whonix-Starter...
Execute: msiexec /i "C:\Users\user\AppData\Local\Whonix-Xfce-17.2.0.1\WhonixStarter.msi"
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
Error : Whonix-Starter could not be installed.

-> fixed: see https://github.com/einsiedler90/Whonix-Installer/commit/5e52d6fc436001345db896ffee2f89c8b20c0ab1archive.org

Debugging Attempt 1: Double Clicking WhonixStarter.msi

While this happened, user kept the Whonix-Installer open and then attempted to manually run C:\Users\user\AppData\Local\Whonix-Xfce-17.2.0.1\WhonixStarter.msi for debugging reasons. Result:

WhonixStarter

Another installation in progress. You must complete that installation before continuing this one.

Retry | Cancel

What can we conclude from this? Maybe while Whonix installer is running, it cannot execute msiexec /i "C:\Users\user\AppData\Local\Whonix-Xfce-17.2.0.1\WhonixStarter.msi" because it is blocking itself?

-> perhaps obsolete due to previous fix? Strange: execution of "msiexec" is finnished when "Whonix-Starter could not be installed." appears.

Debugging Attempt 2: Starting WhonixStarter.msi from Terminal

Starting msiexec /i "C:\Users\user\AppData\Local\Whonix-Xfce-17.2.0.1\WhonixStarter.msi" from terminal does not show any additional output.

TODO[edit]

merge[edit]

Reminder: Always please merge first before developing further.

update this wiki page[edit]

Please add comments to this wiki page once any things have been changed.

rename setup to installer - update #3[edit]

file: Whonix-Installer build-dummy.sh

export FILE_INSTALLER_BINARY_FINAL=~/windows-installer-dummy-temp-delete-me/WhonixSetup-$VERSION_FULL.exe

Please don't name "setup" because not using that word anywhere else.

do not delete WhonixStarter.msi after installation[edit]

A user reported that WhonixStarter.msi gets deleted once Whonix installer gets closed.

If possible, please do not delete WhonixStarter.msi after installation. Why? To make this process easier to debug.

Instructions for AI based Code Review[edit]

If the AI is talking trash, just delete it from the wiki without comment. No need to comment on AI generated text which is nonsense.

Please delete this wiki chapter now.

Claude AI based Code Review of whonixinstaller_main.pas[edit]

1) Resource management: While the code does free some resources, it's not consistent. For instance, in the FormCreate method, ResourceStream is created and freed, but in some other places like InstallationBuildIn, resources aren't always explicitly freed.

2) Potential infinite loop: In FormCreate, there's a while loop checking for disk space. If the user keeps selecting invalid directories, this could potentially become an infinite loop.

3) Unused variables: There are some unused variables in the code, like filename in the commented-out EnsureValidExePath procedure.

4) Inconsistent use of string types: The code mixes string and AnsiString types, which could lead to encoding issues.

5) Lack of input validation: When executing external commands, there's little to no validation of input parameters, which could potentially be a security risk.

6) UI responsiveness: The code uses Sleep in the SetNextStatus method, which could make the UI less responsive. Consider using a timer or async operations instead.

7) Incomplete implementation: Some methods like LabelCompleteDescClick are empty, suggesting incomplete functionality.

ChatGPT AI based Code Review of whonixinstaller_main.pas[edit]

Cross-Platform Compatibility[edit]

The function AppDiskGetFreeSpace in WhonixUtils.pas seems to have platform-specific implementations, but it doesn't handle cases for platforms other than Windows and Linux.

{$ifdef linux}
// Linux-specific code
{$endif}

{$ifdef windows}
// Windows-specific code
{$endif}

//cannot detect
Exit(-1);

Solution: Add support for other platforms or at least log an appropriate message when the platform is unsupported.

Missing ApplicationName Declaration[edit]

In WhonixInstaller_Main.pas, ApplicationName is used but not declared or initialized:

UnpackPath := IncludeTrailingPathDelimiter(
  IncludeTrailingPathDelimiter(SelectDirectoryDialog.FileName) +
  ApplicationName);

Solution: Declare and initialize ApplicationName.

Thread Blocking Due to Sleep and Application.ProcessMessages[edit]

In WhonixUtils.pas, Execute uses Sleep and Application.ProcessMessages in a loop, which can lead to a blocking UI:

repeat
  Sleep(100);
  Application.ProcessMessages;
  ...
until (BytesRead = 0) and not Running;

Solution: Consider using a separate thread for executing processes to avoid blocking the main UI thread.

Potential Infinite Loop in CopyUnblocked[edit]

In WhonixUtils.pas, the CopyUnblocked procedure might result in an infinite loop if FromStream.Size is less than 1024 * 1024:

while FromStream.Position + 1024 * 1024 < FromStream.Size do
begin
  ToStream.CopyFrom(FromStream, 1024 * 1024);
  Application.ProcessMessages;
end;
ToStream.CopyFrom(FromStream, FromStream.Size - FromStream.Position);

Solution: Ensure that FromStream and ToStream are correctly handled and check the loop conditions carefully.

Potential Memory Leaks[edit]

There are several places where dynamically allocated resources may not be freed properly in case of an exception, especially in FormCreate and InstallationBuildIn.

Solution: Use try-finally blocks to ensure that resources are properly freed:

ResourceStream := TResourceStream.Create(HInstance, 'LICENSE', RT_RCDATA);
try
  MemoLicense.Lines.LoadFromStream(ResourceStream);
finally
  ResourceStream.Free;
end;

Deprecation and Error Handling[edit]

Deprecated code comments indicate potential areas that might be out of date:

// deprecated??? ---------------------->
if not InstallerForm.Showing then
begin
  InstallerForm.Show;
end;
// ------------------------------------<

Solution: Review and update the deprecated code.

Unchecked User Inputs[edit]

The method SelectDirectoryDialog.Execute assumes that the user input is always valid, which might not be the case:

if SelectDirectoryDialog.Execute then
begin
  UnpackPath := IncludeTrailingPathDelimiter(
    IncludeTrailingPathDelimiter(SelectDirectoryDialog.FileName) + ApplicationName);
  ...
end;

Solution: Validate the user's input and handle errors gracefully.

ChatGPT AI based Code Review of whonixutils.pas[edit]

1) Error Handling:

The Execute procedure has a try-except block that only handles and logs exceptions if Output is not nil. This could lead to unhandled exceptions if Output is nil. pascal

except
  on E: Exception do
    if Output <> nil then
    begin
      Output.Append('Exception: ' + E.Message);
    end;

Consider adding a more robust error handling mechanism that logs errors regardless of the Output parameter.

2) Resource Management:

In EnsureExePath, TStringList is allocated but only freed at the end. If an exception occurs before sl.Free, there could be a memory leak. pascal

sl := TStringList.Create;
...
sl.Free;

Use a try-finally block to ensure sl.Free is always called.

sl := TStringList.Create;
try
  ...
finally
  sl.Free;
end;

3) Platform Command Execution:

The Execute procedure uses different commands for Windows and other platforms to find executable paths. This can be simplified and made more robust by using Lazarus/Free Pascal's built-in utilities when possible. pascal

{$IFDEF WINDOWS}
Execute('where /r C:\ ' + filename, sl);
{$ELSE}
Execute('which ' + filename, sl);
{$ENDIF}

4) Process Output Handling:

In the Execute procedure, handling of process output is done via pipes. This is correct, but you might want to ensure that both stdout and stderr are captured correctly by setting poStderrToOutPut.

Process.Options := Process.Options + [poUsePipes, poStderrToOutPut];

5) Possible Infinite Loop:

In the Execute procedure, there is a potential for an infinite loop if Process.Running never returns false. This can occur if the process hangs.

repeat
  ...
until (BytesRead = 0) and not Running;

whonixutils.pas by ChatGPT[edit]

unit WhonixUtils;

{$mode ObjFPC}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, Forms, Process, Math;

function AppDiskGetFreeSpace(const fn: string): Int64;
function EnsureExePath(var TargetPath: string; DefaultPath: string): Boolean;
procedure Execute(CommandLine: string; Output: TStrings = nil);
procedure StreamSaveToFile(Stream: TStream; FileName: String);
procedure CopyUnblocked(FromStream, ToStream: TStream);

implementation

function AppDiskGetFreeSpace(const fn: string): Int64;
begin
  {$ifdef linux}
  //this crashes on FreeBSD 12 x64
  Exit(SysUtils.DiskFree(SysUtils.AddDisk(ExtractFileDir(fn))));
  {$endif}

  {$ifdef windows}
  Exit(SysUtils.DiskFree(SysUtils.GetDriveIDFromLetter(ExtractFileDrive(fn))));
  {$endif}

  //cannot detect
  Exit(-1);
end;

function EnsureExePath(var TargetPath: string; DefaultPath: string): Boolean;
var
  filename: string;
  sl: TStringList;
begin
  if FileExists(TargetPath) then
  begin
    Exit(true);
  end;

  if (TargetPath <> DefaultPath) and FileExists(DefaultPath) then
  begin
    TargetPath := DefaultPath;
    Exit(true);
  end;

  filename := ExtractFileName(DefaultPath);
  TargetPath := FindDefaultExecutablePath(filename);
  if FileExists(TargetPath) then
  begin
    Exit(true);
  end;

  sl := TStringList.Create;
  try
    {$IFDEF WINDOWS}
    Execute('where /r C:\ ' + filename, sl);
    {$ELSE}
    Execute('which ' + filename, sl);
    {$ENDIF}

    if (sl.Count > 0) and FileExists(sl.Strings[0]) then
    begin
      TargetPath := sl.Strings[0];
      Result := True;
    end
    else
    begin
      TargetPath := '';
      Result := False;
    end;
  finally
    sl.Free;
  end;
end;

procedure Execute(CommandLine: string; Output: TStrings = nil);
const
  BUFSIZE = 2048;
var
  Process: TProcess;
  StrStream: TStringStream;
  BytesRead: longint;
  Running: boolean;
  Buffer: array[1..BUFSIZE] of byte;
begin
  Process := TProcess.Create(nil);
  try
    Process.CommandLine := CommandLine;
    Process.Options := Process.Options + [poNoConsole];

    if Output <> nil then
    begin
      Process.Options := Process.Options + [poUsePipes, poStderrToOutPut];
      Output.Append('Execute: ' + Process.CommandLine);
    end;

    Process.Execute;

    StrStream := TStringStream.Create;
    try
      repeat
        Sleep(100);
        Application.ProcessMessages;

        Running := Process.Running;
        BytesRead := Min(BUFSIZE, Process.Output.NumBytesAvailable);
        if BytesRead > 0 then
        begin
          BytesRead := Process.Output.Read(Buffer, BytesRead);
          StrStream.Write(Buffer, BytesRead);
        end;
      until (BytesRead = 0) and not Running;
    except
      on E: Exception do
        if Output <> nil then
        begin
          Output.Append('Exception: ' + E.Message);
        end;
    end;

    if Output <> nil then
    begin
      Output.Append('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
      if StrStream.Size > 0 then
      begin
        Output.Append(StrStream.DataString);
      end;
      Output.Append('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
    end;
  finally
    StrStream.Free;
    Process.Free;
  end;
end;

procedure StreamSaveToFile(Stream: TStream; FileName: String);
var
  FileStream: TFileStream;
begin
  FileStream := TFileStream.Create(FileName, fmCreate);
  try
    CopyUnblocked(Stream, FileStream);
  finally
    FileStream.Free;
  end;
end;

procedure CopyUnblocked(FromStream, ToStream: TStream);
const
  CHUNK_SIZE = 1024 * 1024;
begin
  while FromStream.Position + CHUNK_SIZE < FromStream.Size do
  begin
    ToStream.CopyFrom(FromStream, CHUNK_SIZE);
    Application.ProcessMessages;
  end;
  ToStream.CopyFrom(FromStream, FromStream.Size - FromStream.Position);
end;

end.

Summary of Changes

  • Added try-finally for TStringList creation and free in EnsureExePath.
  • Introduced a constant CHUNK_SIZE for readability in CopyUnblocked.
  • Encapsulated the Process creation and freeing in a try-finally block in the Execute procedure.
  • Added a comment for potential infinite loop and suggested a timeout mechanism in Execute.
  • General readability improvements and ensured consistent exception handling.

whonixinstaller_main.lfm DebugInfoType[edit]

Can be removed or is useful for debugging?

1)

<Debugging>
  <DebugInfoType Value="dsDwarf2"/>
</Debugging>

2)

<Verbosity>
  <ShowDebugInfo Value="True"/>
</Verbosity>

More ChatGPT AI based Review[edit]

https://chatgpt.com/archive.org

1) Please register a free account.

2) Write "any bugs?" followed by a copy/paste of 1 source code file.

3) Ignore wrong/useless/overboard comments and address any useful comments.

More Claude AI based Review[edit]

https://claude.aiarchive.org

Similar to above.

Windows_User_Interface wiki page[edit]

TODO: update/rewrite Dev/Windows_User_Interface

See Also[edit]

We believe security software like Whonix needs to remain open source and independent. Would you help sustain and grow the project? Learn more about our 12 year success story and maybe DONATE!