I have a Windows Forms application installed using InnoSetup that my users download from my website. They install this software onto multiple PCs.
The application talks to a Web API which must be able to identify the user. I am creating a web application where the user can log in and download the app. I would like to embed a universally unique ID into the installer so that they do not have to login again after installation. I want them to download and run setup.exe, and have the application take care of itself.
I am considering a couple of options:
- Embed a user-specific UUID into setup.exe and perform code-signing on-demand on the web server
Downside: not sure how to do this?
- Embed a user-specific UUID into the name of the installer file (e.g. setup_08adfb12_2712_4f1e_8630_e202da352657.exe)
Downside: this is not pretty and would fail if the installer is renamed
- Wrap the installer and a settings file containing the UUID into a self-extracting zip
How can I embed user-specific data into a signed executable on the web server?
The entire PE is not signed. You can embed data into a signed PE by adding it to the signature table. This method is used by Webex and other tools to provide the one-click meeting utilities.
Technically, the PKCS#7 signature has a list of attributes that are specifically designated as unauthenticated which could be used, but I know of no easy way to write to these fields without a full PE parser. Luckily, we already have signtool
, and adding an additional signature to an already signed file is a non-destructive operation that uses the unauthenticated fields.
I put together a demo which uses this technique to pass data from an MVC website to a downloadable windows forms executable.
The procedure is to:
- Start with the authenticode signed and timestamped exe produced by standard processes
(must be able to run without dependencies - ILMerge or similar)
- Copy the unstamped exe to a temp file
- Create an ephemeral code signing certificate which includes the auxiliary data as an X509 extension
- Use
signtool
to add the auxiliary signature to the temp file
- Return the temp file to the client, delete it after the download completes
On the client side, the app:
- Reads the signing certificates from the currently executing exe
- Finds the certificate with a known subject name
- Finds the extension with a known OID
- Alters its behavior based off of the data contained in the extension
The process has a number of advantages:
- No monkeying with the PE layout
- The publicly trusted code signing certificate can stay offline (or even in an HSM), only ephemeral certificates are used on the web server
- No outbound traffic is generated from the web server (as would otherwise be required if timestamping were performed)
- Fast (<50ms for a 1MB exe)
- Can be run from within IIS
Usage
Client side retrieval of data (Demo Application\MainForm.cs)
try
{
var thisPath = Assembly.GetExecutingAssembly().Location;
var stampData = StampReader.ReadStampFromFile(thisPath, StampConstants.StampSubject, StampConstants.StampOid);
var stampText = Encoding.UTF8.GetString(stampData);
lbStamped.Text = stampText;
}
catch (StampNotFoundException ex)
{
MessageBox.Show(this, $"Could not locate stamp\r\n\r\n{ex.Message}", Text);
}
Server side stamping (Demo Website\Controllers\HomeController.cs)
var stampText = $"Server time is currently {DateTime.Now} at time of stamping";
var stampData = Encoding.UTF8.GetBytes(stampText);
var sourceFile = Server.MapPath("~/Content/Demo Application.exe");
var signToolPath = Server.MapPath("~/App_Data/signtool.exe");
var tempFile = Path.GetTempFileName();
bool deleteStreamOpened = false;
try
{
IOFile.Copy(sourceFile, tempFile, true);
StampWriter.StampFile(tempFile, signToolPath, StampConstants.StampSubject, StampConstants.StampOid, stampData);
var deleteOnClose = new FileStream(tempFile, FileMode.Open, FileAccess.Read, FileShare.Read | FileShare.Delete, 4096, FileOptions.DeleteOnClose);
deleteStreamOpened = true;
return File(deleteOnClose, "application/octet-stream", "Demo Application.exe");
}
finally
{
if (!deleteStreamOpened)
{
try
{
IOFile.Delete(tempFile);
}
catch
{
// no-op, opportunistic cleanup
Debug.WriteLine("Failed to cleanup file");
}
}
}