42

I have a project for which I have built a WiX msi file. I also have a WiX bootstrapper (exe file) that checks for the existence of C++ 2005, installs it if not found and then installs the msi package. My project includes Crystal Reports as an msm file which is installed with the msi but requires C++ 2005 to install properly.

On the MSI project I have included the following post build event to digitally sign the msi file.

  sign  /f "$(ProjectDir)\myPFXFile.pfx" /p mySecretKey/d "My Program" /t http://timestamp.verisign.com/scripts/timstamp.dll /v "MyProgram.msi"

If I install just the msi it correctly identifies the Publisher when requesting elevated permission to install.

I have tried adding the same post-build event to the bootstrapper project as follows:

  sign  /f "$(ProjectDir)\myPFXFile.pfx" /p mySecretKey/d "My Program" /t http://timestamp.verisign.com/scripts/timstamp.dll /v "MyProgram Setup.exe"

When I attempt to install the exe file it correctly identifies the Publisher but then fails to install with the following from the log file:

[1604:2574][2013-12-04T11:49:51]i001: Burn v3.7.1224.0, Windows v6.2 (Build 9200: Service Pack 0), path: C:\Users\.....\MyProgram Setup.exe, cmdline: ''
[1604:2574][2013-12-04T11:49:51]i000: Setting string variable 'WixBundleLog' to value 'C:\Users\.....\MyProgram_20131204114951.log'
[1604:2574][2013-12-04T11:49:51]i000: Setting string variable 'WixBundleOriginalSource' to value 'C:\Users\.....\MyProgram Setup.exe'
[1604:2574][2013-12-04T11:49:51]i000: Setting string variable 'WixBundleName' to value 'MyProgram'
[1604:2574][2013-12-04T11:49:51]i100: Detect begin, 2 packages
[1604:2574][2013-12-04T11:49:51]i000: Setting string variable 'vcredist_x86' to value '1'
[1604:2574][2013-12-04T11:49:51]i000: Setting string variable 'vcredist_x64' to value '1'
[1604:2574][2013-12-04T11:49:51]i052: Condition 'vcredist_x86 AND (vcredist_x86 >= 1)' evaluates to true.
[1604:2574][2013-12-04T11:49:51]i101: Detected package: vcredist_x86, state: Present, cached: None
[1604:2574][2013-12-04T11:49:51]i101: Detected package: MyProgram, state: Absent, cached: None
[1604:2574][2013-12-04T11:49:51]i199: Detect complete, result: 0x0
[1604:2574][2013-12-04T11:49:53]i200: Plan begin, 2 packages, action: Install
[1604:2574][2013-12-04T11:49:53]w321: Skipping dependency registration on package with no dependency providers: vcredist_x86
[1604:2574][2013-12-04T11:49:53]i000: Setting string variable 'WixBundleRollbackLog_MyProgram' to value 'C:\Users\.....\MyProgram_20131204114951_0_MyProgram_rollback.log'
[1604:2574][2013-12-04T11:49:53]i000: Setting string variable 'WixBundleLog_MyProgram' to value 'C:\Users\.....\MyProgram_20131204114951_0_MyProgram.log'
[1604:2574][2013-12-04T11:49:53]i201: Planned package: vcredist_x86, state: Present, default requested: Present, ba requested: Present, execute: None, rollback: None, cache: No, uncache: No, dependency: None
[1604:2574][2013-12-04T11:49:53]i201: Planned package: MyProgram, state: Absent, default requested: Present, ba requested: Present, execute: Install, rollback: Uninstall, cache: Yes, uncache: No, dependency: Register
[1604:2574][2013-12-04T11:49:53]i299: Plan complete, result: 0x0
[1604:2574][2013-12-04T11:49:53]i300: Apply begin
[1FF8:10F8][2013-12-04T11:49:58]i360: Creating a system restore point.
[1FF8:10F8][2013-12-04T11:49:59]i361: Created a system restore point.
[1FF8:10F8][2013-12-04T11:50:00]i000: Caching bundle from: 'C:\Users\.....\{6ab8eece-89c6-4417-905f-6d9c5136519d}\.be\MyProgram Setup.exe' to: 'C:\ProgramData\Package Cache\{6ab8eece-89c6-4417-905f-6d9c5136519d}\MyProgram Setup.exe'
[1FF8:10F8][2013-12-04T11:50:00]i320: Registering bundle dependency provider: {6ab8eece-89c6-4417-905f-6d9c5136519d}, version: 2.0.0.0
[1604:2FB4][2013-12-04T11:50:00]i336: Acquiring container: WixAttachedContainer, copy from: C:\Users\.....\MyProgram Setup.exe
[1604:2FB4][2013-12-04T11:50:00]i000: Setting string variable 'WixBundleLastUsedSource' to value 'C:\Users\.....'
[1604:24F8][2013-12-04T11:50:00]e000: Error 0x80004005: Failed to extract all files from container.
[1604:2FB4][2013-12-04T11:50:00]e000: Error 0x80004005: Failed to wait for operation complete.
[1604:2FB4][2013-12-04T11:50:00]e000: Error 0x80004005: Failed to open container.
[1604:2FB4][2013-12-04T11:50:00]e000: Error 0x80004005: Failed to open container: WixAttachedContainer.
[1604:2FB4][2013-12-04T11:50:00]e312: Failed to extract payloads from container: WixAttachedContainer to working path: C:\Users\.....\{6ab8eece-89c6-4417-905f-6d9c5136519d}\C7C1FB4E513C19E0F5E8F6856FF2ACC4D7D143A2, error: 0x80004005.
[1604:2574][2013-12-04T11:50:00]e000: Error 0x80004005: Failed while caching, aborting execution.
[1FF8:10F8][2013-12-04T11:50:00]i330: Removed bundle dependency provider: {6ab8eece-89c6-4417-905f-6d9c5136519d}
[1FF8:10F8][2013-12-04T11:50:00]i352: Removing cached bundle: {6ab8eece-89c6-4417-905f-6d9c5136519d}, from path: C:\ProgramData\Package Cache\{6ab8eece-89c6-4417-905f-6d9c5136519d}\
[1604:2574][2013-12-04T11:50:00]i399: Apply complete, result: 0x80004005, restart: None, ba requested restart:  No

I then found another alternative to signing the exe by adding the following to the end of the .wixproj file:

  <Target Name="SignBundleEngine">
    <Exec Command="&quot;C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin\signtool.exe&quot; sign /f &quot;$(ProjectDir)\sigFile.pfx&quot; /p sigKey /d &quot;My Program&quot; /t http://timestamp.verisign.com/scripts/timstamp.dll &quot;@(SignBundleEngine)&quot;" />
  </Target>
  <Target Name="SignBundle">
    <Exec Command="&quot;C:\Program Files (x86)\Microsoft SDKs\Windows\v7.0A\bin\signtool.exe&quot; sign /f &quot;$(ProjectDir)\sigFile.pfx&quot; /p sigKey /d &quot;My Program&quot; /t http://timestamp.verisign.com/scripts/timstamp.dll &quot;@(SignBundle)&quot;" />
  </Target>
  <PropertyGroup>
    <PostBuildEvent>"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe"  sign  /f "$(ProjectDir)\sigFile.pfx" /p sigKey /d "My Program" /t http://timestamp.verisign.com/scripts/timstamp.dll /v "MyProgram Setup.exe"</PostBuildEvent>
  </PropertyGroup>

Using this method the setup file executes and installs everything correctly but doesn't identify the publisher when requesting elevated permission to install, it says "Publisher: Unknown".

Anyone know how to get the digital signing to work on this bootstrapper?

Here is my Bundle.wxs and vcredist.wxs files:

Bundle.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">


  <Bundle Name="My Program"
          Version="2.0.0"
          Manufacturer="My Company"
          UpgradeCode="PUT-GUID-HERE"
          HelpUrl="http://www.mycompany.com"
          AboutUrl="http://www.mycompany.com"
          HelpTelephone="888 888 8888"
          IconSourceFile="Resources\program.ico">

    <BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.HyperlinkLicense">
      <Payload SourceFile="Resources\Bootstrapper Screen.png" />
    </BootstrapperApplicationRef>

    <WixVariable Id="WixStdbaLicenseUrl" Value=""/>

    <WixVariable Id="WixStdbaThemeXml" Value="Resources\CustomHyperlinkTheme.xml"/>
    <WixVariable Id="WixStdbaThemeWxl" Value="Resources\CustomHyperlinkTheme.wxl"/>

    <Chain>

      <!-- Define the list of chained packages. -->
      <PackageGroupRef Id="vcredist"/>
      <MsiPackage Id="MyProgram"
                  SourceFile="$(var.MyProgramSetup.TargetPath)"
                  ForcePerMachine="yes" />
    </Chain>
  </Bundle>
</Wix>

vcredist.wxs

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
     xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
  <Fragment>
    <util:RegistrySearch Root="HKLM" Key="SOFTWARE\Microsoft\VisualStudio\10.0\VC\VCRedist\x86" Value="Installed" Variable="vcredist_x86" />
    <util:RegistrySearch Root="HKLM" Key="SOFTWARE\Microsoft\VisualStudio\10.0\VC\VCRedist\x64" Value="Installed" Variable="vcredist_x64" />
    <PackageGroup Id="vcredist">
      <ExePackage Id="vcredist_x86"
        Cache="no"
        Compressed="yes"
        PerMachine="yes"
        Permanent="yes"
        Vital="yes"
        Name="vcredist_x86.exe"
        SourceFile="vcredist3.5_x86.exe"
        InstallCommand="/q"
        DetectCondition="vcredist_x86 AND (vcredist_x86 &gt;= 1)">
      </ExePackage>
    </PackageGroup>
  </Fragment>
</Wix>
Stein Åsmul
  • 39,960
  • 25
  • 91
  • 164
BrianKE
  • 4,035
  • 13
  • 65
  • 115
  • How do your post build events know how to sign the file without a reference there to signtool.exe? Is something missing here? – karfus Apr 15 '16 at 15:12

4 Answers4

39
  <Target Name="UsesFrameworkSdk">
    <GetFrameworkSdkPath>
      <Output TaskParameter="Path" PropertyName="FrameworkSdkPath" />
    </GetFrameworkSdkPath>
    <PropertyGroup>
      <Win8SDK>$(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.0@InstallationFolder)</Win8SDK>
    </PropertyGroup>    
  </Target>

  <Target Name="UsesSignTool" DependsOnTargets="UsesFrameworkSdk">
    <PropertyGroup>
      <SignToolPath Condition="('@(SignToolPath)'=='') and Exists('$(FrameworkSdkPath)bin\signtool.exe')">$(FrameworkSdkPath)bin\signtool.exe</SignToolPath>
      <SignToolPath Condition="('@(SignToolPath)'=='') and Exists('$(Win8SDK)\bin\x86\signtool.exe')">$(Win8SDK)\bin\x86\signtool.exe</SignToolPath>
    </PropertyGroup>
  </Target>

  <Target Name="SignBundleEngine" DependsOnTargets="UsesSignTool">
    <Exec Command="&quot;$(SignToolPath)&quot; sign /d &quot;App Setup&quot; /t http://timestamp.digicert.com /a &quot;@(SignBundleEngine)&quot;" />
  </Target>

  <Target Name="SignBundle" DependsOnTargets="UsesSignTool">
    <Exec Command="&quot;$(SignToolPath)&quot; sign /d &quot;App Setup&quot; /t http://timestamp.digicert.com /a &quot;@(SignBundle)&quot;" />
  </Target>

This works well for me. Either you do it during the build, or you need to use insignia.
Ex: http://wixtoolset.org/documentation/manual/v3/overview/insignia.html

insignia -ib bundle.exe -o engine.exe
... sign engine.exe
insignia -ab engine.exe bundle.exe -o bundle.exe
... sign bundle.exe
Patrick Hofman
  • 153,850
  • 22
  • 249
  • 325
jchoover
  • 668
  • 6
  • 10
  • I tried what you have above with one minor mod to accommodate my environment: point to ...Windows\v7.1A as I don't have a ...\v8.0 on my machine and v7.1.A is where signtool.exe resides. I get a similar error message as before of "The command """ sign /d "App Setup" /t http://timestamp.digicert.com /a "obj\Debug\MyProgram.exe"" exited with code 9009.". I am running Windows 8.1, do I need a newer version of signtool perhaps? – BrianKE Dec 09 '13 at 16:06
  • So you are having issues finding the installed SDK. You need to either tweak the UsesFrameworkSDK target to probe to find yours, or optionally hard code the path (yuck). – jchoover Dec 09 '13 at 16:08
  • I hardcoded the to C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe and now the project builds, EXE is digitally signed and installs correctly. The only nagging issue I have is that during installation when prompted for elevated permission the prompt shows the Publisher as "Unknown" instead of showing my company name. Any idea how to fix this? – BrianKE Dec 09 '13 at 16:25
  • I assume the cert you are using from a trusted root? Also, did you kill your PostBuildEvent property? In order for the UAC prompt to show you as the publisher, the engine needs to be signed. Once you had the path hard coded, you might want to try a forced rebuild to ensure both targets are being executed successfully. – jchoover Dec 09 '13 at 16:29
  • The cert is from Comodo and is current. I did remove the Post Build Event. I have cleaned and rebuilt the project and it still Shows "Publisher: Unknown". I thought there might be an issue with the VC++ 2005 package so I commented out that package but no luck. One thing I did notice is that the MSI file I created correctly shows the signer as "My Company" but the EXE file shows "benderle" as the signer. – BrianKE Dec 09 '13 at 17:02
  • Hmm, you say it's from Comodo buy your pointing at Verisign for your time stamping. Not sure if it matters but I would try to follow the recommendations of your cert provider which would probably provide their own time stamping server. Also, can you run the command against the bundle from the command line, and verify the signature in explorer? Until you can get the manual signing working, it's best to keep the MSBuild side out of the equation. – jchoover Dec 09 '13 at 17:17
  • I can try building from the command line but can you tell me what I use in place of @(SignBuildEngine) and @(SignBuild) (I assume this one is MyProgramSetup.EXE)? – BrianKE Dec 09 '13 at 18:44
  • I found my issue with this, I had not added the credentials for the signature file to the signing tasks. Thanks for all the help! – BrianKE Dec 09 '13 at 20:01
  • For what it's worth, we only sign our release builds and our cert is only on our build server. As such, we add it to the cert storage and skip the messy handling of the cert file. That cert is your company identity, protect it well. – jchoover Dec 10 '13 at 15:25
  • 1
    Could you please provide the parent element with with your xml? – JDennis May 14 '14 at 13:55
  • Alos, make sure `MsiPackage Compressed="true"`. When left out out I also get the "unknown publisher" behaviour. – William Jul 05 '15 at 02:15
  • 19
    It may be obvious to most but for me there was a crucial bit of information left out here...the "SignBundleEngine" and "SignBundle" targets won't do anything unless you add a SignOutput property to the project file as well, e.g. true – karfus Apr 15 '16 at 15:09
  • +1 for the insignia method. I put this into into a post-build event in the a Visual Studio WiX bootstraper project: insignia -ib !(TargetPath) -o engine.exe signtool sign .... engine.exe insignia -ab engine.exe !(TargetPath) -o !(TargetPath) signtool sign /a /n .... !(TargetPath) – Daniel Aug 12 '16 at 18:16
  • @jchoover, I think hard-coding the signtool path is no worse than hard-coding a registry string which is not necessarily pointing at the latest installed version. May as well hard code it! In addition, the registry read doesn't seem to work in the latest wIX - I get an error `Undefined preprocessor variable '$(registry:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Microsoft SDKs\Windows\v8.1A@InstallationFolder)'` – noelicus Nov 22 '16 at 14:02
  • I second the comment about parent element in the XML. The target elements (all of them, actually) are undefined in the latest manual. So none of the answers here provide any real answers to the original question. Also, what is engine.exe? http://wixtoolset.org/documentation/manual/v3/ – Andrew Jay Jun 18 '18 at 19:50
29

For me using WiX's in-built tool insignia is the most straight-forward. Here's the steps I made to do code-sign a WiX MSI and bootstrap installer:
(steps 1 & 2 are just set up to make 3 & 4 read easy and more reusable and updatable! Steps 3 & 4 are the actual signing)

  1. Set up the signtool as a batch file in my PATH so that I can call it and change it easily. I'm running Windows 10 and so my "signtool.bat" looks like this:
    "c:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe" %*
  2. Set up insignia as a batch file in my PATH too so you can change it with new WiX builds as they come. My "insignia.bat" looks like this:
    "C:\Program Files (x86)\WiX Toolset v3.10\bin\insignia.exe" %*
  3. Sign my MSI in a post-build event (MSI Project -> Properties -> Build Events) by calling this:
    signtool sign /f "c:\certificates\mycert.pfx" /p cert-password /d "Your Installer Label" /t http://timestamp.verisign.com/scripts/timstamp.dll /v $(TargetFileName)
  4. Sign my bundle in a post-build event for the bootstrap project like this:

    CALL insignia -ib "$(TargetFileName)" -o engine.exe
    CALL signtool sign /f "c:\certificates\mycert.pfx" /p cert-password /d "Installer Name" /t http://timestamp.verisign.com/scripts/timstamp.dll /v engine.exe
    CALL insignia -ab engine.exe "$(TargetFileName)" -o "$(TargetFileName)"
    CALL signtool sign /f "c:\certificates\mycert.pfx" /p cert-password /d "Installer Name" /t http://timestamp.verisign.com/scripts/timstamp.dll /v "$(TargetFileName)"


Further notes and thoughts:

  • I have also signed the application (I think) by just doing Project Properties -> Signing and enabling click-once manifests, selecting the certificate and checking the Sign the assembly option.

  • Specifying CALL is necessary in post-build events when calling a batch file or only the first one gets called.

noelicus
  • 14,468
  • 3
  • 92
  • 111
  • 4
    If you're getting this error when trying to sign engine.exe: `SignedCode::Sign returned error: 0x800700C1`, make sure your $(TargetFileName) isn't signed first. – Josh Noe Jan 31 '18 at 22:32
  • 1
    Josh, you pointed me to the right direction. My msbuild script was not cleaning the Release folder and Wix was skipping the compile target as there was no changes. – Frederico Almeida Jun 19 '18 at 10:27
  • 1
    this helps me. I used nmake to manage wix project. – Lucas Aug 03 '22 at 10:38
  • Do we need to purchase the certificate for signing the MSI? – Tapas Ranjan Singh Aug 24 '22 at 15:04
  • @TapasRanjanSingh if you want to distribute it for users, yes you do. If you just want to sign it for testing you can create a developer certificate. – noelicus Aug 24 '22 at 16:34
13

Updating for VS2019, and based on @jchoover's answer, here is what I got working.

This leverages some MSBuild property function work by @webjprgm here that makes finding signtool.exe more generic across Windows Kit versions. As mentioned by @karfus in a comment above, adding the SignOutput section is the incantation that kicks everything off.

This goes at the end of your bootstrap.wixproj file, before the closing /Project tag.

  <!-- SignOutput must be present in some PropertyGroup to trigger signing. -->
  <PropertyGroup> 
    <SignOutput>true</SignOutput>
  </PropertyGroup>

  <!-- Find Windows Kit path and then SignTool path for the post-build event -->
  <Target Name="FindSignTool">
      <PropertyGroup>
        <WindowsKitsRoot>$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots', 'KitsRoot10', null, RegistryView.Registry32, RegistryView.Default))</WindowsKitsRoot>
        <WindowsKitsRoot Condition="'$(WindowsKitsRoot)' == ''">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots', 'KitsRoot81', null, RegistryView.Registry32, RegistryView.Default))</WindowsKitsRoot>
        <WindowsKitsRoot Condition="'$(WindowsKitsRoot)' == ''">$([MSBuild]::GetRegistryValueFromView('HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows Kits\Installed Roots', 'KitsRoot', null, RegistryView.Registry32, RegistryView.Default))</WindowsKitsRoot>
        <SignToolPath Condition="'$(SignToolPath)' == '' And '$(Platform)' == 'AnyCPU' and Exists('$(WindowsKitsRoot)bin\x64\signtool.exe')">$(WindowsKitsRoot)bin\x64\</SignToolPath>
        <SignToolPath Condition="'$(SignToolPath)' == '' And Exists('$(WindowsKitsRoot)bin\$(Platform)\signtool.exe')">$(WindowsKitsRoot)bin\$(Platform)\</SignToolPath>
        <SignToolPathBin Condition="'$(SignToolPath)' == ''">$([System.IO.Directory]::GetDirectories('$(WindowsKitsRoot)bin',"10.0.*"))</SignToolPathBin>
        <SignToolPathLen Condition="'$(SignToolPathBin)' != ''">$(SignToolPathBin.Split(';').Length)</SignToolPathLen>
        <SignToolPathIndex Condition="'$(SignToolPathLen)' != ''">$([MSBuild]::Add(-1, $(SignToolPathLen)))</SignToolPathIndex>
        <SignToolPathBase Condition="'$(SignToolPathIndex)' != ''">$(SignToolPathBin.Split(';').GetValue($(SignToolPathIndex)))\</SignToolPathBase>
        <SignToolPath Condition="'$(SignToolPath)' == '' And '$(SignToolPathBase)' != '' And '$(Platform)' == 'AnyCPU'">$(SignToolPathBase)x64\</SignToolPath>
        <SignToolPath Condition="'$(SignToolPath)' == '' And '$(SignToolPathBase)' != ''">$(SignToolPathBase)$(Platform)\</SignToolPath>
      </PropertyGroup>
  </Target>

  <!-- Sign the bundle engine -->
  <Target Name="SignBundleEngine" DependsOnTargets="FindSignTool">
    <Exec Command="&quot;$(SignToolPath)signtool.exe&quot; sign /d &quot;MyApp Setup&quot; /fd SHA256 /td SHA256 /a /f &quot;MyApp Code Certificate.pfx&quot; /p CertPassword /tr http://timestamp.digicert.com /a &quot;@(SignBundleEngine)&quot;" />
  </Target>

  <!-- Sign the final bundle -->
  <Target Name="SignBundle" DependsOnTargets="FindSignTool">
    <Exec Command="&quot;$(SignToolPath)signtool.exe&quot; sign /d &quot;MyApp Setup&quot;  /fd SHA256 /td SHA256 /a /f &quot;MyApp Code Certificate.pfx&quot; /p CertPassword /tr http://timestamp.digicert.com /a &quot;@(SignBundle)&quot;" />
  </Target>
entiat
  • 473
  • 5
  • 9
  • This works but the problem is you are including a signing certificate password in a file that would normally get checked into source control. Any ideas how to externally reference this password? – Tarazed Oct 04 '21 at 21:49
  • Nevermind, I learned how to add my cert to my personal store using Certmgr.exe. That solved my problem. – Tarazed Oct 05 '21 at 16:16
10

Further @jchoover's answer, you have 3 options when signing bundles:

  • Build the bundle unsigned, then sign it later. However, you also need to sign the engine exe which is embedded within the bundle. As @jchoover states, you can use insignia to get around this by extracting the engine to a file. You can then sign the file using your normal process (for example, with signtool.exe) and then import it back into the bundle

  • Add the SignBundle and SignBundleEngine targets to your project(s). You can do this by opening them up in a text editor, and editing the underlying MSBuild code. @jchoover's answer describes how you can do this.

  • Create a .targets file with the SignBundle and SignBundleEngine targets, and passing the path using the CustomAfterWixTargets property:

    msbuild your.sln /p:CustomAfterWixTargets=customafterwix.targets /p:SignOutput=true

Chris
  • 839
  • 1
  • 12
  • 30
JohnL
  • 3,922
  • 3
  • 22
  • 22