@@ -153,6 +153,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
153
153
bool trust = false ,
154
154
bool includePrivateKey = false ,
155
155
string password = null ,
156
+ CertificateKeyExportFormat keyExportFormat = CertificateKeyExportFormat . Pfx ,
156
157
bool isInteractive = true )
157
158
{
158
159
var result = EnsureCertificateResult . Succeeded ;
@@ -170,6 +171,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
170
171
certificates = filteredCertificates ;
171
172
172
173
X509Certificate2 certificate = null ;
174
+ var isNewCertificate = false ;
173
175
if ( certificates . Any ( ) )
174
176
{
175
177
certificate = certificates . First ( ) ;
@@ -216,6 +218,7 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
216
218
try
217
219
{
218
220
Log . CreateDevelopmentCertificateStart ( ) ;
221
+ isNewCertificate = true ;
219
222
certificate = CreateAspNetCoreHttpsDevelopmentCertificate ( notBefore , notAfter ) ;
220
223
}
221
224
catch ( Exception e )
@@ -260,13 +263,13 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
260
263
{
261
264
try
262
265
{
263
- ExportCertificate ( certificate , path , includePrivateKey , password ) ;
266
+ ExportCertificate ( certificate , path , includePrivateKey , password , keyExportFormat ) ;
264
267
}
265
268
catch ( Exception e )
266
269
{
267
270
Log . ExportCertificateError ( e . ToString ( ) ) ;
268
271
// We don't want to mask the original source of the error here.
269
- result = result != EnsureCertificateResult . Succeeded || result != EnsureCertificateResult . ValidCertificatePresent ?
272
+ result = result != EnsureCertificateResult . Succeeded && result != EnsureCertificateResult . ValidCertificatePresent ?
270
273
result :
271
274
EnsureCertificateResult . ErrorExportingTheCertificate ;
272
275
@@ -292,9 +295,58 @@ public EnsureCertificateResult EnsureAspNetCoreHttpsDevelopmentCertificate(
292
295
}
293
296
}
294
297
298
+ DisposeCertificates ( ! isNewCertificate ? certificates : certificates . Append ( certificate ) ) ;
299
+
295
300
return result ;
296
301
}
297
302
303
+ internal ImportCertificateResult ImportCertificate ( string certificatePath , string password )
304
+ {
305
+ if ( ! File . Exists ( certificatePath ) )
306
+ {
307
+ Log . ImportCertificateMissingFile ( certificatePath ) ;
308
+ return ImportCertificateResult . CertificateFileMissing ;
309
+ }
310
+
311
+ var certificates = ListCertificates ( StoreName . My , StoreLocation . CurrentUser , isValid : false , requireExportable : false ) ;
312
+ if ( certificates . Any ( ) )
313
+ {
314
+ Log . ImportCertificateExistingCertificates ( ToCertificateDescription ( certificates ) ) ;
315
+ return ImportCertificateResult . ExistingCertificatesPresent ;
316
+ }
317
+
318
+ X509Certificate2 certificate ;
319
+ try
320
+ {
321
+ Log . LoadCertificateStart ( certificatePath ) ;
322
+ certificate = new X509Certificate2 ( certificatePath , password , X509KeyStorageFlags . Exportable | X509KeyStorageFlags . EphemeralKeySet ) ;
323
+ Log . LoadCertificateEnd ( GetDescription ( certificate ) ) ;
324
+ }
325
+ catch ( Exception e )
326
+ {
327
+ Log . LoadCertificateError ( e . ToString ( ) ) ;
328
+ return ImportCertificateResult . InvalidCertificate ;
329
+ }
330
+
331
+ if ( ! IsHttpsDevelopmentCertificate ( certificate ) )
332
+ {
333
+ Log . NoHttpsDevelopmentCertificate ( GetDescription ( certificate ) ) ;
334
+ return ImportCertificateResult . NoDevelopmentHttpsCertificate ;
335
+ }
336
+
337
+ try
338
+ {
339
+ SaveCertificate ( certificate ) ;
340
+ }
341
+ catch ( Exception e )
342
+ {
343
+ Log . SaveCertificateInStoreError ( e . ToString ( ) ) ;
344
+ return ImportCertificateResult . ErrorSavingTheCertificateIntoTheCurrentUserPersonalStore ;
345
+ }
346
+
347
+ return ImportCertificateResult . Succeeded ;
348
+ }
349
+
298
350
public void CleanupHttpsCertificates ( )
299
351
{
300
352
// On OS X we don't have a good way to manage trusted certificates in the system keychain
@@ -329,7 +381,7 @@ public void CleanupHttpsCertificates()
329
381
330
382
protected abstract IList < X509Certificate2 > GetCertificatesToRemove ( StoreName storeName , StoreLocation storeLocation ) ;
331
383
332
- internal void ExportCertificate ( X509Certificate2 certificate , string path , bool includePrivateKey , string password )
384
+ internal void ExportCertificate ( X509Certificate2 certificate , string path , bool includePrivateKey , string password , CertificateKeyExportFormat format )
333
385
{
334
386
Log . ExportCertificateStart ( GetDescription ( certificate ) , path , includePrivateKey ) ;
335
387
if ( includePrivateKey && password == null )
@@ -345,15 +397,69 @@ internal void ExportCertificate(X509Certificate2 certificate, string path, bool
345
397
}
346
398
347
399
byte [ ] bytes ;
400
+ byte [ ] keyBytes ;
401
+ byte [ ] pemEnvelope = null ;
402
+ RSA key = null ;
403
+
348
404
try
349
405
{
350
- bytes = includePrivateKey ? certificate . Export ( X509ContentType . Pkcs12 , password ) : certificate . Export ( X509ContentType . Cert ) ;
406
+ if ( includePrivateKey )
407
+ {
408
+ switch ( format )
409
+ {
410
+ case CertificateKeyExportFormat . Pfx :
411
+ bytes = certificate . Export ( X509ContentType . Pkcs12 , password ) ;
412
+ break ;
413
+ case CertificateKeyExportFormat . Pem :
414
+ key = certificate . GetRSAPrivateKey ( ) ;
415
+
416
+ char [ ] pem ;
417
+ if ( password != null )
418
+ {
419
+ keyBytes = key . ExportEncryptedPkcs8PrivateKey ( password , new PbeParameters ( PbeEncryptionAlgorithm . Aes256Cbc , HashAlgorithmName . SHA256 , 100000 ) ) ;
420
+ pem = PemEncoding . Write ( "ENCRYPTED PRIVATE KEY" , keyBytes ) ;
421
+ pemEnvelope = Encoding . ASCII . GetBytes ( pem ) ;
422
+ }
423
+ else
424
+ {
425
+ // Export the key first to an encrypted PEM to avoid issues with System.Security.Cryptography.Cng indicating that the operation is not supported.
426
+ // This is likely by design to avoid exporting the key by mistake.
427
+ // To bypass it, we export the certificate to pem temporarily and then we import it and export it as unprotected PEM.
428
+ keyBytes = key . ExportEncryptedPkcs8PrivateKey ( "" , new PbeParameters ( PbeEncryptionAlgorithm . Aes256Cbc , HashAlgorithmName . SHA256 , 1 ) ) ;
429
+ pem = PemEncoding . Write ( "ENCRYPTED PRIVATE KEY" , keyBytes ) ;
430
+ key . Dispose ( ) ;
431
+ key = RSA . Create ( ) ;
432
+ key . ImportFromEncryptedPem ( pem , "" ) ;
433
+ Array . Clear ( keyBytes , 0 , keyBytes . Length ) ;
434
+ Array . Clear ( pem , 0 , pem . Length ) ;
435
+ keyBytes = key . ExportPkcs8PrivateKey ( ) ;
436
+ pem = PemEncoding . Write ( "PRIVATE KEY" , keyBytes ) ;
437
+ pemEnvelope = Encoding . ASCII . GetBytes ( pem ) ;
438
+ }
439
+
440
+ Array . Clear ( keyBytes , 0 , keyBytes . Length ) ;
441
+ Array . Clear ( pem , 0 , pem . Length ) ;
442
+
443
+ bytes = certificate . Export ( X509ContentType . Cert ) ;
444
+ break ;
445
+ default :
446
+ throw new InvalidOperationException ( "Unknown format." ) ;
447
+ }
448
+ }
449
+ else
450
+ {
451
+ bytes = certificate . Export ( X509ContentType . Cert ) ;
452
+ }
351
453
}
352
454
catch ( Exception e )
353
455
{
354
456
Log . ExportCertificateError ( e . ToString ( ) ) ;
355
457
throw ;
356
458
}
459
+ finally
460
+ {
461
+ key ? . Dispose ( ) ;
462
+ }
357
463
358
464
try
359
465
{
@@ -369,6 +475,25 @@ internal void ExportCertificate(X509Certificate2 certificate, string path, bool
369
475
{
370
476
Array . Clear ( bytes , 0 , bytes . Length ) ;
371
477
}
478
+
479
+ if ( includePrivateKey && format == CertificateKeyExportFormat . Pem )
480
+ {
481
+ try
482
+ {
483
+ var keyPath = Path . ChangeExtension ( path , ".key" ) ;
484
+ Log . WritePemKeyToDisk ( keyPath ) ;
485
+ File . WriteAllBytes ( keyPath , pemEnvelope ) ;
486
+ }
487
+ catch ( Exception ex )
488
+ {
489
+ Log . WritePemKeyToDiskError ( ex . ToString ( ) ) ;
490
+ throw ;
491
+ }
492
+ finally
493
+ {
494
+ Array . Clear ( pemEnvelope , 0 , pemEnvelope . Length ) ;
495
+ }
496
+ }
372
497
}
373
498
374
499
internal X509Certificate2 CreateAspNetCoreHttpsDevelopmentCertificate ( DateTimeOffset notBefore , DateTimeOffset notAfter )
@@ -496,7 +621,7 @@ internal X509Certificate2 CreateSelfSignedCertificate(
496
621
DateTimeOffset notBefore ,
497
622
DateTimeOffset notAfter )
498
623
{
499
- var key = CreateKeyMaterial ( RSAMinimumKeySizeInBits ) ;
624
+ using var key = CreateKeyMaterial ( RSAMinimumKeySizeInBits ) ;
500
625
501
626
var request = new CertificateRequest ( subject , key , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
502
627
foreach ( var extension in extensions )
@@ -745,6 +870,31 @@ public void ExportCertificateStart(string certificate, string path, bool include
745
870
746
871
[ Event ( 56 , Level = EventLevel . Error ) ]
747
872
internal void MacOSAddCertificateToKeyChainError ( int exitCode ) => WriteEvent ( 56 , $ "An error has ocurred while importing the certificate to the keychain: { exitCode } .") ;
873
+
874
+
875
+ [ Event ( 57 , Level = EventLevel . Verbose ) ]
876
+ public void WritePemKeyToDisk ( string path ) => WriteEvent ( 57 , $ "Writing the certificate to: { path } .") ;
877
+
878
+ [ Event ( 58 , Level = EventLevel . Error ) ]
879
+ public void WritePemKeyToDiskError ( string ex ) => WriteEvent ( 58 , $ "An error has ocurred while writing the certificate to disk: { ex } .") ;
880
+
881
+ [ Event ( 59 , Level = EventLevel . Error ) ]
882
+ internal void ImportCertificateMissingFile ( string certificatePath ) => WriteEvent ( 59 , $ "The file '{ certificatePath } ' does not exist.") ;
883
+
884
+ [ Event ( 60 , Level = EventLevel . Error ) ]
885
+ internal void ImportCertificateExistingCertificates ( string certificateDescription ) => WriteEvent ( 60 , $ "One or more HTTPS certificates exist '{ certificateDescription } '.") ;
886
+
887
+ [ Event ( 61 , Level = EventLevel . Verbose ) ]
888
+ internal void LoadCertificateStart ( string certificatePath ) => WriteEvent ( 61 , $ "Loading certificate from path '{ certificatePath } '.") ;
889
+
890
+ [ Event ( 62 , Level = EventLevel . Verbose ) ]
891
+ internal void LoadCertificateEnd ( string description ) => WriteEvent ( 62 , $ "The certificate '{ description } ' has been loaded successfully.") ;
892
+
893
+ [ Event ( 63 , Level = EventLevel . Error ) ]
894
+ internal void LoadCertificateError ( string ex ) => WriteEvent ( 63 , $ "An error has ocurred while loading the certificate from disk: { ex } .") ;
895
+
896
+ [ Event ( 64 , Level = EventLevel . Error ) ]
897
+ internal void NoHttpsDevelopmentCertificate ( string description ) => WriteEvent ( 64 , $ "The provided certificate '{ description } ' is not a valid ASP.NET Core HTTPS development certificate.") ;
748
898
}
749
899
750
900
internal class UserCancelledTrustException : Exception
0 commit comments