Software 24 June 2026 9 min read My code-signing certificate is valid for three days. On purpose. By Old Forge Technologies 295 reads Contents Open the properties of my signed Windows agent and look at the certificate. Valid from 24 June 2026 to 27 June 2026. Three days. If you have ever bought a code-signing certificate, that looks broken. It is not broken. It is the whole point of how modern code signing works, and the road to it had enough surprises to be worth writing down - for the next person googling these exact error messages, and because one detail stops everyone the first time they see it. The thing being signed is the Wegweiser agent: a small Go binary that runs on endpoints I don't own, on my customers' clients' machines. Two things have to be true before it is allowed to live there. Windows Defender and whatever EDR the site runs have to trust it, and the person installing it has to be able to confirm, cryptographically, that it came from me and nobody has touched it since. That is what code signing buys. I finally did it properly this week. So let's start with the three days. ## The certificate that expires in three days If you are used to traditional code signing, a three-day certificate looks like a misconfiguration. A colleague compared it to an RMM binary whose certificate runs from 2025 to 2028, a *three-year* cert. Mine expires before the weekend. Did I get something wrong? No. The short window is the design. A code signature answers two separate questions, on two separate clocks: 1. **Was the certificate valid at the moment of signing?** 2. **Is the certificate still valid right now?** For the signature to be trusted *forever*, only the first one has to be true, and you prove it with an **RFC 3161 timestamp**. When I sign, a timestamping authority countersigns with "this happened at 15:41 on 24 June," and that countersignature is anchored in its own long-lived trusted chain. So three days later, when my signing cert has expired, Windows can still check the one thing that matters: *the binary was signed while the cert was live*. The signature stays good in 2027, in 2030, whenever someone runs it. The three-year RMM cert leans on exactly the same timestamping to outlive its own 2028 expiry. The only difference is the size of the window, and a smaller window is a security *feature*, not a bug. Which is the whole reason mine is measured in days. ## Managed signing versus a key in a drawer The traditional way to sign code: you buy a certificate from a CA, it is valid for a few years, and **you hold the private key**. These days that key has to live on a hardware token or an HSM, because since 2023 the rules require it to sit on certified hardware. The catch is the same either way - that long-lived key is the liability. If it ever leaks, an attacker can sign malware as you until someone notices and revokes the cert. I used **Azure Trusted Signing** instead. (Microsoft recently renamed it **Artifact Signing**, and the rename is only half done across the portal, the docs and the tooling, so keep both names in your head.) The model is different in three ways that matter: - **The key never leaves Microsoft's HSM.** I never see it, never store it, never rotate it. There is nothing on my side to leak. - **Every signing operation mints a fresh certificate**, valid for roughly three days, chained to a Microsoft trusted root. The leaf rotates constantly and automatically. - **Microsoft verifies your organisation's identity up front.** That is why the certificate's subject reads `CN=Old Forge Technologies` and the issuer is `Microsoft ID Verified CS EOC CA 03`. *ID Verified* meaning they checked I am a real, registered business and not someone who typed the name into a box. No key to steal, no HSM to babysit, no certificate to renew at two in the morning. A leaked short-lived cert is worthless within days. This is where the industry is heading, and for a one-person shop it is strictly less to get wrong. There is one honest trade-off, and it is worth saying plainly. A brand-new publisher has no **SmartScreen** reputation, and reputation is earned through download volume. An EV certificate buys it instantly; this approach builds it over time. It does not touch Defender or EDR trust of the running agent, and it does not change whether the signature is valid. It only governs the "unknown publisher" prompt on the first handful of downloads, and it clears itself as installs add up. ## Wiring it up (the passwordless bits) The setup, end to end: 1. **Identity validation.** You submit your organisation's details and Microsoft verifies them. This is the gate. Nothing else works until it reads "Completed." 2. **A certificate profile** of type **Public Trust**. You do not type the subject - it is pulled from the validated identity, which is the entire point. 3. **An identity for CI:** an Entra app registration with a **federated credential**, so GitHub Actions authenticates to Azure with **no stored secret**. The trust is a claims match, something like `claims['sub'] matches 'repo:your-org/your-repo:ref:refs/tags/agent-v*'` - only a tag-triggered build of *your* repo can assume the identity. 4. **The role nobody mentions until it bites you.** Signing is a **data-plane** action, so being subscription **Owner is not enough**. You have to explicitly grant **"Trusted Signing Certificate Profile Signer"** (the role kept its old name through the rename) to whatever identity does the signing. Skip it and everything looks perfect right up until `signtool` comes back with an auth error and no obvious cause. After that the signing itself is mercifully boring. Microsoft's `sign` tool: ``` sign code artifact-signing <files> \ --artifact-signing-endpoint https://<region>.codesigning.azure.net/ \ --artifact-signing-account <account> \ --artifact-signing-certificate-profile <profile> ``` I sign all three Windows executables (amd64 and arm64) and the MSI - the EXEs first, so the installer ships binaries that are already signed, then the MSI on top. EDR watches the running `.exe`, not the installer, so signing the EXE is the part that keeps the agent out of quarantine. ## The gotcha that ate an afternoon Here is the one I would have loved to find in somebody else's blog post. My Windows binaries embed an icon and a version resource, so Task Manager and Explorer show "Wegweiser Agent / 0.3.59" instead of a bare filename. The standard Go approach is `goversioninfo`, which generates a `.syso` file that `go build` links in automatically. It stopped working without telling me. `goversioninfo` ran fine and produced a valid 94 KB `.syso` with the icon inside. `go build` succeeded. The resulting binary had a **completely blank** version resource, every field empty, and not one error anywhere along the way. I chased it the obvious wrong ways first. Cleared Go's build cache (`go clean -cache`, no change). Took a clean checkout and built it on Linux instead of Windows (no change). That second test was the useful one: it ruled out "Windows versus Linux" entirely. The `.syso` embed simply was not linking under my current Go toolchain, 1.25.x, on any platform - and that is the same toolchain my CI runs, so a fresh CI build would have shipped unbranded too. Older releases had branding only because they were built before the toolchain moved underneath them. That is the worst kind of regression: nothing changed in my code, so nothing pointed at the cause. I stopped digging through the linker and reached for the tool that does not depend on any of it: **[rcedit](https://github.com/electron/rcedit)**. It writes the icon and version strings straight into the finished `.exe` through the Win32 resource APIs - it is what Electron uses to brand its binaries. One command per file, *before* signing: ``` rcedit wegweiser-agent.exe \ --set-icon wegweiser.ico \ --set-product-version 0.3.59 --set-file-version 0.3.59.0 \ --set-version-string ProductName "Wegweiser Agent" \ --set-version-string CompanyName "Wegweiser" ``` Branding back. Order matters here: brand first, sign last, because the signature has to cover the final bytes. Stamp the resource after signing and you have just invalidated the signature. The lesson is not "rcedit good, goversioninfo bad." It is that a build step which *succeeds while producing nothing* is the most dangerous kind there is, and the version resource is exactly the thing nobody checks until a customer screenshots a generic-icon process and asks why it looks like malware. ## You don't need a CI bill for any of this Here is the part I did not expect: none of this needs GitHub's hosted runners. Signing is an Azure API call. Hosting a release is free. The build is just `go` and GoReleaser. So when I hit GitHub's Actions quota in the middle of the project, I did not have to pay my way out. I built the whole release on a spare Linux box, signed it from a laptop where I had run `az login`, and published it by hand. The federated-credential trust even works from a **self-hosted runner**, because GitHub still issues the OIDC token regardless of where the runner happens to execute. Marginal cost of a signed release, after the one-time setup: zero. ## The result What I have at the end of it: a Windows agent, and its installer, Authenticode-signed by a Microsoft-verified publisher, timestamped so the signatures never expire, with no private key for me to lose and no certificate to renew. Defender and EDR trust it. The person installing it can see exactly who built it. A certificate valid for three days turns out to be the most reassuring three-day expiry I have ever shipped. --- *Notes for whoever is doing this next:* - *"Trusted Signing" and "Artifact Signing" are the same product mid-rename. Expect both names in the portal, the action and the role.* - *The signer **role** is a separate, explicit grant. Owner is not enough.* - *Timestamp everything (`--timestamp-url` / `-tr`). Without it your signatures die with the three-day cert.* - *Check the version resource actually landed (`(Get-Item x.exe).VersionInfo`). Do not trust that the build "succeeded."*