7
7
using System . IO ;
8
8
using System . Linq ;
9
9
using System . Runtime . InteropServices ;
10
+ using System . Runtime . InteropServices . ComTypes ;
10
11
using System . Security . Cryptography ;
11
12
using System . Security . Cryptography . X509Certificates ;
12
13
using System . Text ;
@@ -50,6 +51,8 @@ internal class CertificateManager
50
51
private static readonly string MacOSTrustCertificateCommandLineArguments = "security add-trusted-cert -d -r trustRoot -k " + MacOSSystemKeyChain + " " ;
51
52
#endif
52
53
private const int UserCancelledErrorCode = 1223 ;
54
+ private const string MacOSSetPartitionKeyPermissionsCommandLine = "sudo" ;
55
+ private static readonly string MacOSSetPartitionKeyPermissionsCommandLineArguments = "security set-key-partition-list -D localhost -S unsigned:,teamid:UBF8T346G9 " + MacOSUserKeyChain ;
53
56
54
57
public IList < X509Certificate2 > ListCertificates (
55
58
CertificatePurpose purpose ,
@@ -147,6 +150,39 @@ private void DisposeCertificates(IEnumerable<X509Certificate2> disposables)
147
150
}
148
151
}
149
152
153
+ internal static bool IsHttpsDevelopmentCertificate ( X509Certificate2 certificate ) =>
154
+ certificate . Extensions . OfType < X509Extension > ( )
155
+ . Any ( e => string . Equals ( AspNetHttpsOid , e . Oid . Value , StringComparison . Ordinal ) ) ;
156
+
157
+ internal static bool CheckDeveloperCertificateKey ( X509Certificate2 candidate )
158
+ {
159
+ // Tries to use the certificate key to validate it can't access it
160
+ try
161
+ {
162
+ var rsa = candidate . GetRSAPrivateKey ( ) ;
163
+ if ( rsa == null )
164
+ {
165
+ return false ;
166
+ }
167
+
168
+ // Encrypting a random value is the ultimate test for a key validity.
169
+ // Windows and Mac OS both return HasPrivateKey = true if there is (or there has been) a private key associated
170
+ // with the certificate at some point.
171
+ var value = new byte [ 32 ] ;
172
+ using ( var rng = RandomNumberGenerator . Create ( ) )
173
+ {
174
+ rsa . Decrypt ( rsa . Encrypt ( value , RSAEncryptionPadding . Pkcs1 ) , RSAEncryptionPadding . Pkcs1 ) ;
175
+ }
176
+
177
+ // Being able to encrypt and decrypt a payload is the strongest guarantee that the key is valid.
178
+ return true ;
179
+ }
180
+ catch ( Exception )
181
+ {
182
+ return false ;
183
+ }
184
+ }
185
+
150
186
#if NETCOREAPP2_0 || NETCOREAPP2_1
151
187
152
188
public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate ( DateTimeOffset notBefore , DateTimeOffset notAfter , string subjectOverride )
@@ -192,6 +228,27 @@ public X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate(DateTimeOffs
192
228
return certificate ;
193
229
}
194
230
231
+ internal bool HasValidCertificateWithInnaccessibleKeyAcrossPartitions ( )
232
+ {
233
+ var certificates = GetHttpsCertificates ( ) ;
234
+ if ( certificates . Count == 0 )
235
+ {
236
+ return false ;
237
+ }
238
+
239
+ // We need to check all certificates as a new one might be created that hasn't been correctly setup.
240
+ var result = false ;
241
+ foreach ( var certificate in certificates )
242
+ {
243
+ result = result || ! CanAccessCertificateKeyAcrossPartitions ( certificate ) ;
244
+ }
245
+
246
+ return result ;
247
+ }
248
+
249
+ public IList < X509Certificate2 > GetHttpsCertificates ( ) =>
250
+ ListCertificates ( CertificatePurpose . HTTPS , StoreName . My , StoreLocation . CurrentUser , isValid : true , requireExportable : true ) ;
251
+
195
252
public X509Certificate2 CreateApplicationTokenSigningDevelopmentCertificate ( DateTimeOffset notBefore , DateTimeOffset notAfter , string subjectOverride )
196
253
{
197
254
var subject = new X500DistinguishedName ( subjectOverride ?? IdentityDistinguishedName ) ;
@@ -596,9 +653,10 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
596
653
bool trust = false ,
597
654
bool includePrivateKey = false ,
598
655
string password = null ,
599
- string subject = LocalhostHttpsDistinguishedName )
656
+ string subject = LocalhostHttpsDistinguishedName ,
657
+ bool isInteractive = true )
600
658
{
601
- return EnsureValidCertificateExists ( notBefore , notAfter , CertificatePurpose . HTTPS , path , trust , includePrivateKey , password , subject ) ;
659
+ return EnsureValidCertificateExists ( notBefore , notAfter , CertificatePurpose . HTTPS , path , trust , includePrivateKey , password , subject , isInteractive ) ;
602
660
}
603
661
604
662
public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCertificate (
@@ -610,7 +668,7 @@ public EnsureCertificateResult EnsureAspNetCoreApplicationTokensDevelopmentCerti
610
668
string password = null ,
611
669
string subject = IdentityDistinguishedName )
612
670
{
613
- return EnsureValidCertificateExists ( notBefore , notAfter , CertificatePurpose . Signing , path , trust , includePrivateKey , password , subject ) ;
671
+ return EnsureValidCertificateExists ( notBefore , notAfter , CertificatePurpose . Signing , path , trust , includePrivateKey , password , subject , isInteractive : true ) ;
614
672
}
615
673
616
674
public EnsureCertificateResult EnsureValidCertificateExists (
@@ -621,7 +679,8 @@ public EnsureCertificateResult EnsureValidCertificateExists(
621
679
bool trust = false ,
622
680
bool includePrivateKey = false ,
623
681
string password = null ,
624
- string subjectOverride = null )
682
+ string subjectOverride = null ,
683
+ bool isInteractive = true )
625
684
{
626
685
if ( purpose == CertificatePurpose . All )
627
686
{
@@ -633,6 +692,33 @@ public EnsureCertificateResult EnsureValidCertificateExists(
633
692
634
693
certificates = subjectOverride == null ? certificates : certificates . Where ( c => c . Subject == subjectOverride ) ;
635
694
695
+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) )
696
+ {
697
+ foreach ( var cert in certificates )
698
+ {
699
+ if ( ! CanAccessCertificateKeyAcrossPartitions ( cert ) )
700
+ {
701
+ if ( ! isInteractive )
702
+ {
703
+ // If the process is not interactive (first run experience) bail out. We will simply create a certificate
704
+ // in case there is none or report success during the first run experience.
705
+ break ;
706
+ }
707
+ try
708
+ {
709
+ // The command we run handles making keys for all localhost certificates accessible across partitions. If it can not run the
710
+ // command safely (because there are other localhost certificates that were not created by asp.net core, it will throw.
711
+ MakeCertificateKeyAccessibleAcrossPartitions ( cert ) ;
712
+ break ;
713
+ }
714
+ catch ( Exception )
715
+ {
716
+ return EnsureCertificateResult . FailedToMakeKeyAccessible ;
717
+ }
718
+ }
719
+ }
720
+ }
721
+
636
722
var result = EnsureCertificateResult . Succeeded ;
637
723
638
724
X509Certificate2 certificate = null ;
@@ -672,6 +758,11 @@ public EnsureCertificateResult EnsureValidCertificateExists(
672
758
{
673
759
return EnsureCertificateResult . ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore ;
674
760
}
761
+
762
+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . OSX ) && isInteractive )
763
+ {
764
+ MakeCertificateKeyAccessibleAcrossPartitions ( certificate ) ;
765
+ }
675
766
}
676
767
if ( path != null )
677
768
{
@@ -704,6 +795,74 @@ public EnsureCertificateResult EnsureValidCertificateExists(
704
795
return result ;
705
796
}
706
797
798
+ private void MakeCertificateKeyAccessibleAcrossPartitions ( X509Certificate2 certificate )
799
+ {
800
+ if ( OtherNonAspNetCoreHttpsCertificatesPresent ( ) )
801
+ {
802
+ throw new InvalidOperationException ( "Unable to make HTTPS ceritificate key trusted across security partitions." ) ;
803
+ }
804
+ using ( var process = Process . Start ( MacOSSetPartitionKeyPermissionsCommandLine , MacOSSetPartitionKeyPermissionsCommandLineArguments ) )
805
+ {
806
+ process . WaitForExit ( ) ;
807
+ if ( process . ExitCode != 0 )
808
+ {
809
+ throw new InvalidOperationException ( "Error making the key accessible across partitions." ) ;
810
+ }
811
+ }
812
+
813
+ var certificateSentinelPath = GetCertificateSentinelPath ( certificate ) ;
814
+ File . WriteAllText ( certificateSentinelPath , "true" ) ;
815
+ }
816
+
817
+ private static string GetCertificateSentinelPath ( X509Certificate2 certificate ) =>
818
+ Path . Combine ( Environment . GetEnvironmentVariable ( "HOME" ) , ".dotnet" , $ "certificate.{ certificate . GetCertHashString ( HashAlgorithmName . SHA256 ) } .sentinel") ;
819
+
820
+ private bool OtherNonAspNetCoreHttpsCertificatesPresent ( )
821
+ {
822
+ var certificates = new List < X509Certificate2 > ( ) ;
823
+ try
824
+ {
825
+ using ( var store = new X509Store ( StoreName . My , StoreLocation . CurrentUser ) )
826
+ {
827
+ store . Open ( OpenFlags . ReadOnly ) ;
828
+ certificates . AddRange ( store . Certificates . OfType < X509Certificate2 > ( ) ) ;
829
+ IEnumerable < X509Certificate2 > matchingCertificates = certificates ;
830
+ // Ensure the certificate hasn't expired, has a private key and its exportable
831
+ // (for container/unix scenarios).
832
+ var now = DateTimeOffset . Now ;
833
+ matchingCertificates = matchingCertificates
834
+ . Where ( c => c . NotBefore <= now &&
835
+ now <= c . NotAfter && c . Subject == LocalhostHttpsDistinguishedName ) ;
836
+
837
+ // We need to enumerate the certificates early to prevent dispoisng issues.
838
+ matchingCertificates = matchingCertificates . ToList ( ) ;
839
+
840
+ var certificatesToDispose = certificates . Except ( matchingCertificates ) ;
841
+ DisposeCertificates ( certificatesToDispose ) ;
842
+
843
+ store . Close ( ) ;
844
+
845
+ return matchingCertificates . All ( c => ! HasOid ( c , AspNetHttpsOid ) ) ;
846
+ }
847
+ }
848
+ catch
849
+ {
850
+ DisposeCertificates ( certificates ) ;
851
+ certificates . Clear ( ) ;
852
+ return true ;
853
+ }
854
+
855
+ bool HasOid ( X509Certificate2 certificate , string oid ) =>
856
+ certificate . Extensions . OfType < X509Extension > ( )
857
+ . Any ( e => string . Equals ( oid , e . Oid . Value , StringComparison . Ordinal ) ) ;
858
+ }
859
+
860
+ private bool CanAccessCertificateKeyAcrossPartitions ( X509Certificate2 certificate )
861
+ {
862
+ var certificateSentinelPath = GetCertificateSentinelPath ( certificate ) ;
863
+ return File . Exists ( certificateSentinelPath ) ;
864
+ }
865
+
707
866
private class UserCancelledTrustException : Exception
708
867
{
709
868
}
@@ -717,4 +876,4 @@ private enum RemoveLocations
717
876
}
718
877
#endif
719
878
}
720
- }
879
+ }
0 commit comments