在下载时将用户特定数据嵌入到authenticode签名安装程序中
我有一个使用InnoSetup安装的Windows窗体应用程序,我的用户从我的网站下载。 他们将此软件安装到多台PC上。
该应用程序与Web API进行通信,该API必须能够识别用户。 我正在创建一个Web应用程序,用户可以登录并下载该应用程序。 我想在安装程序中嵌入一个通用唯一ID,这样他们就不必在安装后再次登录。 我希望他们下载并运行setup.exe,并让应用程序自行处理。
我正在考虑几个选项:
- 将用户特定的UUID嵌入到setup.exe中,并在Web服务器上按需执行代码签名
缺点:不知道怎么做? - 将用户特定的UUID嵌入到安装程序文件的名称中(例如setup_08adfb12_2712_4f1e_8630_e202da352657.exe)
缺点:这不是很漂亮,如果重命名安装程序会失败 - 将安装程序和包含UUID的设置文件包装到自解压缩zip中
如何将用户特定数据嵌入到Web服务器上的已签名可执行文件中?
整个PE没有签名。 您可以通过将数据添加到签名表中将数据嵌入到已签名的PE中。 Webex和其他工具使用此方法来提供一键式会议实用程序。
从技术上讲,PKCS#7签名有一个属性列表,这些属性被专门指定为可以使用的未经身份validation的属性,但我知道没有完整的PE解析器就没有简单的方法可以写入这些字段。 幸运的是,我们已经有了signtool
,并且对已经签名的文件添加了一个额外的签名是一种使用未经身份validation的字段的非破坏性操作。
我整理了一个演示 ,它使用这种技术将数据从MVC网站传递到可下载的Windows窗体可执行文件。
程序是:
- 从标准进程生成的authenticode signed和timestamped exe开始
(必须能够无依赖地运行 – ILMerge或类似的) - 将未加阻的exe复制到临时文件
- 创建一个短暂的代码签名证书,其中包括辅助数据作为X509扩展
- 使用
signtool
将辅助签名添加到临时文件 - 将临时文件返回给客户端,下载完成后将其删除
在客户端,应用程序:
- 从当前正在执行的exe中读取签名证书
- 查找具有已知主题名称的证书
- 查找具有已知OID的扩展
- 根据扩展中包含的数据更改其行为
该过程有许多优点:
- 对PE布局没有任何影响
- 公共信任的代码签名证书可以保持脱机(甚至在HSM中),只在Web服务器上使用临时证书
- Web服务器不会生成出站流量(如果执行了时间戳,则需要这样做)
- 快速(1MB exe的<50ms)
- 可以在IIS中运行
用法
客户端检索数据( 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); }
服务器端标记( 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"); } } }