Jelajahi Sumber

Add code signing of installer

Antony Male 10 tahun lalu
induk
melakukan
7d1368c269

+ 2 - 0
.gitignore

@@ -43,3 +43,5 @@ syncthing.exe
 Coverage
 RefitStubs.cs
 SyncTrayzorPortable
+*.pfx
+*.pvk

+ 32 - 3
Rakefile

@@ -7,8 +7,9 @@ rescue LoadError
   exit 1
 end
 
-ISCC = 'C:\Program Files (x86)\Inno Setup 5\ISCC.exe'
-SZIP = 'C:\Program Files\7-Zip\7z.exe'
+ISCC = ENV['ISCC'] || 'C:\Program Files (x86)\Inno Setup 5\ISCC.exe'
+SZIP = ENV['SZIP'] || 'C:\Program Files\7-Zip\7z.exe'
+SIGNTOOL = ENV['SIGNTOOL'] || 'C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe'
 
 CONFIG = ENV['CONFIG'] || 'Release'
 
@@ -16,6 +17,8 @@ SRC_DIR = 'src/SyncTrayzor'
 INSTALLER_DIR = 'installer'
 PORTABLE_DIR = 'portable'
 
+PFX = ENV['PFX'] || File.join(INSTALLER_DIR, 'SyncTrayzorCA.pfx')
+
 class ArchDirConfig
   attr_reader :arch
   attr_reader :bin_dir
@@ -71,6 +74,32 @@ end
 desc 'Build both 64-bit and 32-bit installers'
 task :installer => ARCH_CONFIG.map{ |x| :"installer:#{x.arch}" }
 
+namespace :"sign-installer" do
+  ARCH_CONFIG.each do |arch_config|
+    desc "Sign the installer (#{arch_config.arch}). Specify PASSWORD if required"
+    task arch_config.arch => [:"installer:#{arch_config.arch}"] do
+      unless File.exist?(SIGNTOOL)
+        warn "You must install the Windows SDK"
+        exit 1
+      end
+
+      unless File.exist?(PFX)
+        warn "#{PFX} must exist"
+        exit 1
+      end
+
+      args = ['sign', "/f #{PFX}", "/t http://timestamp.verisign.com/scripts/timstamp.dll"]
+      args << "/p #{ENV['PASSWORD']}" if ENV['PASSWORD']
+      args << "/v #{arch_config.installer_output}"
+
+      sh %Q{"#{SIGNTOOL}"}, *args
+    end
+  end
+end
+
+desc 'Sign both 64-bit and 32-bit installers. Specify PASSWORD if required'
+task :"sign-installer" => ARCH_CONFIG.map{ |x| :"sign-installer:#{x.arch}" }
+
 def cp_to_portable(output_dir, src)
   dest = File.join(output_dir, src)
   # It could be an empty directory - so ignore it
@@ -140,7 +169,7 @@ task :clean => ARCH_CONFIG.map{ |x| :"clean:#{x.arch}" }
 namespace :package do
   ARCH_CONFIG.each do |arch_config|
     desc "Build installer and portable (#{arch_config.arch})"
-    task arch_config.arch => [:"clean:#{arch_config.arch}", :"installer:#{arch_config.arch}", :"portable:#{arch_config.arch}"]
+    task arch_config.arch => [:"clean:#{arch_config.arch}", :"installer:#{arch_config.arch}", :"sign-installer:#{arch_config.arch}", :"portable:#{arch_config.arch}"]
   end
 end
 

+ 1 - 1
installer/certnotes.txt

@@ -3,7 +3,7 @@ http://www.jayway.com/2014/09/03/creating-self-signed-certificates-with-makecert
 
 admin console:
 
-makecert -r -pe -n "CN=TheCN" -a sha256 -cy authority -sky signature -cv MyCA.pvk MyCA.cer
+makecert -r -pe -n "CN=TheCN" -a sha256 -cy authority -sky signature -sv MyCA.pvk MyCA.cer
 pvk2pfx -pvk MyCA.pvk -spc MyCA.cer -pfx MyCA.pfx
 
 Optional: Install MyCA.pfx into cert store by double-clicking it

+ 1 - 0
src/SyncTrayzor/Bootstrapper.cs

@@ -53,6 +53,7 @@ namespace SyncTrayzor
             builder.Bind<IUpdateCheckerFactory>().To<UpdateCheckerFactory>();
             builder.Bind<IUpdatePromptProvider>().To<UpdatePromptProvider>();
             builder.Bind<IUpdateNotificationClientFactory>().To<UpdateNotificationClientFactory>();
+            builder.Bind<IInstallerCertificateVerifier>().To<InstallerCertificateVerifier>().InSingletonScope();
             builder.Bind<IProcessStartProvider>().To<ProcessStartProvider>().InSingletonScope();
             builder.Bind<IFilesystemProvider>().To<FilesystemProvider>().InSingletonScope();
 

TEMPAT SAMPAH
src/SyncTrayzor/Resources/SyncTrayzorCA.cer


+ 59 - 0
src/SyncTrayzor/Services/UpdateManagement/InstallerCertificateVerifier.cs

@@ -0,0 +1,59 @@
+using NLog;
+using SyncTrayzor.Utils;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Services.UpdateManagement
+{
+    public interface IInstallerCertificateVerifier
+    {
+        bool Verify(string filePath);
+    }
+
+    public class InstallerCertificateVerifier : IInstallerCertificateVerifier
+    {
+        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+        private string certThumbprint;
+
+        public InstallerCertificateVerifier(IAssemblyProvider assemblyProvider)
+        {
+            using (var certStream = assemblyProvider.GetManifestResourceStream("SyncTrayzor.Resources.SyncTrayzorCA.cer"))
+            {
+                this.certThumbprint = this.LoadCertificate(certStream).Thumbprint;
+            }
+        }
+
+        private X509Certificate2 LoadCertificate(Stream stream)
+        {
+            using (var ms = new MemoryStream())
+            {
+                stream.CopyTo(ms);
+                return new X509Certificate2(ms.ToArray(), "");
+            }
+        }
+
+        public bool Verify(string filePath)
+        {
+            if (!AuthenticodeTools.VerifyEmbeddedSignature(filePath, true))
+            {
+                logger.Warn("Signature of {0} not valid", filePath);
+                return false;
+            }
+
+            var cert = new X509Certificate2(filePath);
+            if (cert.Thumbprint != this.certThumbprint)
+            {
+                logger.Warn("Thumbprint of download file {0} {1} does not match expected value of {2}", filePath, cert.Thumbprint, this.certThumbprint);
+                return false;
+            }
+
+            return true;
+        }
+    }
+}

+ 26 - 7
src/SyncTrayzor/Services/UpdateManagement/UpdateDownloader.cs

@@ -25,11 +25,13 @@ namespace SyncTrayzor.Services.UpdateManagement
 
         private readonly string downloadsDir;
         private readonly IFilesystemProvider filesystemProvider;
+        private readonly IInstallerCertificateVerifier installerVerifier;
 
-        public UpdateDownloader(IApplicationPathsProvider pathsProvider, IFilesystemProvider filesystemProvider)
+        public UpdateDownloader(IApplicationPathsProvider pathsProvider, IFilesystemProvider filesystemProvider, IInstallerCertificateVerifier installerVerifier)
         {
             this.downloadsDir = pathsProvider.UpdatesDownloadPath;
             this.filesystemProvider = filesystemProvider;
+            this.installerVerifier = installerVerifier;
         }
 
         public async Task<string> DownloadUpdateAsync(string url, Version version)
@@ -41,9 +43,28 @@ namespace SyncTrayzor.Services.UpdateManagement
             if (this.filesystemProvider.Exists(finalPath))
             {
                 logger.Info("Skipping download as final file {0} already exists", finalPath);
-                return finalPath;
+            }
+            else
+            {
+                bool downloaded = await this.TryDownloadToFileAsync(tempPath, finalPath, url);
+                if (!downloaded)
+                    return null;
+            }
+
+            logger.Info("Verifying...");
+
+            if (!this.installerVerifier.Verify(finalPath))
+            {
+                logger.Warn("Download verification failed");
+                this.filesystemProvider.Delete(finalPath);
+                return null;
             }
 
+            return finalPath;
+        }
+
+        private async Task<bool> TryDownloadToFileAsync(string tempPath, string finalPath, string url)
+        {
             // Just in case...
             this.filesystemProvider.CreateDirectory(this.downloadsDir);
 
@@ -77,7 +98,7 @@ namespace SyncTrayzor.Services.UpdateManagement
             catch (IOException e)
             {
                 logger.Warn(String.Format("Failed to initiate download to temp file {0}", tempPath), e);
-                return null;
+                return false;
             }
 
             // Possible, I guess, that the finalPath now exists. If it does, that's fine
@@ -89,14 +110,12 @@ namespace SyncTrayzor.Services.UpdateManagement
             catch (IOException e)
             {
                 logger.Warn(String.Format("Failed to move temp file {0} to final file {1}", tempPath, finalPath), e);
-                return null;
+                return false;
             }
 
             this.filesystemProvider.Delete(tempPath);
 
-            logger.Info("Done");
-
-            return finalPath;
+            return true;
         }
     }
 }

+ 3 - 0
src/SyncTrayzor/SyncTrayzor.csproj

@@ -128,6 +128,7 @@
     <Compile Include="Services\UpdateManagement\InstalledUpdateVariantHandler.cs" />
     <Compile Include="Services\UpdateManagement\IUpdateNotificationApi.cs" />
     <Compile Include="Services\UpdateManagement\IUpdateVariantHandler.cs" />
+    <Compile Include="Services\UpdateManagement\InstallerCertificateVerifier.cs" />
     <Compile Include="Services\UpdateManagement\UpdateCheckerFactory.cs" />
     <Compile Include="Services\UpdateManagement\UpdateDownloader.cs" />
     <Compile Include="Services\UpdateManagement\UpdateManager.cs" />
@@ -227,6 +228,7 @@
     <Compile Include="SyncThing\SyncThingProcessRunner.cs" />
     <Compile Include="SyncThing\SyncThingState.cs" />
     <Compile Include="SyncThing\SyncThingStateChangedEventArgs.cs" />
+    <Compile Include="Utils\AuthenticodeTools.cs" />
     <Compile Include="Utils\Buffer.cs" />
     <Compile Include="Utils\EnvVarTransformer.cs" />
     <Compile Include="Utils\FluentModelValidator.cs" />
@@ -261,6 +263,7 @@
       <LastGenOutput>Settings.Designer.cs</LastGenOutput>
     </None>
     <AppDesigner Include="Properties\" />
+    <EmbeddedResource Include="Resources\SyncTrayzorCA.cer" />
   </ItemGroup>
   <ItemGroup>
     <None Include="App.config">

+ 168 - 0
src/SyncTrayzor/Utils/AuthenticodeTools.cs

@@ -0,0 +1,168 @@
+using NLog;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SyncTrayzor.Utils
+{
+    // From http://www.pinvoke.net/default.aspx/wintrust.winverifytrust
+    public static class AuthenticodeTools
+    {
+        private static readonly Logger logger = LogManager.GetCurrentClassLogger();
+
+        private static readonly IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1);
+        // GUID of the action to perform
+        private const string WINTRUST_ACTION_GENERIC_VERIFY_V2 = "{00AAC56B-CD44-11d0-8CC2-00C04FC295EE}";
+
+        [DllImport("wintrust.dll", ExactSpelling = true, SetLastError = false, CharSet = CharSet.Unicode)]
+        private static extern WinVerifyTrustResult WinVerifyTrust(
+            [In] IntPtr hwnd,
+            [In] [MarshalAs(UnmanagedType.LPStruct)] Guid pgActionID,
+            [In] WinTrustData pWVTData
+        );
+
+        // call WinTrust.WinVerifyTrust() to check embedded file signature
+        public static bool VerifyEmbeddedSignature(string fileName, bool allowInvalidRoot)
+        {
+            WinTrustData wtd = new WinTrustData(fileName);
+            Guid guidAction = new Guid(WINTRUST_ACTION_GENERIC_VERIFY_V2);
+            WinVerifyTrustResult result = WinVerifyTrust(INVALID_HANDLE_VALUE, guidAction, wtd);
+            logger.Info("Result: {0}", result);
+            bool ret = (result == WinVerifyTrustResult.Success || (allowInvalidRoot && result == WinVerifyTrustResult.UntrustedRoot));
+            return ret;
+        }
+
+        private enum WinTrustDataUIChoice : uint
+        {
+            All = 1,
+            None = 2,
+            NoBad = 3,
+            NoGood = 4
+        }
+
+        private enum WinTrustDataRevocationChecks : uint
+        {
+            None = 0x00000000,
+            WholeChain = 0x00000001
+        }
+
+        private enum WinTrustDataChoice : uint
+        {
+            File = 1,
+            Catalog = 2,
+            Blob = 3,
+            Signer = 4,
+            Certificate = 5
+        }
+
+        private enum WinTrustDataStateAction : uint
+        {
+            Ignore = 0x00000000,
+            Verify = 0x00000001,
+            Close = 0x00000002,
+            AutoCache = 0x00000003,
+            AutoCacheFlush = 0x00000004
+        }
+
+        [Flags]
+        private enum WinTrustDataProvFlags : uint
+        {
+            UseIe4TrustFlag = 0x00000001,
+            NoIe4ChainFlag = 0x00000002,
+            NoPolicyUsageFlag = 0x00000004,
+            RevocationCheckNone = 0x00000010,
+            RevocationCheckEndCert = 0x00000020,
+            RevocationCheckChain = 0x00000040,
+            RevocationCheckChainExcludeRoot = 0x00000080,
+            SaferFlag = 0x00000100,        // Used by software restriction policies. Should not be used.
+            HashOnlyFlag = 0x00000200,
+            UseDefaultOsverCheck = 0x00000400,
+            LifetimeSigningFlag = 0x00000800,
+            CacheOnlyUrlRetrieval = 0x00001000,      // affects CRL retrieval and AIA retrieval
+            DisableMD2andMD4 = 0x00002000      // Win7 SP1+: Disallows use of MD2 or MD4 in the chain except for the root
+        }
+
+        private enum WinTrustDataUIContext : uint
+        {
+            Execute = 0,
+            Install = 1
+        }
+
+        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+        private class WinTrustFileInfo
+        {
+            UInt32 StructSize = (UInt32)Marshal.SizeOf(typeof(WinTrustFileInfo));
+            IntPtr pszFilePath;                     // required, file name to be verified
+            IntPtr hFile = IntPtr.Zero;             // optional, open handle to FilePath
+            IntPtr pgKnownSubject = IntPtr.Zero;    // optional, subject type if it is known
+
+            public WinTrustFileInfo(String _filePath)
+            {
+                pszFilePath = Marshal.StringToCoTaskMemAuto(_filePath);
+            }
+            ~WinTrustFileInfo()
+            {
+                Marshal.FreeCoTaskMem(pszFilePath);
+            }
+        }
+
+        [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
+        private class WinTrustData
+        {
+            UInt32 StructSize = (UInt32)Marshal.SizeOf(typeof(WinTrustData));
+            IntPtr PolicyCallbackData = IntPtr.Zero;
+            IntPtr SIPClientData = IntPtr.Zero;
+            // required: UI choice
+            WinTrustDataUIChoice UIChoice = WinTrustDataUIChoice.None;
+            // required: certificate revocation check options
+            WinTrustDataRevocationChecks RevocationChecks = WinTrustDataRevocationChecks.None;
+            // required: which structure is being passed in?
+            WinTrustDataChoice UnionChoice = WinTrustDataChoice.File;
+            // individual file
+            IntPtr FileInfoPtr;
+            WinTrustDataStateAction StateAction = WinTrustDataStateAction.Ignore;
+            IntPtr StateData = IntPtr.Zero;
+            String URLReference = null;
+            WinTrustDataProvFlags ProvFlags = WinTrustDataProvFlags.RevocationCheckChainExcludeRoot;
+            WinTrustDataUIContext UIContext = WinTrustDataUIContext.Execute;
+
+            // constructor for silent WinTrustDataChoice.File check
+            public WinTrustData(String _fileName)
+            {
+                // On Win7SP1+, don't allow MD2 or MD4 signatures
+                if ((Environment.OSVersion.Version.Major > 6) ||
+                    ((Environment.OSVersion.Version.Major == 6) && (Environment.OSVersion.Version.Minor > 1)) ||
+                    ((Environment.OSVersion.Version.Major == 6) && (Environment.OSVersion.Version.Minor == 1) && !String.IsNullOrEmpty(Environment.OSVersion.ServicePack)))
+                {
+                    ProvFlags |= WinTrustDataProvFlags.DisableMD2andMD4;
+                }
+
+                WinTrustFileInfo wtfiData = new WinTrustFileInfo(_fileName);
+                FileInfoPtr = Marshal.AllocCoTaskMem(Marshal.SizeOf(typeof(WinTrustFileInfo)));
+                Marshal.StructureToPtr(wtfiData, FileInfoPtr, false);
+            }
+            ~WinTrustData()
+            {
+                Marshal.FreeCoTaskMem(FileInfoPtr);
+            }
+        }
+
+        private enum WinVerifyTrustResult : uint
+        {
+            Success = 0,
+            ProviderUnknown = 0x800b0001,           // Trust provider is not recognized on this system
+            ActionUnknown = 0x800b0002,         // Trust provider does not support the specified action
+            SubjectFormUnknown = 0x800b0003,        // Trust provider does not support the form specified for the subject
+            SubjectNotTrusted = 0x800b0004,         // Subject failed the specified verification action
+            FileNotSigned = 0x800B0100,         // TRUST_E_NOSIGNATURE - File was not signed
+            SubjectExplicitlyDistrusted = 0x800B0111,   // Signer's certificate is in the Untrusted Publishers store
+            SignatureOrFileCorrupt = 0x80096010,    // TRUST_E_BAD_DIGEST - file was probably corrupt
+            SubjectCertExpired = 0x800B0101,        // CERT_E_EXPIRED - Signer's certificate was expired
+            SubjectCertificateRevoked = 0x800B010C,     // CERT_E_REVOKED Subject's certificate was revoked
+            UntrustedRoot = 0x800B0109          // CERT_E_UNTRUSTEDROOT - A certification chain processed correctly but terminated in a root certificate that is not trusted by the trust provider.
+        }
+    }
+}