Skip to content

Commit cb0a7f3

Browse files
committed
Changed certificate management to use c# APIs, and not pwsh.
1 parent def43aa commit cb0a7f3

File tree

4 files changed

+81
-40
lines changed

4 files changed

+81
-40
lines changed

src/winsdk-CLI/Winsdk.Cli/Commands/CertInstallCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ public CertInstallCommand()
3939

4040
public class Handler(ICertificateService certificateService, ILogger<CertInstallCommand> logger) : AsynchronousCommandLineAction
4141
{
42-
public override async Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
42+
public override Task<int> InvokeAsync(ParseResult parseResult, CancellationToken cancellationToken = default)
4343
{
4444
var certPath = parseResult.GetRequiredValue(CertPathArgument);
4545
var password = parseResult.GetRequiredValue(PasswordOption);
4646
var force = parseResult.GetRequiredValue(ForceOption);
4747

4848
try
4949
{
50-
var result = await certificateService.InstallCertificateAsync(certPath, password, force, cancellationToken);
50+
var result = certificateService.InstallCertificate(certPath, password, force);
5151
if (!result)
5252
{
5353
logger.LogInformation("ℹ️ Certificate is already installed");
@@ -57,12 +57,12 @@ public override async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio
5757
logger.LogInformation("✅ Certificate installed successfully!");
5858
}
5959

60-
return 0;
60+
return Task.FromResult(0);
6161
}
6262
catch (Exception error)
6363
{
6464
logger.LogError("❌ Failed to install certificate: {ErrorMessage}", error.Message);
65-
return 1;
65+
return Task.FromResult(1);
6666
}
6767
}
6868
}

src/winsdk-CLI/Winsdk.Cli/Services/CertificateService.cs

Lines changed: 75 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,18 @@
22
using System.Diagnostics.Eventing.Reader;
33
using System.Text.RegularExpressions;
44
using Microsoft.Extensions.Logging;
5+
using System.Security.Cryptography;
6+
using System.Security.Cryptography.X509Certificates;
57

68
namespace Winsdk.Cli.Services;
79

810
internal 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))

src/winsdk-CLI/Winsdk.Cli/Services/ICertificateService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public Task<CertificateResult> GenerateDevCertificateAsync(
2222
int validDays = 365,
2323
CancellationToken cancellationToken = default);
2424

25-
public Task<bool> InstallCertificateAsync(string certPath, string password, bool force, CancellationToken cancellationToken = default);
25+
public bool InstallCertificate(string certPath, string password, bool force);
2626

2727
public Task SignFileAsync(string filePath, string certificatePath, string? password = "password", string? timestampUrl = null, CancellationToken cancellationToken = default);
2828
}

src/winsdk-CLI/Winsdk.Cli/Services/MsixService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1034,7 +1034,7 @@ private async Task SignMsixPackageAsync(string outputFolder, string certificateP
10341034
// Install certificate if requested
10351035
if (installDevCert)
10361036
{
1037-
var result = await certificateService.InstallCertificateAsync(certPath, certificatePassword, false, cancellationToken);
1037+
var result = certificateService.InstallCertificate(certPath, certificatePassword, false);
10381038
}
10391039

10401040
// Sign the package

0 commit comments

Comments
 (0)