22using System . Diagnostics . Eventing . Reader ;
33using System . Text . RegularExpressions ;
44using Microsoft . Extensions . Logging ;
5+ using System . Security . Cryptography ;
6+ using System . Security . Cryptography . X509Certificates ;
57
68namespace Winsdk . Cli . Services ;
79
810internal partial class CertificateService (
911 IBuildToolsService buildToolsService ,
10- IPowerShellService powerShellService ,
1112 IGitignoreService gitignoreService ,
1213 ILogger < CertificateService > logger ) : ICertificateService
1314{
1415 public const string DefaultCertFileName = "devcert.pfx" ;
1516
16- private static Dictionary < string , string > GetCertificateEnvironmentVariables ( )
17- {
18- return new Dictionary < string , string >
19- {
20- [ "PSModulePath" ] = "C:\\ Program Files\\ WindowsPowerShell\\ Modules;C:\\ WINDOWS\\ system32\\ WindowsPowerShell\\ v1.0\\ Modules"
21- } ;
22- }
23-
2417 public record CertificateResult (
2518 string CertificatePath ,
2619 string Password ,
@@ -53,22 +46,44 @@ public async Task<CertificateResult> GenerateDevCertificateAsync(
5346 // Ensure we have a proper CN format
5447 var subjectName = $ "CN={ cleanPublisher } ";
5548
56- var command = $ "$dest='{ outputPath } ';$cert=New-SelfSignedCertificate -Type Custom -Subject '{ subjectName } ' -KeyUsage DigitalSignature -FriendlyName 'MSIX Dev Certificate' -CertStoreLocation 'Cert:\\ CurrentUser\\ My' -KeyProtection None -KeyExportPolicy Exportable -Provider 'Microsoft Software Key Storage Provider' -TextExtension @('2.5.29.37={{text}}1.3.6.1.5.5.7.3.3', '2.5.29.19={{text}}') -NotAfter (Get-Date).AddDays({ validDays } ); Export-PfxCertificate -Cert $cert -FilePath $dest -Password (ConvertTo-SecureString -String '{ password } ' -Force -AsPlainText) -Force";
57-
5849 try
5950 {
60- var ( exitCode , output ) = await powerShellService . RunCommandAsync ( command , environmentVariables : GetCertificateEnvironmentVariables ( ) , cancellationToken : cancellationToken ) ;
61-
62- if ( exitCode != 0 )
51+ // 1) Create a persisted CNG key in MS Software KSP with AllowExport
52+ var creationParams = new CngKeyCreationParameters
6353 {
64- var message = $ "PowerShell command failed with exit code { exitCode } ";
65- if ( logger . IsEnabled ( LogLevel . Debug ) )
66- {
67- message += $ ": { output } ";
68- }
69- throw new InvalidOperationException ( message ) ;
54+ Provider = CngProvider . MicrosoftSoftwareKeyStorageProvider ,
55+ ExportPolicy = CngExportPolicies . AllowExport ,
56+ KeyCreationOptions = CngKeyCreationOptions . None ,
57+ KeyUsage = CngKeyUsages . Signing
58+ } ;
59+ // Set length = 2048
60+ creationParams . Parameters . Add ( new CngProperty ( "Length" , BitConverter . GetBytes ( 2048 ) , CngPropertyOptions . None ) ) ;
61+
62+ using var cngKey = CngKey . Create ( CngAlgorithm . Rsa , $ "MSIXDev-{ Guid . NewGuid ( ) } ", creationParams ) ;
63+ using var rsa = new RSACng ( cngKey ) ;
64+
65+ // 2) Build req to mirror PS flags
66+ var req = new CertificateRequest ( subjectName , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
67+ req . CertificateExtensions . Add ( new X509KeyUsageExtension ( X509KeyUsageFlags . DigitalSignature , critical : false ) ) ;
68+ req . CertificateExtensions . Add ( new X509EnhancedKeyUsageExtension (
69+ new OidCollection { new Oid ( "1.3.6.1.5.5.7.3.3" ) } , critical : false ) ) ;
70+ // BasicConstraints like PS default (non-CA, non-critical)
71+ req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( false , false , 0 , false ) ) ;
72+
73+ var notBefore = DateTimeOffset . UtcNow ;
74+ var notAfter = DateTimeOffset . UtcNow . AddDays ( validDays ) ;
75+ using var cert = req . CreateSelfSigned ( notBefore , notAfter ) ;
76+ cert . FriendlyName = "MSIX Dev Certificate" ;
77+
78+ using ( var store = new X509Store ( StoreName . My , StoreLocation . CurrentUser ) )
79+ {
80+ store . Open ( OpenFlags . ReadWrite ) ;
81+ store . Add ( cert ) ;
7082 }
7183
84+ var pfx = cert . Export ( X509ContentType . Pfx , password ) ;
85+ await File . WriteAllBytesAsync ( outputPath , pfx , cancellationToken ) ;
86+
7287 logger . LogDebug ( "Certificate generated: {OutputPath}" , outputPath ) ;
7388
7489 return new CertificateResult (
@@ -84,7 +99,7 @@ public async Task<CertificateResult> GenerateDevCertificateAsync(
8499 }
85100 }
86101
87- public async Task < bool > InstallCertificateAsync ( string certPath , string password , bool force , CancellationToken cancellationToken = default )
102+ public bool InstallCertificate ( string certPath , string password , bool force )
88103 {
89104 if ( ! Path . IsPathRooted ( certPath ) )
90105 {
@@ -103,31 +118,57 @@ public async Task<bool> InstallCertificateAsync(string certPath, string password
103118 // Check if certificate is already installed (unless force is true)
104119 if ( ! force )
105120 {
106- var certName = Path . GetFileNameWithoutExtension ( certPath ) ;
107- var checkCommand = $ "Get-ChildItem -Path 'Cert:\\ LocalMachine\\ TrustedPeople' | Where-Object {{ $_.Subject -like '*{ certName } *' }}";
108-
109121 try
110122 {
111- var ( _, result ) = await powerShellService . RunCommandAsync ( checkCommand , environmentVariables : GetCertificateEnvironmentVariables ( ) , cancellationToken : cancellationToken ) ;
112-
113- if ( ! string . IsNullOrWhiteSpace ( result ) )
123+ // Load the certificate to get its thumbprint/subject for comparison
124+ using var certToCheck = X509CertificateLoader . LoadPkcs12FromFile (
125+ certPath ,
126+ password ,
127+ X509KeyStorageFlags . Exportable ) ;
128+
129+ // Check if this certificate is already in the TrustedPeople store
130+ using var store = new X509Store ( StoreName . TrustedPeople , StoreLocation . LocalMachine ) ;
131+ store . Open ( OpenFlags . ReadOnly ) ;
132+
133+ var existingCerts = store . Certificates . Find (
134+ X509FindType . FindByThumbprint ,
135+ certToCheck . Thumbprint ,
136+ validOnly : false ) ;
137+
138+ if ( existingCerts . Count > 0 )
114139 {
115140 logger . LogDebug ( "Certificate appears to already be installed" ) ;
116141 return false ;
117142 }
118143 }
119- catch
144+ catch ( Exception ex )
120145 {
121146 // Continue with installation if check fails
147+ logger . LogDebug ( "Could not check existing certificates: {Message}" , ex . Message ) ;
122148 }
123149 }
124150
125151 // Install to TrustedPeople store (required for MSIX sideloading)
126- // Create the PowerShell command directly
152+ // Load the certificate from the PFX file
127153 var absoluteCertPath = Path . GetFullPath ( certPath ) ;
128- var installCommand = $ "Import-PfxCertificate -FilePath '{ absoluteCertPath } ' -CertStoreLocation 'Cert:\\ LocalMachine\\ TrustedPeople' -Password (ConvertTo-SecureString -String '{ password } ' -Force -AsPlainText)";
154+ using var cert = X509CertificateLoader . LoadPkcs12FromFile (
155+ absoluteCertPath ,
156+ password ,
157+ X509KeyStorageFlags . MachineKeySet | X509KeyStorageFlags . PersistKeySet ) ;
129158
130- await powerShellService . RunCommandAsync ( installCommand , elevated : true , cancellationToken : cancellationToken ) ;
159+ // Install to LocalMachine\TrustedPeople store (requires elevation)
160+ try
161+ {
162+ using var store = new X509Store ( StoreName . TrustedPeople , StoreLocation . LocalMachine ) ;
163+ store . Open ( OpenFlags . ReadWrite ) ;
164+ store . Add ( cert ) ;
165+ }
166+ catch ( CryptographicException ex ) when ( ex . Message . Contains ( "Access is denied" ) )
167+ {
168+ throw new InvalidOperationException (
169+ "Failed to install certificate: Administrator privileges are required to install certificates to the LocalMachine store. " +
170+ "Please run this command as an administrator." , ex ) ;
171+ }
131172
132173 logger . LogDebug ( "Certificate installed successfully to TrustedPeople store" ) ;
133174
@@ -286,7 +327,7 @@ record = reader.ReadEvent();
286327 {
287328 logger . LogDebug ( "Installing certificate..." ) ;
288329
289- var installResult = await InstallCertificateAsync ( result . CertificatePath , password , false , cancellationToken ) ;
330+ var installResult = InstallCertificate ( result . CertificatePath , password , false ) ;
290331 if ( installResult )
291332 {
292333 logger . LogInformation ( "✅ Certificate installed successfully!" ) ;
@@ -328,8 +369,8 @@ public static string ExtractPublisherFromCertificate(string certificatePath, str
328369
329370 try
330371 {
331- using var cert = System . Security . Cryptography . X509Certificates . X509CertificateLoader . LoadPkcs12FromFile (
332- certificatePath , password , System . Security . Cryptography . X509Certificates . X509KeyStorageFlags . Exportable ) ;
372+ using var cert = X509CertificateLoader . LoadPkcs12FromFile (
373+ certificatePath , password , X509KeyStorageFlags . Exportable ) ;
333374
334375 var subject = cert . Subject ;
335376 if ( string . IsNullOrWhiteSpace ( subject ) )
0 commit comments