Skip to content

macOS dev-certs overhaul#42251

Merged
adityamandaleeka merged 22 commits into
dotnet:mainfrom
adityamandaleeka:macos_devcerts_updates
Jul 26, 2022
Merged

macOS dev-certs overhaul#42251
adityamandaleeka merged 22 commits into
dotnet:mainfrom
adityamandaleeka:macos_devcerts_updates

Conversation

@adityamandaleeka
Copy link
Copy Markdown
Member

@adityamandaleeka adityamandaleeka commented Jun 17, 2022

Improve the experience of doing development with HTTPS on macOS:

  • Narrow the scope of trust for the developer HTTPS certificate. The trust policy for the certificate will be set to be always trusted for SSL and X.509 Basic Policy.
  • Add the certificate to the per-user trust settings in the user keychain rather than system-wide.
  • Use a well-known location on disk (in $HOME/.aspnet/dev-certs/https) to load the certificate from when running Kestrel in development mode bound to an HTTPS address.
  • Improve user-facing message when trusting existing developer certificate.
  • Remove unnecessary un-trust step prior to deletion which added an extra authentication prompt to the flow. This seems not to be necessary any more.
  • Create a mechanism for OS-specific CertificateManager implementations to check if their state is consistent, and check for that as part of the --check command (and warn the user to clean up if it's not). The macOS implementation will now use this to detect cases where certs are only in one of the two places they should be (on-disk store or keychain). Update: instead of doing this we now just rev the certificate version, enabling both types of dev certs to be present. There was no good way to handle cleaning up the old state in an automated, non-interactive way, which is a requirement for the SDK first-run experience. EDIT 2: Change approach again. The macOS CertificateManager will handles pre-7 certificates and update them on first-run (with a prompt).
  • Some housekeeping/code cleanup and comments.

This change greatly reduces the number of password/Touch ID prompts that appear when trying to develop on macOS, and makes the dev-certs tool generally more pleasant to use on macOS.

Verified scenarios with these changes on Monterey and Big Sur. There are no automated tests for this since the keychain/trust manipulation requires user interaction.

Fixes #41879 and #41878

@adityamandaleeka
Copy link
Copy Markdown
Member Author

Comment thread src/Shared/CertificateGeneration/MacOSCertificateManager.cs Outdated
Comment on lines +349 to +355
if (result == EnsureCertificateResult.ValidCertificatePresent)
{
result = EnsureCertificateResult.ExistingHttpsCertificateTrusted;
}
else
{
result = EnsureCertificateResult.NewHttpsCertificateTrusted;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we avoid adding new results here? This is a contract between us and the tooling teams, so adding new results changes that contract and might break them.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@javiercn Ah, I see, good to know. I'll check out how the tooling stuff consumes this today.

I'll also add a note mentioning this contract next to the enum declaration so someone doesn't accidentally break the contract in the future.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... can you point me to where this is used by tooling? I can't seem to find it.

cc @vijayrkn

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @vijayrkn! Looks like the tooling just calls the dev-certs tool and relies on some of the status codes here which are the exit codes of the tool:

private const int Success = 0;
private const int ErrorCreatingTheCertificate = 1;
private const int ErrorSavingTheCertificate = 2;
private const int ErrorExportingTheCertificate = 3;
private const int ErrorTrustingTheCertificate = 4;
private const int ErrorUserCancelledTrustPrompt = 5;
private const int ErrorNoValidCertificateFound = 6;
private const int ErrorCertificateNotTrusted = 7;

The EnsureCertificateResult enum changes I made won't change the actual exit codes from the tool, since these turn into "Success" here:

https://github.com/dotnet/aspnetcore/pull/42251/files#diff-1454ec4889b7f660676a246d1b0f74f29b980e4370e1932e012e2b968d33702aR433

So I think we're safe.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding @BillHiebert in case there are other places where we call into the tool.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a note of caution about modifying the exit codes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also impacts VS 4 Mac and Docker container tools. FYI.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see now that this only affects the internal reporting, but that the tool still returns 0 (success) so I think we are good here.

@mkArtakMSFT mkArtakMSFT added area-commandlinetools Includes: Command line tools, dotnet-dev-certs, dotnet-user-jwts, and OpenAPI feature-devcerts labels Jun 17, 2022
Comment thread src/Shared/CertificateGeneration/CertificateManager.cs Outdated
Comment thread src/Shared/CertificateGeneration/MacOSCertificateManager.cs Outdated
@adityamandaleeka adityamandaleeka added the blog-candidate Consider mentioning this in the release blog post label Jun 18, 2022
@ghost
Copy link
Copy Markdown

ghost commented Jun 18, 2022

@adityamandaleeka, this change will be considered for inclusion in the blog post for the release it'll ship in. Nice work!

Please ensure that the original comment in this thread contains a clear explanation of what the change does, why it's important (what problem does it solve?), and, if relevant, include things like code samples and/or performance numbers.

This content may not be exactly what goes into the blog post, but it will help the team putting together the announcement.

Thanks!

Copy link
Copy Markdown
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

@adityamandaleeka
Copy link
Copy Markdown
Member Author

Going to hold off on this PR until the upgrade experience (coming in with some existing state left behind by the older version of the tool) is smoother. Right now you end up with torn state that's annoying to clean up.

@adityamandaleeka
Copy link
Copy Markdown
Member Author

After trying a few different approaches, the cleanest way forward seems to be to change the certificate's OID so that .NET 7+ dev certs won't conflict at all with the certs used in 6 and below.

Trying to do otherwise causes problems since the old SDK would continue to locate and run trust operations on the newer certs, breaking the improved (fewer prompts, etc.) experience for people using both .NET 6 and .NET 7 SDKs.


protected override void PopulateCertificatesFromStore(X509Store store, List<X509Certificate2> certificates)
{
bool useDiskStore = store.Name! == StoreName.My.ToString() && store.Location == StoreLocation.CurrentUser;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit: does var not work here?

@javiercn
Copy link
Copy Markdown
Member

@adityamandaleeka Changing the OID will break developing .NET 6.0 apps with the 7.0 SDK.

@javiercn javiercn self-requested a review June 22, 2022 23:28
@adityamandaleeka
Copy link
Copy Markdown
Member Author

This is true. However, there's no great option here since keeping the OID the same will break things in cases where people switch between multiple SDK versions (e.g. if one of the repos they work in specifies an SDK in a global.json).

I discussed this with @DamianEdwards and we decided that the least awful option is to take the hit of changing the OID and print a message when --trust is run:

reporter.Warn("NOTE: If your app is targeting a version of .NET earlier than .NET 7, this version of "
                + "the dev-certs tool will not install the correct certificate for your app. Please use the dev-certs "
                + "tool from the .NET 6 SDK or the SDK version that matches your app instead.");

I've also scoped the OID change to be macOS only to limit the blast radius of this change.

Comment thread src/Shared/CertificateGeneration/CertificateManager.cs Outdated
Comment thread src/Tools/FirstRunCertGenerator/test/CertificateManagerTests.cs Outdated
@javiercn
Copy link
Copy Markdown
Member

@adityamandaleeka I'm struggling to understand why changing the OID is necessary at all.

Could you please help me understand what didn't work in the older version that changing the OID fixes?

@adityamandaleeka
Copy link
Copy Markdown
Member Author

Sure, the short summary is what I mentioned above; keeping the same OID will make switching between SDK versions cause inscrutable problems. Here are some examples:

Example 1

  • Create in 7
  • Clean in 6

Leaves behind the on-disk file and further cleans on 7 will fail

Example 2

  • Have existing trusted cert from 6 and move to 7.
  • Create on 7 will "succeed" and say a valid cert is present.

Kestrel on 7 will fail on startup unable to find the cert (and show instructions for create/trust).

  • Trust on 7 will say it succeeded.

Kestrel on 7 will still fail on startup unable to find the cert (until a clean is done from 6, then a create on 7).



One of the options considered (and attempted earlier in the PR) involved detecting and clean old state, but we can't go down that path since it would involve adding authentication during steps that occur in various tooling/first-run scenarios and therefore need to be non-interactive.

@adityamandaleeka
Copy link
Copy Markdown
Member Author

adityamandaleeka commented Jun 29, 2022

I'll resolve the conflicts and update the failing test with the new cert. @javiercn any further comments?

@javiercn
Copy link
Copy Markdown
Member

javiercn commented Jul 1, 2022

@adityamandaleeka Sorry for the delayed response.

I'm wondering if we are optimizing for the right scenarios. From what I understand there can be these cases

Only .NET 6.0 SDK is installed -> This would continue to work as it does today.
Only .NET 7.0 SDK is installed -> This would work with the new way.

.NET 7.0 and .NET 6.0 SDKs are installed.
There is an existing .NET 6.0 certificate
There is an existing .NET 7.0 certificate.
There are no certificates.

Would it work if the .NET 7.0 SDK does the following?

  • If it sees an existing certificate, it exports it to the location on disk the first time the certificate is accessed.
  • If it sees an existing certificate on disk but no certificate on the keychain, it deletes the file.
  • .NET 7.0 always installs the certificate on the keychain and on disk.
    • .NET 6.0 apps use the certificate from keychain (same experience as today).
    • .NET 7.0 apps use the certificate from disk (new experience).
  • .NET 6.0 SDK trusts the certificate system wide (same experience as today).
  • .NET 7.0 SDK uses the new more scoped trust experience (new experience).

In general, if you have .NET 7.0 SDK installed, that will be used unless you have a global.json file on your project.

In the event of clean with the .NET 6.0 SDK, it will remove the certificate only from the keychain and leave the file behind. .NET 7.0 SDK will clean that up the next time the tool runs.

In that way, the only thing ever left behind is the file on disk if you use .NET 6.0 to do the cleaning (which should be rare).

The advantage is that the .NET 7 SDK don't break your ability to develop .NET 6.0 apps, which is something that newer SDKs must support.

@DamianEdwards
Copy link
Copy Markdown
Member

If it sees an existing certificate, it exports it to the location on disk the first time the certificate is accessed.

@javiercn I think the issue is that exporting the cert requires user elevation/confirmation and as such can't be done as part of the existing first-run experience that we have today (which typically just creates the certs in the key ring). This is ultimately what led us to this approach. We could get the machine into the state you're suggesting via an interactive command, but we also need clean machines installing .NET for the first time to just work with the simpler flow. We couldn't figure out a combination of changes that would satisfy that.

@javiercn
Copy link
Copy Markdown
Member

javiercn commented Jul 1, 2022

@DamianEdwards its possible to skip this in the first run experience and wait until the certificate is accessed for the first time in an interactive way.

There is already a flag for determining when interaction is possible and when it is not, which the first run experience used in the past.

Would that not work?

@DamianEdwards
Copy link
Copy Markdown
Member

So on a machine that has .NET 6 SDK already installed (and thus .NET 6 certs) what would the .NET 7 SDK first-run do? It can't export the existing cert (that requires elevation) so it must do nothing at all.

Creating a new web app from the CLI and trying to run it will work in the default case because we now default to an http-only launch profile. But if the https launch profile is used (which VS for Mac will default to) Kestrel will fail to launch because the on-disk certificate is not present yet. If it is via VS we might be able to get their launch experience to prompt to fix up the cert at that point (like they do today), but if it's the CLI, what then? Won't Kestrel just fail? Are you suggesting we have it fail with a message that instructs the user to run the dev-certs command to fix it up?

@javiercn
Copy link
Copy Markdown
Member

javiercn commented Jul 1, 2022

@DamianEdwards the cert manager checks for the certificate on disk, it doesn’t find it. It sees the certificate on the keychain and tries to export it to the disk location. Next time, it will find it there. On the first run experience it skips the export step.

Does that make sense?

@DamianEdwards
Copy link
Copy Markdown
Member

@javiercn are you saying it exports it to disk when Kestrel tries to load it? That won't work because it requires user confirmation. Or are you saying Kestrel startup would fail with a message, and then the user would run the dev-certs CLI to set the machine up?

@adityamandaleeka adityamandaleeka force-pushed the macos_devcerts_updates branch from dc24b2b to 0346d22 Compare July 26, 2022 02:08
@adityamandaleeka
Copy link
Copy Markdown
Member Author

Update: After some discussion with @javiercn and @DamianEdwards, we decided to soften the requirements a tiny bit to improve the experience. dotnet dev-certs https on .NET 7 will prompt the first time if there is leftover state from pre-.NET 7 (so that it can 'upgrade' it by exporting it to the on-disk location).

The latest update brings back a bunch of the code from earlier commits in this PR that dealt with pre-7 certs, re-unifies the OIDs so we won't require older certs to be managed by older SDKs (which would have been an annoying pain-point), and makes it so that Kestrel no longer tries to access the certificate's key at startup when CheckCertificateState fails.

With these changes, we still eliminate nearly all of the prompts people encounter while also making the experience reasonable for those who are working on both 6.0 and 7.0 codebases.

Thanks @javiercn and @DamianEdwards. I think we've landed at a better experience now.

Comment thread src/Tools/dotnet-dev-certs/src/Program.cs Outdated
Copy link
Copy Markdown
Member

@javiercn javiercn left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great @adityamandaleeka.

Thanks for persevering through this.

I do have a minor comment on a log message to the console, but other than that it looks solid!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-commandlinetools Includes: Command line tools, dotnet-dev-certs, dotnet-user-jwts, and OpenAPI blog-candidate Consider mentioning this in the release blog post feature-devcerts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Load the HTTPS developer certificate from disk instead of the Keychain on macOS

7 participants