| File: Certificates\CertificateGeneration\WindowsCertificateManager.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.Versioning; using System.Security.AccessControl; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Certificates.Generation; [SupportedOSPlatform("windows")] internal sealed class WindowsCertificateManager : CertificateManager { /// Win32 ERROR_CANCELLED (0x4C7) encoded as an HRESULT (0x800704C7). /// Thrown when the user dismisses the Windows certificate-store security dialog. private const int UserCancelledHResult = unchecked((int)0x800704C7); private const int UserCancelledErrorCode = 1223; public WindowsCertificateManager(ILogger logger) : base(logger) { } // For testing purposes only internal WindowsCertificateManager(string subject, int version) : base(subject, version) { } internal override bool IsExportable(X509Certificate2 c) { #if XPLAT // For the first run experience we don't need to know if the certificate can be exported. return true; #else using var key = c.GetRSAPrivateKey(); return (key is RSACryptoServiceProvider rsaPrivateKey && rsaPrivateKey.CspKeyContainerInfo.Exportable) || (key is RSACng cngPrivateKey && cngPrivateKey.Key.ExportPolicy == CngExportPolicies.AllowExport); #endif } internal override CheckCertificateStateResult CheckCertificateState(X509Certificate2 candidate) { return new CheckCertificateStateResult(true, null); } internal override void CorrectCertificateState(X509Certificate2 candidate) { // Do nothing since we don't have anything to check here. } protected override X509Certificate2 SaveCertificateCore(X509Certificate2 certificate, StoreName storeName, StoreLocation storeLocation) { // On non OSX systems we need to export the certificate and import it so that the transient // key that we generated gets persisted. var export = certificate.Export(X509ContentType.Pkcs12, ""); certificate.Dispose(); certificate = X509CertificateLoader.LoadPkcs12(export, "", X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.Exportable); Array.Clear(export, 0, export.Length); certificate.FriendlyName = AspNetHttpsOidFriendlyName; using (var store = new X509Store(storeName, storeLocation)) { store.Open(OpenFlags.ReadWrite); store.Add(certificate); store.Close(); }; return certificate; } protected override TrustLevel TrustCertificateCore(X509Certificate2 certificate) { using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); if (TryFindCertificateInStore(store, certificate, out _)) { Log.WindowsCertificateAlreadyTrusted(); return TrustLevel.Full; } try { Log.WindowsAddCertificateToRootStore(); using var publicCertificate = X509CertificateLoader.LoadCertificate(certificate.Export(X509ContentType.Cert)); publicCertificate.FriendlyName = certificate.FriendlyName; store.Add(publicCertificate); return TrustLevel.Full; } catch (CryptographicException exception) when (exception.HResult == UserCancelledHResult || exception.HResult == UserCancelledErrorCode) { Log.WindowsCertificateTrustCanceled(); throw new UserCancelledTrustException(); } } protected override void RemoveCertificateFromTrustedRoots(X509Certificate2 certificate) { Log.WindowsRemoveCertificateFromRootStoreStart(); using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); if (TryFindCertificateInStore(store, certificate, out var matching)) { store.Remove(matching); } else { Log.WindowsRemoveCertificateFromRootStoreNotFound(); } Log.WindowsRemoveCertificateFromRootStoreEnd(); } public override TrustLevel GetTrustLevel(X509Certificate2 certificate) { var isTrusted = ListCertificates(StoreName.Root, StoreLocation.CurrentUser, isValid: true, requireExportable: false) .Any(c => AreCertificatesEqual(c, certificate)); return isTrusted ? TrustLevel.Full : TrustLevel.None; } protected override IList<X509Certificate2> GetCertificatesToRemove(StoreName storeName, StoreLocation storeLocation) { return ListCertificates(storeName, storeLocation, isValid: false); } protected override void CreateDirectoryWithPermissions(string directoryPath) { var dirInfo = new DirectoryInfo(directoryPath); if (!dirInfo.Exists) { // We trust the default permissions on Windows enough not to apply custom ACLs. // We'll warn below if things seem really off. dirInfo.Create(); } var currentUser = WindowsIdentity.GetCurrent(); var currentUserSid = currentUser.User; var systemSid = new SecurityIdentifier(WellKnownSidType.LocalSystemSid, domainSid: null); var adminGroupSid = new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, domainSid: null); var dirSecurity = dirInfo.GetAccessControl(); var accessRules = dirSecurity.GetAccessRules(true, true, typeof(SecurityIdentifier)); foreach (FileSystemAccessRule rule in accessRules) { var idRef = rule.IdentityReference; if (rule.AccessControlType == AccessControlType.Allow && !idRef.Equals(currentUserSid) && !idRef.Equals(systemSid) && !idRef.Equals(adminGroupSid)) { // This is just a heuristic - determining whether the cumulative effect of the rules // is to allow access to anyone other than the current user, system, or administrators // is very complicated. We're not going to do anything but log, so an approximation // is fine. Log.DirectoryPermissionsNotSecure(dirInfo.FullName); break; } } } }