Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HLKSigner

We hit this wall while taking our Windows kernel driver through HLK certification: an EV cert in an HSM-backed Key Vault, and no Microsoft tool that could sign the submission package with it. This is the tool we wished existed.

Built by Tracenyx — kernel-native network security for Kubernetes.

A small .NET 8 console tool that signs Microsoft HLK submission packages (.hlkx) using an EV code-signing certificate stored in Azure Key Vault (Premium SKU, RSA-HSM, non-exportable).

.hlkx files are Open Packaging Convention (OPC) packages — the same family as .vsix, .nupkg, .docx. They are not Authenticode-signed, so signtool / AzureSignTool reject them ("not a recognized file type"). Microsoft signs them via XML Digital Signatures embedded in the package itself — see https://learn.microsoft.com/en-us/windows-hardware/test/hlk/user/hlk-signing-with-an-hsm.

This tool builds the OPC XML-DSIG signature directly (manipulating the package as a ZipArchive) and calls RSA.SignHash() on an RSAKeyVaultProvider instance that proxies the operation to Key Vault. The private key never leaves the HSM — only the precomputed hash of the canonicalized <SignedInfo> is sent over the wire, and the signed bytes come back.

We do not use System.IO.Packaging.PackageDigitalSignatureManager, because it reads cert.PrivateKey internally, which on .NET 8 Windows forces an RSA.ExportParameters(true) call. Non-exportable HSM-backed keys fail that call with "Private keys cannot be exported by this provider". The manual XML-DSIG approach (same as OpenOpcSignTool) avoids this entirely.

Build

Requires the .NET 8 SDK on Windows.

dotnet build -c Release

The built executable is at:

bin\Release\net8.0\HLKSigner.exe

For a self-contained, single-file build:

dotnet publish -c Release -r win-x64 --self-contained -p:PublishSingleFile=true

Auth setup

The tool authenticates with DefaultAzureCredential. The simplest path on a developer machine:

az login
az account set --subscription "<your subscription>"

The signed-in identity (or whatever credential DefaultAzureCredential resolves — managed identity, service principal env vars, etc.) needs two Key Vault role assignments on the vault holding the cert:

Role Why
Key Vault Crypto User Sign + Get on the underlying RSA-HSM key
Key Vault Reader Get on the certificate (to fetch the public part)

Using legacy access policies instead of RBAC, you need:

  • Key permissions: Sign, Get
  • Certificate permissions: Get

Usage

HLKSigner.exe <package.hlkx> <kv_url> <cert_name> [timestamp_url]

timestamp_url is optional and defaults to none — no RFC 3161 timestamp is applied, matching what HLK Studio itself produces. Pass a TSA URL (e.g. http://timestamp.digicert.com) to add a timestamp Object to the signature.

The package is modified in place — back it up first if you need the unsigned original.

Example:

HLKSigner.exe `
  C:\submissions\MyDriver_2025_05_24.hlkx `
  https://contoso-signing.vault.azure.net `
  contoso-ev-codesign

Exit codes: 0 success, 1 usage, 2 bad input/package, 3 auth failure, 4 Key Vault error, 5 signing failure.

Verifying a signed package

A successfully signed .hlkx contains a digital-signature origin and one or more XML signature parts.

Quick check — extract as zip

.hlkx is a zip. Rename a copy to .zip and open it (or use any zip tool). Confirm these paths exist:

  • package/services/digital-signature/origin.psdsor
  • package/services/digital-signature/xml-signature/*.psdsxs
  • package/services/digital-signature/certificate/*.cer

PowerShell one-liner:

Add-Type -AssemblyName System.IO.Compression.FileSystem
[IO.Compression.ZipFile]::OpenRead("C:\submissions\MyDriver.hlkx").Entries |
  Where-Object { $_.FullName -like "package/services/digital-signature/*" } |
  Select-Object FullName

Deeper check — re-hash and compare

PackageDigitalSignatureManager can verify packages our tool signs, but note it requires the cert chain to validate against the local trust store (an EV cert from DigiCert should chain cleanly on any up-to-date Windows box). PowerShell snippet:

Add-Type -AssemblyName WindowsBase
$pkg = [System.IO.Packaging.Package]::Open(
  "C:\submissions\MyDriver.hlkx",
  [System.IO.FileMode]::Open,
  [System.IO.FileAccess]::Read)
$mgr = New-Object System.IO.Packaging.PackageDigitalSignatureManager $pkg
"IsSigned       : $($mgr.IsSigned)"
"Verify result  : $($mgr.VerifySignatures($true))"
foreach ($sig in $mgr.Signatures) {
  "  Signer       : $($sig.Signer.Subject)"
  "  Signed parts : $($sig.SignedParts.Count)"
}
$pkg.Close()

Implementation notes

  • The cert public part is loaded from Key Vault via CertificateClient.GetCertificate.
  • An RSA instance is created with RSAKeyVaultProvider.RSAFactory.Create bound to the certificate's KeyId. The only operation we invoke on it is SignHash(byte[], HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), which proxies to Key Vault's cryptographyClient.Sign(RS256, hash).
  • The package is opened as a ZipArchive in Update mode. We:
    1. Ensure [Content_Types].xml has Default entries for .psdsor / .psdsxs and an Override for the origin part.
    2. Create an empty origin part at package/services/digital-signature/origin.psdsor.
    3. Add a Relationship from the package root to the origin (type .../digital-signature/origin).
    4. Write an origin-rels file package/services/digital-signature/_rels/origin.psdsor.rels pointing to the signature part (type .../digital-signature/signature).
    5. Compute SHA-256 of every other part in the zip and build the XML signature manifest from those digests.
    6. Canonicalize the manifest <Object> and <SignedInfo> with C14N, compute SHA-256 of <SignedInfo>, and call kvRsa.SignHash(...) to produce the <SignatureValue>.
    7. Embed the cert in <KeyInfo><X509Data><X509Certificate> and write the full XML to package/services/digital-signature/xml-signature/{thumbprint}.psdsxs.
  • Digest algorithm: SHA-256 throughout (http://www.w3.org/2001/04/xmlenc#sha256 for digests, http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 for the signature).
  • RFC 3161 timestamping is optional and off by default (default none, matching HLK Studio). Passing a TSA URL adds a <TimeStamp> element built via Rfc3161TimestampRequest over the <SignatureValue>.

Known limitations / non-goals

  • No batch mode. One package per invocation.
  • No global tool packaging. Plain .exe only.

Troubleshooting

Symptom Likely cause
CredentialUnavailableException Run az login; verify az account show returns the right tenant.
403 Forbidden on the certificate fetch Missing Key Vault Reader (or Certificate Get permission).
403 Forbidden on signing Missing Key Vault Crypto User (or Key Sign permission).
FileFormatException opening the package Not a valid OPC/zip package, or file is open in another process.
CryptographicException: Invalid algorithm specified Vault key is not RSA, or vault is Standard SKU and EV cert needs Premium HSM.

References

License / Apache-2.0 — see LICENSE.

About

Sign Microsoft HLK submission packages (.hlkx) with an EV certificate in Azure Key Vault HSM — where signtool and dotnet sign can't.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages