Fix certificate chain install hang, elevation-aware stores, and idempotent gallery publish #20

Merged
gsadmin merged 1 commits from dev into main 2026-07-30 23:20:57 +00:00
Owner

Follow-up to #19, covering the issues hit while testing profile-based issuance end to end.

Fixed: -InstallChain could hang forever

Adding a root certificate to CurrentUser\Root makes Windows raise a modal trust confirmation dialog, and X509Store.Add blocks until it is answered. With the dialog hidden behind the console the cmdlet appeared to stop right after installing the intermediate, leaving a subordinate installed with no root above it — which Windows then reports as "The issuer of this certificate could not be found".

The chain itself was never incomplete: the run reached a Root-store write, and only a self-signed certificate routes there, so Infisical had returned the root all along. The dialog was the blocker.

Certificate store selection now follows elevation

-StoreLocation is chosen from the process's elevation when not supplied, which also sidesteps the prompt entirely (writing LocalMachine\Root already required elevation, so Windows does not ask):

Session Leaf Intermediates Roots
Elevated LocalMachine\My LocalMachine\CertificateAuthority LocalMachine\Root
Not elevated CurrentUser\My CurrentUser\CertificateAuthority CurrentUser\Root

Applies to Request-InfisicalCertificate and Install-InfisicalCertificate. The resolved value is reported on the verbose stream and an explicit -StoreLocation still wins. A warning is emitted before any blocking CurrentUser\Root write.

Only the leaf honours -StoreName (default My). Chain members are routed by what they are — self-signed to the trusted-root store, anything with an issuer above it to the intermediate store — for a chain of any depth.

Related correctness fixes

  • Machine key store. When the resolved location is LocalMachine and -KeyStorageFlags was not supplied, the private key is written to the machine key store. Otherwise the key lands in the calling user's profile while the certificate sits in LocalMachine\My, the usual cause of an installed certificate with no usable private key for a service.
  • Reuse detection searches the store location the install will write to, instead of always searching CurrentUser — otherwise every elevated run would reissue.
  • Issuers install before the leaf, so the certificate is chainable the moment it appears.
  • Chain verification runs after -InstallChain and names the certificate whose issuer is missing rather than leaving it to be found in certmgr.
  • Friendly name defaults to the common name in upper case. -FriendlyName overrides it and moves from the ByCa set to every set; the CA path still forwards it to Infisical as the issued certificate's friendlyName.

Fixed: the gallery publish reported failure on a successful push

The publish job failed on #19 with a 409 for 2026.7.30.2309, but that version is live on the gallery (created 23:10:04) — the push landed and the client surfaced an error anyway, so the retry collided with its own upload.

The step now checks the gallery before pushing, skips when the version is already there, and re-checks before failing on a publish error. This mirrors the release job, which already looks for an existing tag and skips.

Version comparison normalizes each segment the way NuGet does: the manifest carries 2026.07.30.2309 while the gallery lists 2026.7.30.2309, so comparing raw strings would never match and the guard would never fire. Verified against four published versions, and a live lookup confirms the guard would have exited 0 on the run that failed.

Both workflow files are updated and still parse as YAML with the same three jobs; .github keeps its ubuntu-latest runners and standard artifact actions.

These workflow changes take effect on this merge. Gitea evaluates workflows at the merge commit — run #16 executed publish-psgallery.yml@5e5145f, a commit that introduced a workflow file absent from main beforehand — so the updated publish step is what runs when this PR lands.

Tests

282 pass, up from 268. New coverage for the trust-prompt warning, store routing across a real root/intermediate/leaf chain, friendly-name application, partial-chain detection, and elevation-aware resolution. Full build.ps1 -RunTests green including module import, manifest, and help validation across 53 cmdlets.

Verified live under Windows PowerShell 5.1 in an elevated session: auto-resolution reports LocalMachine, -WhatIf confirms LocalMachine\My, and an explicit -StoreLocation produces no auto-resolve message.

Co-Authored-By: Claude Opus 5 noreply@anthropic.com

Generated with Claude Code

Follow-up to #19, covering the issues hit while testing profile-based issuance end to end. ## Fixed: `-InstallChain` could hang forever Adding a root certificate to `CurrentUser\Root` makes Windows raise a modal trust confirmation dialog, and `X509Store.Add` blocks until it is answered. With the dialog hidden behind the console the cmdlet appeared to stop right after installing the intermediate, leaving a subordinate installed with no root above it — which Windows then reports as **"The issuer of this certificate could not be found"**. The chain itself was never incomplete: the run reached a `Root`-store write, and only a self-signed certificate routes there, so Infisical had returned the root all along. The dialog was the blocker. ## Certificate store selection now follows elevation `-StoreLocation` is chosen from the process's elevation when not supplied, which also sidesteps the prompt entirely (writing `LocalMachine\Root` already required elevation, so Windows does not ask): | Session | Leaf | Intermediates | Roots | | --- | --- | --- | --- | | Elevated | `LocalMachine\My` | `LocalMachine\CertificateAuthority` | `LocalMachine\Root` | | Not elevated | `CurrentUser\My` | `CurrentUser\CertificateAuthority` | `CurrentUser\Root` | Applies to `Request-InfisicalCertificate` and `Install-InfisicalCertificate`. The resolved value is reported on the verbose stream and an explicit `-StoreLocation` still wins. A warning is emitted before any blocking `CurrentUser\Root` write. Only the leaf honours `-StoreName` (default `My`). Chain members are routed by what they are — self-signed to the trusted-root store, anything with an issuer above it to the intermediate store — for a chain of any depth. ## Related correctness fixes - **Machine key store.** When the resolved location is `LocalMachine` and `-KeyStorageFlags` was not supplied, the private key is written to the machine key store. Otherwise the key lands in the calling user's profile while the certificate sits in `LocalMachine\My`, the usual cause of an installed certificate with no usable private key for a service. - **Reuse detection** searches the store location the install will write to, instead of always searching `CurrentUser` — otherwise every elevated run would reissue. - **Issuers install before the leaf**, so the certificate is chainable the moment it appears. - **Chain verification** runs after `-InstallChain` and names the certificate whose issuer is missing rather than leaving it to be found in `certmgr`. - **Friendly name** defaults to the common name in upper case. `-FriendlyName` overrides it and moves from the `ByCa` set to every set; the CA path still forwards it to Infisical as the issued certificate's `friendlyName`. ## Fixed: the gallery publish reported failure on a successful push The publish job failed on #19 with a 409 for `2026.7.30.2309`, but that version is live on the gallery (created 23:10:04) — the push landed and the client surfaced an error anyway, so the retry collided with its own upload. The step now checks the gallery before pushing, skips when the version is already there, and re-checks before failing on a publish error. This mirrors the release job, which already looks for an existing tag and skips. Version comparison normalizes each segment the way NuGet does: the manifest carries `2026.07.30.2309` while the gallery lists `2026.7.30.2309`, so comparing raw strings would never match and the guard would never fire. Verified against four published versions, and a live lookup confirms the guard would have exited 0 on the run that failed. Both workflow files are updated and still parse as YAML with the same three jobs; `.github` keeps its `ubuntu-latest` runners and standard artifact actions. **These workflow changes take effect on this merge.** Gitea evaluates workflows at the merge commit — run #16 executed `publish-psgallery.yml@5e5145f`, a commit that introduced a workflow file absent from `main` beforehand — so the updated publish step is what runs when this PR lands. ## Tests 282 pass, up from 268. New coverage for the trust-prompt warning, store routing across a real root/intermediate/leaf chain, friendly-name application, partial-chain detection, and elevation-aware resolution. Full `build.ps1 -RunTests` green including module import, manifest, and help validation across 53 cmdlets. Verified live under Windows PowerShell 5.1 in an elevated session: auto-resolution reports `LocalMachine`, `-WhatIf` confirms `LocalMachine\My`, and an explicit `-StoreLocation` produces no auto-resolve message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Generated with [Claude Code](https://claude.com/claude-code)
gsadmin added 1 commit 2026-07-30 23:19:44 +00:00
Make the PowerShell Gallery publish step idempotent
Publish to PowerShell Gallery / build (pull_request) Successful in 25s
Publish to PowerShell Gallery / release (pull_request) Successful in 10s
Publish to PowerShell Gallery / publish (pull_request) Successful in 8s
dadba2f4c8
The publish job failed on PR #19 with a 409 for version 2026.7.30.2309, but
that version is live on the gallery (created 23:10:04) - the push landed and
the client still surfaced an error, so the retry collided with the upload that
had just succeeded. The run went red over a publish that actually worked.

The step now checks the gallery before pushing and skips when the version is
already there, and on a publish error it re-checks before failing. This mirrors
the release job, which already looks for an existing tag and skips.

Version comparison normalizes each segment the way NuGet does, since the
manifest carries zero-padded segments (2026.07.30.2309) while the gallery lists
the stripped form (2026.7.30.2309); comparing the raw strings would never match
and the guard would never fire.

Verified: both workflows still parse as YAML with the same three jobs, the
normalizer reproduces the gallery form for four published versions, and a live
lookup confirms the guard would have exited 0 on the run that failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
gsadmin merged commit 09af8939c7 into main 2026-07-30 23:20:57 +00:00
Sign in to join this conversation.