{"id":5500,"date":"2025-08-11T22:31:59","date_gmt":"2025-08-11T21:31:59","guid":{"rendered":"https:\/\/mortenknudsen.net\/?p=5500"},"modified":"2026-07-24T17:10:19","modified_gmt":"2026-07-24T16:10:19","slug":"dont-let-macros-infect-your-business-enforce-signed-macro-policies","status":"publish","type":"post","link":"https:\/\/mortenknudsen.net\/?p=5500","title":{"rendered":"How to Enforce Macro Security by running only Excel macros signed with your own public-CA\u2013issued code-signing certificate \u2013 stored in Azure Keyvault (HSM)"},"content":{"rendered":"\n<h6 class=\"wp-block-heading\">It may be tempting to click \u201cEnable Macros\u201d when you open a spreadsheet, but macros are executable programs. Without a digital signature, you have no idea who wrote the code or what it might do. Malicious macros are a common attack vector for ransomware, data theft and other malware. Insisting on <strong>digitally signed macros<\/strong> from <strong>trusted publishers<\/strong> closes this door to attackers and <strong>protects your organization<\/strong>.<\/h6>\n\n\n\n<p class=\"wp-block-paragraph\">Macros are one of Excel\u2019s most powerful features: short Visual Basic for Applications (VBA) programs that automate repetitive tasks, manipulate data, or call other Office applications. However, the same automation that saves time also creates an attractive vehicle for malware. Microsoft warns that macros can introduce viruses or malicious software and therefore disables them by default. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The safest configuration in modern Office apps is \u201c<strong>Disable all macros except digitally signed macros<\/strong>\u201d \u2013 unsigned macros do not run, and macros signed by an untrusted publisher prompt the user to trust the publisher or disable the macro. <\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"656\" height=\"306\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-3.png\" alt=\"\" class=\"wp-image-5476\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-3.png 656w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-3-300x140.png 300w\" sizes=\"auto, (max-width: 656px) 100vw, 656px\" \/><figcaption class=\"wp-element-caption\">VBA macros should be disabled, except for digitally signed trusted macros<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To make your macros work seamlessly in this environment, you must sign them with a code\u2011signing certificate and distribute the certificate to users as a trusted publisher.<\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"918\" height=\"435\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-5.png\" alt=\"\" class=\"wp-image-5481\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-5.png 918w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-5-300x142.png 300w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-5-768x364.png 768w\" sizes=\"auto, (max-width: 918px) 100vw, 918px\" \/><figcaption class=\"wp-element-caption\">List certificates are Trusted Publishers<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide explains why you need to disable unsigned macros, how to obtain a code\u2011signing certificate, where to store the private key (SafeNet USB token or Azure HSM), how to sign the macro in Excel, and how to use Microsoft Intune to distribute the publisher certificate. Necessary scripts are provided in my Github. The guide focuses on Excel but the same approach applies to other Office applications.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Update (July 2026) \u2014 troubleshooting <code>800403F4<\/code> and script improvements<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Since publishing this guide I have had a few reports of the signing step failing, and<br>I hit it myself on a freshly built machine. The script has been updated to handle<br>every cause I ran into. If you are seeing this error:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>fail: AzureSignTool.SignCommand&#91;0]\n      Signing failed with error 800403F4.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">\u2026here is what is actually going on, and what changed.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The real cause: <code>vbe7.dll<\/code> cannot load<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>800403F4<\/code> is <code>TRUST_E_SUBJECT_FORM_UNKNOWN<\/code> \u2014 Windows is telling you that no Subject<br>Interface Package claims the file. The obvious conclusion is that the SIPs are not<br>registered, but in my case they were registered perfectly: all six function entries,<br>both DLLs, correct 32-bit hive.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The actual culprit is <code>vbe7.dll<\/code>, which ships inside the Office SIP package. It is the<br>VBE7 runtime that <code>msosipx.dll<\/code> uses to read the VBA project. If it cannot load, the<br>SIP silently declines every file, and the only symptom you ever see is <code>800403F4<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two things in the SIP package readme must be in place, and neither was in my original<br>script:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>1. The Visual C++ x86 runtime is required.<\/strong> <code>vbe7.dll<\/code> depends on it. Without it,<br><code>LoadLibrary<\/code> fails with error 126 (&#8220;the specified module could not be found&#8221;) on a<br>file that plainly exists \u2014 because it is a <em>dependency<\/em> that is missing, not the DLL.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:&#47;&#47;download.microsoft.com\/download\/C\/6\/D\/C6D0FD4E-9E53-4897-9B91-836EBA2AACD3\/vcredist_x86.exe<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>2. <code>vbe7.dll<\/code> must be discoverable via the registry.<\/strong> The readme says it can sit<br>alongside the SIP DLLs \u2014 but that resolves against the <em>calling process&#8217;s<\/em> search path,<br>and signtool\/AzureSignTool live somewhere else entirely. Register the full path:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\VBA\n    REG_SZ  Vbe71DllPath = C:\\Program Files\\Microsoft Office SIPs\\vbe7.dll<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Write it to <code>SOFTWARE\\WOW6432Node\\Microsoft\\VBA<\/code> as well, since the SIP is 32-bit and<br>reads the redirected view.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The updated script does both automatically and verifies <code>vbe7.dll<\/code> actually loads in a<br>32-bit process before it attempts to sign \u2014 so you get a clear message instead of a<br>hex code.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">winget will not install the x86 .NET SDK<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This one caught a reader too. The original script called:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>winget install --id Microsoft.DotNet.SDK.8 --architecture x86 ...<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">winget matches on package ID only. If the x64 SDK is already installed it reports<br><em>&#8220;Found an existing package already installed\u2026 No available upgrade found&#8221;<\/em> and never<br>lays down the x86 payload. <code>C:\\Program Files (x86)\\dotnet\\dotnet.exe<\/code> is silently<br>never created, and every subsequent step fails. The updated script uses the official<br><code>dotnet-install.ps1<\/code> with an explicit <code>-Architecture x86 -InstallDir<\/code>, which works<br>reliably.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Now on .NET 10 and AzureSignTool 7.0.1<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">.NET 8 goes out of support on <strong>10 November 2026<\/strong>, so the script now defaults to the<br>.NET 10 LTS channel with AzureSignTool 7.0.1. These two must be kept in step \u2014 7.x<br>targets .NET 10, while 6.0.0 and 5.0.0 target .NET 8 \u2014 so both are set at the top of<br>the script with a note explaining the pairing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the record: AzureSignTool 7.0.1 signs VBA projects without any problem. If you are<br>hitting <code>800403F4<\/code>, do not waste time downgrading the tool. It is almost certainly<br><code>vbe7.dll<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Other fixes in the updated script<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Native commands do not throw.<\/strong> The original wrapped <code>dotnet<\/code> calls in <code>try\/catch<\/code>,<br>which does nothing \u2014 native executables set <code>$LASTEXITCODE<\/code> instead. Failures passed<br>silently until something later blew up. Now every call checks its exit code.<\/li>\n\n\n\n<li><strong><code>regsvr32 \/s<\/code> hides failures.<\/strong> Registration is now run with the exit code captured,<br>so a failed SIP registration is reported instead of being swallowed.<\/li>\n\n\n\n<li><strong>The NuGet source check was always true.<\/strong> <code>dotnet nuget list source<\/code> returns a<br>string array, and <code>-notmatch<\/code> against an array returns the <em>non-matching elements<\/em>,<br>not a boolean \u2014 which is always truthy. It is joined to a single string first now.<\/li>\n\n\n\n<li><strong><code>Pop-Location<\/code> was skipped on failure.<\/strong> Wrapped in <code>try\/finally<\/code>.<\/li>\n\n\n\n<li><strong>Signing now works on a staging copy<\/strong> and keeps the unsigned original as<br><code>&lt;file>.unsigned<\/code>. This matters more than it looks: once a file has the legacy and<br>agile signatures, signing it again only overwrites the third signature. Staging from<br>the pristine original means every run produces a proper Legacy \u2192 Agile \u2192 V3 set.<br>(<code>offclearsig.exe<\/code>, in the SIP folder, is the alternative \u2014 it strips existing<br>signatures in place.)<\/li>\n\n\n\n<li><strong>A pre-check for the VBA project.<\/strong> The SIP signs the VBA project, not the workbook.<br>If there is no <code>xl\\vbaProject.bin<\/code> there is genuinely nothing to sign, and the script<br>now says so instead of failing later with <code>800403F4<\/code>.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Verifying afterwards<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Open the file, press <code>Alt+F11<\/code>, then <strong>Tools \u2192 Digital Signature<\/strong> to confirm the<br>certificate. Note that <code>signtool verify \/all<\/code> and <code>\/ds<\/code> do <strong>not<\/strong> work with the VBA<br>triple-signature format \u2014 it is not the same as the native Windows multi-signature<br>mechanism, and only one signature is ever validated on a given file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Content<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"#why-bother\" data-type=\"internal\" data-id=\"#why-bother\">Why bother signing macros?<\/a><\/li>\n\n\n\n<li><a href=\"#obtaining\" data-type=\"internal\" data-id=\"#obtaining\">Obtaining a code\u2011signing certificate<\/a><\/li>\n\n\n\n<li><a href=\"#azure-hsm\" data-type=\"internal\" data-id=\"#azure-hsm\">Implementation using Azure HSM<\/a>\n<ul class=\"wp-block-list\">\n<li><a href=\"#step1\" data-type=\"internal\" data-id=\"#step1\">Step 1: Code Signing onboarding<\/a><\/li>\n\n\n\n<li><a href=\"#step2\" data-type=\"internal\" data-id=\"#step2\">Step 2: Create App in Entra ID<\/a><\/li>\n\n\n\n<li id=\"Step3\"><a href=\"#step3\" data-type=\"internal\" data-id=\"#step3\">Step 3: Delegate permissions to App in Azure Keyvault<\/a><\/li>\n\n\n\n<li><a href=\"#step4\" data-type=\"internal\" data-id=\"#step4\">Step 4: Signing your VBA macros in Excel using script<\/a><\/li>\n<\/ul>\n<\/li>\n\n\n\n<li><a href=\"#safenet\" data-type=\"internal\" data-id=\"#safenet\">Safenet signing<\/a><\/li>\n\n\n\n<li id=\"step3\"><a href=\"#regular\" data-type=\"internal\" data-id=\"#regular\">Signing your regular files with Azure Sign Tool (optional)<\/a><\/li>\n\n\n\n<li><a href=\"#pfx\" data-type=\"internal\" data-id=\"#pfx\">Why PFX Method isn&#8217;t supported anymore ?<\/a><\/li>\n\n\n\n<li id=\"macrosecurity\"><a href=\"#macrosecurity\" data-type=\"internal\" data-id=\"#macrosecurity\">Configuring macro security<\/a><\/li>\n\n\n\n<li><a href=\"#deploy\" data-type=\"internal\" data-id=\"#deploy\">Deploying the trusted publisher certificate with Intune<\/a><\/li>\n\n\n\n<li><a href=\"#bestpractice\" data-type=\"internal\" data-id=\"#bestpractice\">Best practices and recommendations<\/a><\/li>\n\n\n\n<li><a href=\"#conclusion\" data-type=\"internal\" data-id=\"#conclusion\">Conclusion<\/a><\/li>\n<\/ul>\n\n\n\n<h2 id=\"why-bother\" class=\"wp-block-heading\">Why bother signing macros?<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Macros are powerful but risky<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Excel macros contain executable code. Users often share workbooks through email or file shares, and malicious actors take advantage of this trust to deliver malware. Microsoft\u2019s documentation states that macros can pose a security risk because they may introduce viruses or malicious software. For that reason, Office disables all macros by default and warns you never to enable a macro unless you know what it does.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Digital signatures prove origin and integrity<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A digital signature uses a certificate issued by a public certificate authority (CA) to bind the macro to your organization\u2019s identity. When you sign a macro project, Office embeds a cryptographic signature into the file. When a user opens the document, the Office Trust Center verifies that the signature comes from a trusted publisher and that the macro has not been altered. Signed macros help users avoid untrusted content because they run silently only if the certificate is in the Trusted Publishers store; otherwise, users receive a prompt and can decide whether to trust the publisher. A signature also helps detect tampering\u2014if someone modifies the macro, Office removes the signature and the code no longer runs.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Protecting the signing key is essential<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Because a macro signature proves authenticity, an attacker could misuse your signing certificate to sign malicious macros. Keyfactor\u2019s security guide notes that protecting the private key used to sign macros is critical because attackers seek to steal code\u2011signing certificates and sign malware. To mitigate this, certificate vendors deliver keys on hardware security modules (HSMs) or hardware tokens. Two common options are:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>SafeNet USB tokens<\/strong> \u2013 Vendors such as DigiCert and their resellers ship the certificate on a SafeNet eToken. RapidSSL\u2019s setup instructions describe installing the SafeNet Authentication Client and DigiCert\u2019s Hardware Certificate Installer to initialize the token, enter a token password and load your certificate. The token protects the private key and requires a PIN each time you sign code.<\/li>\n\n\n\n<li><strong>Cloud HSM (Azure Key&nbsp;Vault)<\/strong> \u2013 Cloud solutions like DigiCert\u2019s KeyLocker and Azure Key&nbsp;Vault store the private key in a cloud\u2011based HSM. When creating an Extended Validation (EV) code\u2011signing certificate in Azure Key&nbsp;Vault, the Premium SKU is required because it includes support for <strong>HSM\u2011backed keys<\/strong>. Azure Key&nbsp;Vault stores the key in an HSM so it cannot be exported, and the CA requires an HSM Private Key Agreement.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Both approaches satisfy modern CA requirements for private\u2011key protection and are compatible with Office macro signing.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"obtaining\" class=\"wp-block-heading\">Obtaining a code\u2011signing certificate<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Choose the right certificate<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For Excel macros you need a certificate that supports <strong>Code Signing<\/strong>. Many CAs offer Organization\u2011validated or Extended Validation (EV) certificates. EV certificates provide higher assurance and always require storing the key in a hardware token or HSM. Vendors such as DigiCert, GlobalSign, Sectigo or Keyfactor sell code\u2011signing certificates; your company may also issue internal code\u2011signing certificates through its PKI.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Storage options<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">After the CA validates your organization, it will either:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Ship a SafeNet token<\/strong> containing the certificate and initialization instructions. You initialize the token, set a PIN and install the certificate using the vendor\u2019s installer.<\/li>\n\n\n\n<li><strong>Allow cloud HSM enrollment<\/strong> where you generate the key in Azure Key&nbsp;Vault (Premium) and submit a certificate signing request (CSR). The key type must be <strong>RSA\u2011HSM<\/strong> and the key is non\u2011exportable. The CA then provisions the certificate; you import it back into your Key&nbsp;Vault.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Timestamping<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When signing macros, include a timestamp so the signature remains valid even after the certificate expires. Adding a timestamp ensures a macro remains treated as signed after the certificate\u2019s expiration as long as the certificate is not revoked. Many vendors provide timestamp servers (e.g., DigiCert\u2019s <code>http:\/\/timestamp.digicert.com<\/code> ).<\/p>\n\n\n\n<h2 id=\"azure-hsm\" class=\"wp-block-heading\">Implementation of Signing using Azure HSM<\/h2>\n\n\n\n<h3 id=\"step1\" class=\"wp-block-heading\">Step 1: Code Signing onboarding<\/h3>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Setup Azure Premium Keyvault<\/li>\n\n\n\n<li>Make certificate<\/li>\n\n\n\n<li>Download CSR<\/li>\n\n\n\n<li>Issue\/re-issue via Trustzone \/ GlobalSign<\/li>\n\n\n\n<li>Azure Keyvault, Certificate, Certificate Operation: Merge certificate<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/trustzone.com\/knowledge-base\/purchasing-an-ev-code-signing-certificate-for-azure\" data-type=\"link\" data-id=\"https:\/\/trustzone.com\/knowledge-base\/purchasing-an-ev-code-signing-certificate-for-azure\">Sample of detailed guide from vendor Trustzone &#8211; purchasing an eV Code Signing certificate for Azure<\/a><br><\/p>\n\n\n\n<h3 id=\"step2\" class=\"wp-block-heading\">Step 2: Create App in Entra ID<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Use guide below as sample<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/trustzone.com\/knowledge-base\/how-to-sign-files-with-azure-sign-tool\">https:\/\/trustzone.com\/knowledge-base\/how-to-sign-files-with-azure-sign-tool<\/a><\/p>\n\n\n\n<h3 id=\"step3\" class=\"wp-block-heading\"><br>Step 3: Delegate permissions to App in Azure Keyvault<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">App must be delegated these 2 RBAC roles on Azure Keyvault: <\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Key Vault Certificate User<\/li>\n\n\n\n<li>Key Vault Secrets User<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">App must be having Reader permissions on resource group where Azure keyvault exist<br><\/p>\n\n\n\n<h3 id=\"step4\" class=\"wp-block-heading\">Step 4: Signing your VBA macros in Excel using script<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">I have provided the steps below to sign your VBA macros in your Excel files. Signing happens with your public Code Signing certificate stored in Azure HSM. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The script can also be found on my Github <a href=\"https:\/\/raw.githubusercontent.com\/KnudsenMorten\/IntuneRepo\/refs\/heads\/main\/Sign_Excel_Macros_From_Azure_HSM\/Sign_Excel_Macros_From_Azure_HSM.ps1\" data-type=\"link\" data-id=\"https:\/\/raw.githubusercontent.com\/KnudsenMorten\/IntuneRepo\/refs\/heads\/main\/Sign_Excel_Macros_From_Azure_HSM\/Sign_Excel_Macros_From_Azure_HSM.ps1\">here<\/a><\/p>\n\n\n\n<pre class=\"wp-block-code has-small-font-size\"><code>&lt;#\nSigning of Excel Macro files using Azure Key Vault\nRuns AzureSignTool via dotnet tool run (x86) to avoid ASR block on user-profile EXEs\nCreated by Morten Knudsen (aka.ms\/morten) \u00e2\u20ac\u201d Updated\n#&gt;\n\n################################################################################\n# VARIABLES\n################################################################################\n\n# Microsoft Office Subject Interface Packages (SIPs)\n$tmp  = \"$env:TEMP\\officesips.exe\"\n$url  = \"https:\/\/download.microsoft.com\/download\/f\/b\/4\/fb46f8ca-6a6f-4cb0-b8f4-06bf3d44da48\/officesips_16.0.16507.43425.exe\"\n$dest = \"C:\\Program Files\\Microsoft Office SIPs\"\n\n# Signing details\n$VaultUri = \"https:\/\/&lt;keyvault name&gt;.vault.azure.net\"\n$CertName = \"&lt;Keyvault certificate name&gt;\"     # KV certificate object name\n$TenantId = \"&lt;tenant id&gt;\"\n$ClientId = \"&lt;clientid&gt;\"\n$ClientSecret = \"&lt;App Secret&gt;\"\n$TimeStampUrl = \"http:\/\/timestamp.globalsign.com\/tsa\/r6advanced1\"  # or your TSA of choice\n$FileToSign = \"&lt;XLSM file&gt;\"\n\n# Use x86 dotnet host explicitly (important for Office SIP compatibility)\n$DotNetX86 = \"C:\\Program Files (x86)\\dotnet\\dotnet.exe\"\n\n# Local tool workspace for dotnet tool manifest (so we can use `dotnet tool run`)\n$ToolWorkDir = Join-Path $env:TEMP \"signing-tool-workdir\"\n\n################################################################################\n# PRE-CHECKS\n################################################################################\n\n# Admin is required for regsvr32 and most installs\n$IsAdmin = (&#91;Security.Principal.WindowsPrincipal] &#91;Security.Principal.WindowsIdentity]::GetCurrent()\n).IsInRole(&#91;Security.Principal.WindowsBuiltInRole]::Administrator)\nif (-not $IsAdmin) { throw \"Please run PowerShell as Administrator.\" }\n\n# Unblock the macro file if it came from the internet\nUnblock-File -Path $FileToSign -ErrorAction SilentlyContinue\n\n################################################################################\n# STEP 1: INSTALLATION (ONE-TIME)\n################################################################################\n\n# Ensure SIPs are present and registered\nNew-Item -ItemType Directory -Force -Path $dest | Out-Null\nInvoke-WebRequest -Uri $url -OutFile $tmp\nStart-Process -FilePath $tmp -ArgumentList \"\/extract:`\"$dest`\" \/quiet\" -Wait\nregsvr32 \/s \"$dest\\msosip.dll\"\nregsvr32 \/s \"$dest\\msosipx.dll\"\n\n# Ensure .NET SDKs (x64 + x86) are installed\nwinget install --id Microsoft.DotNet.SDK.8 --source winget --architecture x64 --accept-package-agreements --accept-source-agreements\nwinget install --id Microsoft.DotNet.SDK.8 --source winget --architecture x86 --accept-package-agreements --accept-source-agreements\n\n# NuGet source (idempotent) \u00e2\u20ac\u201c no -ErrorAction (dotnet nuget doesn't support it)\ntry {\n  dotnet nuget add source https:\/\/api.nuget.org\/v3\/index.json -n nuget.org 2&gt;$null\n} catch {\n  # ignore if it already exists\n}\n\n# Prepare a local tool manifest so we can run \"dotnet tool run AzureSignTool\"\nNew-Item -ItemType Directory -Force -Path $ToolWorkDir | Out-Null\nPush-Location $ToolWorkDir\nif (-not (Test-Path \".config\\dotnet-tools.json\")) {\n  &amp; $DotNetX86 new tool-manifest | Out-Null\n}\n\n# Install (or update) AzureSignTool as a *local* tool into this workdir\n# Using x86 dotnet host ensures the tool runs in a 32-bit context\ntry {\n  &amp; $DotNetX86 tool install AzureSignTool | Out-Null\n} catch {\n  &amp; $DotNetX86 tool update AzureSignTool | Out-Null\n}\n\n################################################################################\n# STEP 2: SIGN FILE (3 PASSES)\n################################################################################\n\nfor ($i = 1; $i -le 3; $i++) {\n  Write-Host \"Signing pass $i...\" -ForegroundColor Cyan\n  &amp; $DotNetX86 tool run AzureSignTool -- sign `\n    \"$FileToSign\" `\n    -kvu \"$VaultUri\" -kvc \"$CertName\" `\n    -kvt \"$TenantId\" -kvi \"$ClientId\" -kvs \"$ClientSecret\" `\n    -tr \"$TimeStampUrl\" -td sha256\n  if ($LASTEXITCODE -ne 0) {\n    throw \"AzureSignTool failed on pass $i with exit code $LASTEXITCODE.\"\n  }\n}\n\nPop-Location\nWrite-Host \"Signing completed successfully.\" -ForegroundColor Green\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"safenet\" class=\"wp-block-heading\">SafeNet signing<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have your certificate on a SafeNet token, follow these steps to sign the macro project. For SafeNet tokens, you will be prompted for your token PIN when you select the certificate. <\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Open the workbook<\/strong> containing the macro project. You can also click <strong>ALT + F11<\/strong> inside Excel.\n<ul class=\"wp-block-list\">\n<li>Enable the <strong>Developer<\/strong> tab if it isn\u2019t already visible (File&nbsp;\u2192&nbsp;Options&nbsp;\u2192&nbsp;Customize Ribbon&nbsp;\u2192&nbsp;check <strong>Developer<\/strong>). <\/li>\n\n\n\n<li>On the <strong>Developer<\/strong> tab, click <strong>Visual&nbsp;Basic<\/strong> to open the VBA editor. Y<\/li>\n<\/ul>\n<\/li>\n\n\n\n<li>In the Project Explorer, select the VBA project you want to sign.<\/li>\n\n\n\n<li>Choose <strong>Tools&nbsp;\u2192&nbsp;Digital&nbsp;Signature<\/strong>. In the <strong>Digital&nbsp;Signature<\/strong> dialog, click <strong>Choose<\/strong>, select your certificate (it will appear under the token or HSM provider) and click <strong>OK<\/strong>.<\/li>\n\n\n\n<li>Save the workbook. Excel embeds the digital signature. If you later modify any code, the signature is removed and you must re\u2011sign before distributing.<\/li>\n<\/ol>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"613\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-1024x613.png\" alt=\"\" class=\"wp-image-5472\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-1024x613.png 1024w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-300x180.png 300w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-768x460.png 768w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-1536x919.png 1536w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image.png 1591w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Signing in Excel<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"792\" height=\"537\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-2.png\" alt=\"\" class=\"wp-image-5474\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-2.png 792w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-2-300x203.png 300w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-2-768x521.png 768w\" sizes=\"auto, (max-width: 792px) 100vw, 792px\" \/><figcaption class=\"wp-element-caption\">Signing in Excel<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"regular\" class=\"wp-block-heading\">Signing your regular files with Azure Sign Tool (optional)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/trustzone.com\/knowledge-base\/how-to-sign-files-with-azure-sign-tool\">https:\/\/trustzone.com\/knowledge-base\/how-to-sign-files-with-azure-sign-tool<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The steps are:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>1) <strong>Install Azure Sign Tool on Computer<\/strong>\ndotnet nuget add source https:\/\/api.nuget.org\/v3\/index.json -n nuget.org\n\ndotnet tool install --global AzureSignTool\n\n<strong>2) Create App in Entra ID<\/strong>\n\n<strong>3) Delegate permissions to app on keyvault<\/strong>\nKey Vault Certificate User \nKey Vault Secrets User\n\n4) <strong>Sign file<\/strong> (sample)\n\nazuresigntool sign -kvu https:\/\/xxxxx.vault.azure.net -kvt xxxxx -kvc xxxxCodeSigning -kvi xxxxxxxxxf00ace1 -kvs xxxxxx -tr http:\/\/timestamp.globalsign.com\/tsa\/r6advanced1 -td &lt;filename&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"pfx\" class=\"wp-block-heading\">Why PFX Method isn&#8217;t supported anymore ?<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>You\u2019ll be out of compliance (and often can\u2019t even buy one).<\/strong> Since <strong>June 1, 2023<\/strong>, the CA\/Browser Forum requires <strong>all publicly-trusted code-signing cert private keys<\/strong> (OV and EV) to be <strong>hardware-protected<\/strong> (FIPS 140-2 L2 or CC EAL4+). That means cloud\/HSM (e.g., Azure Key Vault\/Managed HSM or a vendor cloud HSM) or a hardware token (Thales\/SafeNet). A plain <strong>.PFX (software key)<\/strong> no longer meets the baseline and most CAs stopped issuing them that way. <\/li>\n\n\n\n<li><strong>Higher breach risk.<\/strong> A .PFX is an exportable file protected only by a password. If it\u2019s copied from a build agent, laptop, or share, an attacker can sign malware with <em>your<\/em> identity. The hardware-only rule was created precisely to reduce stolen-key abuse. <\/li>\n\n\n\n<li><strong>Weaker custody &amp; auditing.<\/strong> With a .PFX, it\u2019s hard to prove who used the key and when. HSM\/token approaches can enforce non-exportable keys and provide stronger operational controls (policies, PIN, RBAC, logs).<\/li>\n\n\n\n<li><strong>Macro\/VBA quirks won\u2019t be \u201cfixed\u201d by using PFX.<\/strong> Office\u2019s VBA signer uses <strong>legacy hashing<\/strong> (MD5 is referenced in vendor guidance). Some modern tokens\/HSMs disable legacy algorithms, which is why people hit \u201cVBA project could not be signed\u201d errors\u2014but switching to a PFX doesn\u2019t solve the root security problem, it just avoids the hardware policy. Better to follow vendor-specific workarounds or managed cloud signing designed for VBA.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">When is .PFX still okay?<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Private\/inside your org only:<\/strong> If you\u2019re using an <strong>internal\/private CA<\/strong> purely for internal macro signing and you control every endpoint\u2019s trust, you <em>can<\/em> technically use a PFX. It\u2019s just <strong>not<\/strong> acceptable for <strong>publicly-trusted<\/strong> code signing anymore. If you go this route, lock it down (dedicated signing host, strict ACLs, no network shares, tight monitoring\/rotation) and plan a move to HSM\/cloud signing.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Recommended path<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>For <strong>public CA<\/strong> code signing (including macros that leave your tenant): use an HSM-backed option\u2014<strong>Azure Key Vault Premium\/Managed HSM<\/strong> or a vendor cloud HSM\/token offering\u2014so you\u2019re compliant and safer. If you hit <strong>VBA<\/strong>-specific signing errors with hardware, follow your CA\u2019s guidance for VBA signing or use their managed cloud-signing service that supports Office\/VBA flows.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"macrosecurity\" class=\"wp-block-heading\">Configuring macro security<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">To ensure that only trusted macros run automatically:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>In Excel, go to <strong>File&nbsp;\u2192&nbsp;Options&nbsp;\u2192&nbsp;Trust&nbsp;Center&nbsp;\u2192&nbsp;Trust&nbsp;Center&nbsp;Settings&nbsp;\u2192&nbsp;Macro&nbsp;Settings<\/strong>.<\/li>\n\n\n\n<li>Select <strong>Disable all macros except digitally signed macros<\/strong>. This setting disables unsigned macros but allows macros signed by a trusted publisher to run without notification.<\/li>\n\n\n\n<li>Optionally uncheck \u201cEnable&nbsp;Excel&nbsp;4.0 macros\u201d unless you need legacy XLM macros.<\/li>\n<\/ol>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"656\" height=\"306\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-3.png\" alt=\"\" class=\"wp-image-5476\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-3.png 656w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-3-300x140.png 300w\" sizes=\"auto, (max-width: 656px) 100vw, 656px\" \/><figcaption class=\"wp-element-caption\">Only allow digitally signed macros<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\">For a signed macro to run silently, the signing certificate (or its issuing CA) must be in the <strong>Trusted Publishers<\/strong> store on the user\u2019s computer. If the certificate is not trusted, users will be prompted to trust the publisher the first time they run the macro.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"deploy\" class=\"wp-block-heading\">Deploying the trusted publisher certificate with Intune<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Why distribute the certificate?<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a user runs a signed macro, Office checks whether the certificate\u2019s subject matches a trusted publisher. If the certificate is not in the <strong>Trusted Publishers<\/strong> store, Office displays a security warning and asks whether to trust the publisher Administrators can avoid this prompt by pre\u2011installing the certificate into the Trusted&nbsp;Publisher and Trusted&nbsp;Root stores on corporate devices.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Using a PowerShell script<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Microsoft Intune (Endpoint Manager) allows you to deploy PowerShell scripts to managed Windows devices. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I have provided <a href=\"https:\/\/github.com\/KnudsenMorten\/IntuneRepo\/tree\/main\/Certificate_TrustedPublishers\" data-type=\"link\" data-id=\"https:\/\/github.com\/KnudsenMorten\/IntuneRepo\/tree\/main\/Certificate_TrustedPublishers\">detection\/remediation script on my Github<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"267\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-7-1024x267.png\" alt=\"\" class=\"wp-image-5483\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-7-1024x267.png 1024w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-7-300x78.png 300w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-7-768x200.png 768w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-7.png 1093w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">Trusted Publishers<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n<div class=\"wp-block-image\">\n<figure class=\"aligncenter size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"918\" height=\"435\" src=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-6.png\" alt=\"\" class=\"wp-image-5482\" srcset=\"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-6.png 918w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-6-300x142.png 300w, https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-6-768x364.png 768w\" sizes=\"auto, (max-width: 918px) 100vw, 918px\" \/><figcaption class=\"wp-element-caption\">Trusted Publishers<\/figcaption><\/figure>\n<\/div>\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"bestpractice\" class=\"wp-block-heading\">Best practices and recommendations<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Restrict macros by default<\/strong> \u2013 Keep macro settings at the most restrictive level and only allow digitally signed macros. Educate users not to enable macros in unknown documents.<\/li>\n\n\n\n<li><strong>Use a reputable CA and protect the private key<\/strong> \u2013 Choose a well\u2011known CA and store the signing key in a SafeNet token or HSM as required by CA guidelines. RapidSSL\u2019s instructions emphasise that SafeNet tokens should be initialized with a strong PIN and that existing certificates on the token will be deleted during installation. The Azure Key&nbsp;Vault approach requires the Premium SKU and the RSA\u2011HSM key type to ensure the private key resides in an HSM.<\/li>\n\n\n\n<li><strong>Timestamp signatures<\/strong> \u2013 Add a timestamp so macros remain valid after certificate expiration.<\/li>\n\n\n\n<li><strong>Re\u2011sign after modifications<\/strong> \u2013 Any change to the macro code invalidates the signature; re\u2011sign before distribution.<\/li>\n\n\n\n<li><strong>Test before signing<\/strong> \u2013 Sign macros only after thoroughly testing your solution. Once signed, modifications remove the signature, so plan for finalization before signing.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 id=\"conclusion\" class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Digitally signing Excel macros protects users and your organization by ensuring that macros originate from a trusted source and have not been altered. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Modern Office security settings disable unsigned macros by default, so signed macros and trusted publishers are essential for seamless operation. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Obtaining a code\u2011signing certificate, storing the private key securely on a SafeNet token or in an HSM like Azure Key&nbsp;Vault, signing the macro in Excel, and deploying the publisher certificate through Intune are the key steps to implement a secure macro strategy. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">By following the detailed steps above and enforcing macro\u2011security policies, you can provide automation benefits without exposing your organization to malware.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It may be tempting to click \u201cEnable Macros\u201d when you open a spreadsheet, but macros are executable programs. Without a &#8230; <\/p>\n<p class=\"read-more-container\"><a title=\"How to Enforce Macro Security by running only Excel macros signed with your own public-CA\u2013issued code-signing certificate \u2013 stored in Azure Keyvault (HSM)\" class=\"read-more button\" href=\"https:\/\/mortenknudsen.net\/?p=5500#more-5500\" aria-label=\"Read more about How to Enforce Macro Security by running only Excel macros signed with your own public-CA\u2013issued code-signing certificate \u2013 stored in Azure Keyvault (HSM)\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":5502,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"inline_featured_image":false,"_uf_show_specific_survey":0,"_uf_disable_surveys":false,"ngg_post_thumbnail":0,"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[154,213,57,215,161,60,132,214],"tags":[211,166,210,17,216,212],"class_list":["post-5500","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-automation","category-azure-keyvault","category-azure-security","category-certificate","category-entra-id","category-scripting","category-security","category-zerotrust","tag-macro","tag-microsoft","tag-office","tag-security","tag-signing","tag-zerotrust","infinite-scroll-item","resize-featured-image"],"featured_image_src":"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-10.png","author_info":{"display_name":"Morten Knudsen","author_link":"https:\/\/mortenknudsen.net\/?author=1"},"jetpack_featured_media_url":"https:\/\/mortenknudsen.net\/wp-content\/uploads\/2025\/08\/image-10.png","jetpack_sharing_enabled":true,"jetpack_likes_enabled":true,"_links":{"self":[{"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/posts\/5500","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5500"}],"version-history":[{"count":15,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/posts\/5500\/revisions"}],"predecessor-version":[{"id":6949,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/posts\/5500\/revisions\/6949"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=\/wp\/v2\/media\/5502"}],"wp:attachment":[{"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5500"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5500"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mortenknudsen.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5500"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}