Almost there...

Last time (which was only yesterday), I made a few more strides towards signing cabs in a manner consistent with what was done in VS 2003 (except, of course, using SignTool instead of SignCode). Along the way, I tried to introduce output from MSBuild tasks. I think I have just about finished up most of what I want to do (at least regarding introduction of MSBuild features), but there are still a couple of holes in the story.

One problem I had was with the name of the digital certificate. I had hard-coded this value to "Test", which was consitent with the vbscript that deals with signing, but not consistent what was done in VS 2003. This could cause problems when the installer creates multiple cabs, and the task writes certificate keys with the same name into the MsiDigitalCertificate and MsiDigitalSignature tables. Also, VS 2003 populated the Hash column of the MsiDigitalSignature table. Both of these issues are easily addressed. The game plan:

  1. The DigitalCertificate value will be an underscore followed by the disk id
  2. The hash information can be found by using FileSignatureInfo in a manner similar to getting the certificate information

Issue 1 is easy: we need to only change a couple of records to reflect the DiskIdbeing used:

 string media = "1";
if (!string.IsNullOrEmpty(cabinet.GetMetadata("DiskId")))
{
  media = cabinet.GetMetadata("DiskId");
}
string digitalCertificate = string.Format("_{0}", media);

Record recordCert = installer.CreateRecord(2);
recordCert.set_StringData(1, digitalCertificate);
recordCert.SetStream(2, certificateFileName);
viewCert.Modify(MsiViewModify.msiViewModifyInsert, recordCert);

Record recordSig = installer.CreateRecord(4);
recordSig.set_StringData(1, "Media");
recordSig.set_StringData(2, media);
recordSig.set_StringData(3, digitalCertificate);
viewSig.Modify(MsiViewModify.msiViewModifyInsert, recordSig);

Issue 2 is also pretty easy: we pretty much need to copy the code written for dealing with the certificate and reuse it to deal with the file hash. I have included the entire Execute method below:

 public override bool Execute()
{
    string certificateFileName = string.Empty;
  string hashFileName = string.Empty;
 try
 {
       Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
      Object installerClassObject = Activator.CreateInstance(classType);
      Installer installer = (Installer)installerClassObject;

      Database msi = installer.OpenDatabase(Database, MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
       View viewCert = msi.OpenView("SELECT * FROM `MsiDigitalCertificate`");
      viewCert.Execute(null);
     View viewSig = msi.OpenView("SELECT * FROM `MsiDigitalSignature`");
     viewSig.Execute(null);

      foreach (ITaskItem cabinet in Cabinets)
     {
           Array certificateArray = installer.FileSignatureInfo(Path.Combine(Path.GetDirectoryName(Database), cabinet.ItemSpec), 0, MsiSignatureInfo.msiSignatureInfoCertificate);
         certificateFileName = Path.GetTempFileName();
           using (BinaryWriter fout = new BinaryWriter(new FileStream(certificateFileName, FileMode.Open)))
            {
               fout.Write((byte[])certificateArray);
           }

           Array hashArray = installer.FileSignatureInfo(Path.Combine(Path.GetDirectoryName(Database), cabinet.ItemSpec), 0, MsiSignatureInfo.msiSignatureInfoHash);
           hashFileName = Path.GetTempFileName();
          using (BinaryWriter fout = new BinaryWriter(new FileStream(hashFileName, FileMode.Open)))
           {
               fout.Write((byte[])hashArray);
          }

           string media = "1";
         if (!string.IsNullOrEmpty(cabinet.GetMetadata("DiskId")))
           {
               media = cabinet.GetMetadata("DiskId");
          }
           string digitalCertificate = string.Format("_{0}", media);

           Record recordCert = installer.CreateRecord(2);
          recordCert.set_StringData(1, digitalCertificate);
           recordCert.SetStream(2, certificateFileName);
           viewCert.Modify(MsiViewModify.msiViewModifyInsert, recordCert);

         Record recordSig = installer.CreateRecord(5);
           recordSig.set_StringData(1, "Media");
           recordSig.set_StringData(2, media);
         recordSig.set_StringData(3, digitalCertificate);
            recordSig.SetStream(4, hashFileName);
           viewSig.Modify(MsiViewModify.msiViewModifyInsert, recordSig);

           if (!string.IsNullOrEmpty(certificateFileName) && File.Exists(certificateFileName))
         {
               File.Delete(certificateFileName);
           }
           if (!string.IsNullOrEmpty(hashFileName) && File.Exists(hashFileName))
           {
               File.Delete(hashFileName);
          }

           System.Runtime.InteropServices.Marshal.FinalReleaseComObject(recordCert);
           System.Runtime.InteropServices.Marshal.FinalReleaseComObject(recordSig);
        }
       msi.Commit();

       System.Runtime.InteropServices.Marshal.FinalReleaseComObject(viewCert);
     System.Runtime.InteropServices.Marshal.FinalReleaseComObject(viewSig);
      System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
      System.Runtime.InteropServices.Marshal.FinalReleaseComObject(installer);
    }
   catch (Exception ex)
    {
       Log.LogErrorFromException(ex);
      return false;
   }
   finally
 {
       if (!string.IsNullOrEmpty(certificateFileName) && File.Exists(certificateFileName))
     {
           File.Delete(certificateFileName);
       }
       if (!string.IsNullOrEmpty(hashFileName) && File.Exists(hashFileName))
       {
           File.Delete(hashFileName);
      }
   }

   return true;
}

Of course, we shouldn't just copy and paste code, we should refactor. Ultimately, we want to write an array of bytes into file which is consumed by an MSI record, and then deletes that file when done. We could probably do more, but this will be a good start.

This should be an easy enough class. Let's start with the constructor. One of the things we want to factor away is the temporary file name, so we won't be including this info in the constructor. Instead, we will only pass the data to be written to the file. We are going to make it impossible to change the data once the object has been set, so the only property we need is the name of the temporary file that the information is getting written to. Finally, we only need one method, a way to notify the object that it is safe to delete the file. I know: let's have this thing implement IDisposable and have the file get deleted when the object is disposed. This gives us the following class, which I added to the PopulateDigitalSignature.cs (but not as a nested class: I don't really like them):

 class TemporaryFile : IDisposable
{
  private string fileName;

    public string FileName
  {
       get { return fileName; }
    }

   public TemporaryFile(byte[] data)
   {
       fileName = Path.GetTempFileName();
      using (BinaryWriter fout = new BinaryWriter(new FileStream(FileName, FileMode.Open)))
       {
           fout.Write((byte[])data);
       }
   }

   public void Dispose()
   {
       if (!string.IsNullOrEmpty(FileName) && File.Exists(FileName))
       {
           File.Delete(FileName);
      }
   }
}

This thing should be very to use from our execute method, and should greatly reduce a lot of the copied code we had. The new Execute method is below:

 public override bool Execute()
{
   try
 {
       Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
      Object installerClassObject = Activator.CreateInstance(classType);
      Installer installer = (Installer)installerClassObject;

      Database msi = installer.OpenDatabase(Database, MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
       View viewCert = msi.OpenView("SELECT * FROM `MsiDigitalCertificate`");
      viewCert.Execute(null);
     View viewSig = msi.OpenView("SELECT * FROM `MsiDigitalSignature`");
     viewSig.Execute(null);

      foreach (ITaskItem cabinet in Cabinets)
     {
           string cabFileName = Path.Combine(Path.GetDirectoryName(Database), cabinet.ItemSpec);
           string media = "1";
         if (!string.IsNullOrEmpty(cabinet.GetMetadata("DiskId")))
           {
               media = cabinet.GetMetadata("DiskId");
          }
           string digitalCertificate = string.Format("_{0}", media);

           using (TemporaryFile tempFile = new TemporaryFile((byte [])installer.FileSignatureInfo(cabFileName, 0, MsiSignatureInfo.msiSignatureInfoCertificate)))
          {
               Record recordCert = installer.CreateRecord(2);
              recordCert.set_StringData(1, digitalCertificate);
               recordCert.SetStream(2, tempFile.FileName);
             viewCert.Modify(MsiViewModify.msiViewModifyInsert, recordCert);
             System.Runtime.InteropServices.Marshal.FinalReleaseComObject(recordCert);
           }

           using (TemporaryFile tempFile = new TemporaryFile((byte[])installer.FileSignatureInfo(cabFileName, 0, MsiSignatureInfo.msiSignatureInfoHash)))
          {
               Record recordSig = installer.CreateRecord(5);
               recordSig.set_StringData(1, "Media");
               recordSig.set_StringData(2, media);
             recordSig.set_StringData(3, digitalCertificate);
                recordSig.SetStream(4, tempFile.FileName);
              viewSig.Modify(MsiViewModify.msiViewModifyInsert, recordSig);
               System.Runtime.InteropServices.Marshal.FinalReleaseComObject(recordSig);
            }
       }
       msi.Commit();

       System.Runtime.InteropServices.Marshal.FinalReleaseComObject(viewCert);
     System.Runtime.InteropServices.Marshal.FinalReleaseComObject(viewSig);
      System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
      System.Runtime.InteropServices.Marshal.FinalReleaseComObject(installer);
    }
   catch (Exception ex)
    {
       Log.LogErrorFromException(ex);
      return false;
   }

   return true;
}

I'm feeling a lot better about this code: its much more trim, and I think that its more clear as to what is going on. I just wish there was a way to get rid of the FinalReleaseComObjects.

Well, that just about does it. All the changes here were to the guts of the PopulateDigitalSignatures task, so there is no need to update the project file. There is one more bit of code cleanup to do, but I'll save that for next time.

MWadeBlog_06_12_21.zip