Pre-export dev certificate to Aspire cache to avoid macOS keychain prompts#16282
Conversation
When the Aspire CLI generates, corrects, or trusts a developer certificate on macOS, also write the PFX and PEM key material to the Aspire hosting dev-cert cache (~/.aspire/dev-certs/https/). This lets app-host processes load key material from disk instead of triggering macOS Keychain access prompts. Changes: - MacOSCertificateManager: SaveCertificateCore, CorrectCertificateState, and TrustCertificateCore now write both the .aspnet and .aspire caches using ExportCertificate consistently. - DeveloperCertificateService: Restructured GetKeyMaterialAsync to try cache reads first, then do a single private key access for any misses (reduces two keychain prompts to one on cache miss). - CertificateService: Added TrustCertificateAsync with PreExportKeyMaterialAsync fallback for certs created before the cache writes existed. - CertificatesTrustCommand: Refactored to use ICertificateService.TrustCertificateAsync. - Added tests for trust flow and pre-export behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 16282Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 16282" |
There was a problem hiding this comment.
Pull request overview
Reduces/avoids macOS Keychain prompts when Aspire needs developer certificate private key material by proactively caching/exporting key material on the CLI side and preferring cache reads on the hosting side.
Changes:
- Adds a CLI-level trust flow (
ICertificateService.TrustCertificateAsync) soaspire certs trustandaspire runshare trust + cache-priming behavior. - Adds a macOS fallback pre-export (
ICertificateToolRunner.PreExportKeyMaterialAsync) that populates~/.aspire/dev-certs/https/from the on-disk dev-certs PFX. - Restructures
DeveloperCertificateService.GetKeyMaterialAsyncto read cached PEM/PFX first and only fall back to certificate export when needed, then writes back to cache.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Cli.Tests/TestServices/TestCertificateToolRunner.cs | Extends test tool runner with a pre-export callback to validate new behavior. |
| tests/Aspire.Cli.Tests/TestServices/TestCertificateService.cs | Updates test certificate service to implement the new trust API. |
| tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs | Updates embedded test ICertificateService implementation for the new trust API. |
| tests/Aspire.Cli.Tests/Commands/RunCommandTests.cs | Updates throwing test certificate service to include TrustCertificateAsync. |
| tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs | Updates throwing test certificate service to include TrustCertificateAsync. |
| tests/Aspire.Cli.Tests/Certificates/CertificateServiceTests.cs | Adds coverage asserting pre-export is invoked in trust and ensure-trusted flows. |
| src/Aspire.Hosting/DeveloperCertificateService.cs | Cache-first reads for PEM/PFX with single-pass export fallback and cache write-back. |
| src/Aspire.Cli/Commands/CertificatesTrustCommand.cs | Switches trust command to use ICertificateService.TrustCertificateAsync. |
| src/Aspire.Cli/Certificates/NativeCertificateToolRunner.cs | Implements best-effort pre-export on macOS to prime Aspire’s dev-cert cache. |
| src/Aspire.Cli/Certificates/ICertificateToolRunner.cs | Adds pre-export API to tool runner abstraction. |
| src/Aspire.Cli/Certificates/CertificateService.cs | Introduces TrustCertificateAsync and integrates pre-export into ensure-trusted flow. |
| src/Aspire.Cli/Certificates/CertificateGeneration/MacOSCertificateManager.cs | Exports key material into Aspire cache during trust/correct/save operations. |
|
I feel like there should be some logging around the cert cache. It can be at a low level (trace) |
JamesNK
left a comment
There was a problem hiding this comment.
Reviewed the PR for correctness and found 4 issues:\n\n- 2 bugs (multiple keychain prompt regressions): CorrectCertificateState triggers 3 keychain prompts instead of 1; ExportKeyMaterial makes 2 private-key accesses instead of the intended 1\n- 1 correctness issue: TrustCertificateAsync calls PreExportKeyMaterialAsync unconditionally even after trust failure/cancellation\n- 1 minor issue: PreExportKeyMaterialAsync ignores the CancellationToken parameter
eerhardt
left a comment
There was a problem hiding this comment.
Reviewed the full diff. The approach is sound — pre-populating the Aspire cache from the CLI side at trust/create/correct time so hosting finds cached key material instead of hitting the Keychain is clean and the fallback behavior on cache miss is unchanged.
Key observations:
- File extension alignment between CLI writes (.pfx, .key via ExportCertificate's auto-generated .key from .pem path) and hosting reads (.pfx, .key) is correct.
- Cache key computation (
SHA256(thumbprint)as hex) matches betweenGetAspireCertificateHashand the hosting-side password-less lookup inGetKeyMaterialAsync. SaveCertificateCorecallingExportCertificate3x is safe — the cert is still in-memory (not keychain-backed) at that point sinceSaveCertificateToUserKeychaincreates a separate copy in the keychain.CorrectCertificateStatecorrectly loads from the just-written on-disk PFX to avoid a second keychain prompt.- The pre-trust cache write in
TrustCertificateCoreis harmless if trust fails — hosting checks trust independently.
Test coverage note: The two new TrustCertificateAsync tests cover the service-layer wiring (cancel + failure paths), but the core cache logic (WriteAspireCacheFromDiskPfx, GetAspireCertificateHash, TryReadCachedPfx, TryReadCachedKeyPem, ExportKeyMaterial, WriteCacheFilesAsync) has no unit test coverage. An integration-style test verifying the cache round-trip would add confidence.
Agree with @JamesNK's open comment on ExportKeyMaterial — when both PEM key and PFX are needed on cache miss, there are still two separate private key accesses.
1208d20 to
f4009c1
Compare
f4009c1 to
2a7acc9
Compare
|
@eerhardt we can do SOME testing of certificates, but realistically we can only validate on Linux CI machines; Mac and Windows CI don't support the full set of required certificate operations. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Collapse EnsureCertificatesTrustedAsync and TrustCertificateAsync into a single path used by the apphost, init/template, and 'aspire certs trust' callers. The service always runs the trust operation so the Aspire cache stays populated even when the certificate is already trusted, and it emits the same TrustCancelled / CertificatesMayNotBeFullyTrusted warnings consistently across all callers. SSL_CERT_DIR handling on Linux partial trust is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
WriteAspireCacheFromDiskPfx previously no-op'd when the .aspnet PFX did not already exist on disk, which meant the Aspire cache would not be warmed if the .aspnet cache hadn't already been written during trust. Export the .aspnet PFX on demand in that case so both caches are always populated together. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
In non-interactive environments on macOS and Windows we can't successfully prompt for certificate trust (Keychain password / trust dialog) and we don't want to silently generate an untrusted certificate. Inject ICliHostEnvironment into CertificateService and, when SupportsInteractiveInput is false on non-Linux, skip TrustHttpCertificate but still run CheckHttpCertificate so we can warn with distinct messages for partially trusted and not trusted states. Linux trust is non-interactive so the full flow is still run there. Fixes AspireCliTsStarterSmoke hanging on 'Trusting certificates...' in Windows CI. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
JamesNK
left a comment
There was a problem hiding this comment.
Review of certificate caching changes: 1 potential bug (non-RSA cert regression — uncertain severity), 1 behavioral concern (trust command in non-interactive mode), 1 minor allocation nit.
…rivateKey ExportFromPrivateKey silently returned null for non-RSA certificates, meaning a user-supplied ECDSA certificate would skip cache warming without any diagnostic. Fall back to GetECDsaPrivateKey when the cert has no RSA key and throw InvalidOperationException when neither is available. ExportKeyPem now operates against AsymmetricAlgorithm and picks the right temporary key type when re-exporting unencrypted PKCS#8. Added tests covering the ECDSA path (password and no-password variants), the public-only-certificate failure case, and an RSA sanity check. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…port-dev-cert-cache
Description
On macOS, running an Aspire app host after trusting the developer certificate triggers Keychain access prompts to export the certificate's private key material. This happens because the Aspire hosting layer needs both PFX and PEM key formats, and each access to the keychain-backed certificate triggers a separate prompt.
This PR eliminates those prompts by:
CLI side (
MacOSCertificateManager): When creating, correcting, or trusting a developer certificate, also write the key material to the Aspire hosting dev-cert cache (~/.aspire/dev-certs/https/) using the in-memory certificate — no keychain access needed. All cache writes useExportCertificateconsistently.Hosting side (
DeveloperCertificateService): RestructuredGetKeyMaterialAsyncto try cache reads for both PEM and PFX first, then do a single private key access for any misses. This reduces two keychain prompts to at most one on cache miss.CLI trust flow: Added
TrustCertificateAsynctoICertificateServiceso bothaspire runandaspire certs trustshare the same trust + cache-population flow. Includes aPreExportKeyMaterialAsyncfallback for certificates created before the cache writes existed.Validation
aspire certs trustfollowed byaspire runno longer triggers keychain export promptsChecklist