Skip to content

Commit

Permalink
Added a Refresh button. Added an Open menu command for each certifica…
Browse files Browse the repository at this point in the history
…te. Made the Apply button disabled when there are no changes to apply. Added file conversion and validation instructions to Readme.
  • Loading branch information
Aldaviva committed May 8, 2021
1 parent 2deb454 commit 1cda6e0
Show file tree
Hide file tree
Showing 15 changed files with 182 additions and 37 deletions.
49 changes: 44 additions & 5 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,67 @@

This GUI program lets you choose the certificate to use for your Remote Desktop Services connection.

In Windows Server 2008 R2 and earlier, this functionality was available in the Remote Desktop Session Host Configuration (`tsconfig.msc`), but it was removed in Windows Server 2012 and later, which means you can only configure it programmatically using WMI.
In Windows Server 2008 R2 and earlier, this functionality was available in the Remote Desktop Session Host Configuration (`tsconfig.msc`), but it was removed in Windows Server 2012 and later, which means you can only [configure it programmatically using WMI](https://serverfault.com/a/444287/227008).

![screenshot](https://i.imgur.com/Z9jFnCY.png)
![screenshot](https://i.imgur.com/SmXGPZR.png)

<!-- MarkdownTOC autolink="true" bracket="round" autoanchor="true" levels="1,2,3" style="ordered" -->

1. [Requirements](#requirements)
1. [Installation](#installation)
1. [Certificate Conversion](#certificate-conversion)
1. [Usage](#usage)
1. [Validation](#validation)

<!-- /MarkdownTOC -->

<a id="requirements"></a>
## Requirements

- Windows (tested on Server 2019)
- [.NET Framework 4.8](https://dotnet.microsoft.com/download/dotnet-framework) or later

<a id="installation"></a>
## Installation

1. Download the latest EXE file from [Releases](https://github.com/Aldaviva/RemoteDesktopServicesCertificateSelector/releases).

It's a portable application. You can save it, run it, and then delete it when you're done. It won't leave any files or registry values behind.

<a id="certificate-conversion"></a>
## Certificate Conversion
The private key and public certificate is required for servers like Remote Desktop Services. These must be imported into a Windows certificate store using PCKS#12 format, which uses the PFX file extension.

If you have an X.509 certificate that you want to import, like a PEM-encoded RSA keypair, you will first need to temporarily convert it to PKCS#12.

```sh
openssl pkcs12 -in "mypubliccert.pem.crt" -inkey "myprivatekey.pem.key" -out "mycertandkey.pfx" -export
```

This PFX file is the one to import into Certificates, not the CRT file. Be aware that the Certificate Import Wizard defaults to only showing X.509 keys (CER, CRT), so it's easy to accidentally import only the public key and therefore be unable to use it for your server. Be sure to change the file type dropdown in the Open dialog box to Personal Information Exchange so that your PFX file is shown.

After importing the PFX file, you can delete it from disk.

<a id="usage"></a>
## Usage

1. Install your new certificate into Certificates (Local Computer) › Personal › Certificates. You can open this certificate store by clicking **![certs](https://raw.githubusercontent.com/Aldaviva/RemoteDesktopServicesCertificateSelector/master/RemoteDesktopServicesCertificateSelector/Resources/certs.png) Manage Local Computer Certificates** in this program.
1. Run `RemoteDesktopServicesCertificateSelector.exe`.
1. If you haven't already done so, install your new certificate into Certificates (Local Computer) › Personal › Certificates.
- You can open this certificate store by clicking **![certs](https://raw.githubusercontent.com/Aldaviva/RemoteDesktopServicesCertificateSelector/master/RemoteDesktopServicesCertificateSelector/Resources/certs.ico) Manage Local Computer Certificates**.
- Once it's installed, click **![refresh](https://raw.githubusercontent.com/Aldaviva/RemoteDesktopServicesCertificateSelector/master/RemoteDesktopServicesCertificateSelector/Resources/refresh.png) Refresh** in this program to show the newly-installed certificate.
1. Click the radio button for the certificate you want to use on your RDP connections.
1. Click **![save](https://raw.githubusercontent.com/Aldaviva/RemoteDesktopServicesCertificateSelector/master/RemoteDesktopServicesCertificateSelector/Resources/save.ico) Apply**.

New connections to your `RDP-tcp` listener will now use the new certificate.
New connections to your `RDP-tcp` listener will now use the new certificate. This change takes effect immediately for any new connections; you don't need to restart any services or your computer.

You can view a certificate or copy its thumbprint by right-clicking on a row.

<a id="validation"></a>
## Validation

To test the new certificate, you can reconnect using `mstsc.exe` and click the 🔒 button in the fullscreen toolbar.

You can copy a certificate's thumbprint by right-clicking on a row and selecting **![copy](https://raw.githubusercontent.com/Aldaviva/RemoteDesktopServicesCertificateSelector/master/RemoteDesktopServicesCertificateSelector/Resources/copy.ico) Copy thumbprint (SHA-1)**.
You can also test it with `openssl`:
```sh
echo | openssl s_client -connect myserver.com:3389 2>/dev/null | openssl x509 -noout -text
```
13 changes: 9 additions & 4 deletions RemoteDesktopServicesCertificateSelector/Data/Certificate.cs
Original file line number Diff line number Diff line change
@@ -1,31 +1,36 @@
#nullable enable

using System;
using System.Security.Cryptography.X509Certificates;

namespace RemoteDesktopServicesCertificateSelector.Data {

public class Certificate {

public string? name { get; }
public string? issuerName { get; }

public DateTime expirationDate { get; }

// public bool isExpired => expirationDate <= DateTime.Now;
public string thumbprint { get; }
public bool isDefaultSelfSigned { get; }
internal X509Certificate2? windowsCertificate { get; }

public Certificate(string thumbprint) {
this.thumbprint = thumbprint;
}

public Certificate(string thumbprint, string name, string issuerName, DateTime expirationDate, bool isDefaultSelfSigned): this(thumbprint) {
private Certificate(string thumbprint, string name, string issuerName, DateTime expirationDate, bool isDefaultSelfSigned): this(thumbprint) {
this.name = name;
this.issuerName = issuerName;
this.expirationDate = expirationDate;
this.isDefaultSelfSigned = isDefaultSelfSigned;
}

public Certificate(X509Certificate2 cert, bool isDefaultSelfSigned): this(cert.Thumbprint!,
string.IsNullOrWhiteSpace(cert.FriendlyName) ? cert.GetNameInfo(X509NameType.SimpleName, false)! : cert.FriendlyName,
cert.GetNameInfo(X509NameType.SimpleName, true)!, cert.NotAfter, isDefaultSelfSigned) {
windowsCertificate = cert;
}

protected bool Equals(Certificate other) {
return thumbprint == other.thumbprint;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public interface CertificateManager {

Task launchCertificateManagementConsole();

void openCertificate(Certificate certificate);

}

public class CertificateManagerImpl: CertificateManager, IDisposable {
Expand Down Expand Up @@ -53,8 +55,7 @@ public void Dispose() {
IEnumerable<Certificate> certificates = store.Certificates
.Find(X509FindType.FindByApplicationPolicy, Oid.FromFriendlyName("Server Authentication", OidGroup.EnhancedKeyUsage).Value, false)
.Cast<X509Certificate2>()
.Select(cert => new Certificate(cert.Thumbprint!, string.IsNullOrWhiteSpace(cert.FriendlyName) ? cert.GetNameInfo(X509NameType.SimpleName, false)! : cert.FriendlyName,
cert.GetNameInfo(X509NameType.SimpleName, true)!, cert.NotAfter, store.Name == REMOTE_DESKTOP_STORE));
.Select(cert => new Certificate(cert, store.Name == REMOTE_DESKTOP_STORE));
store.Close();
return certificates;
});
Expand Down Expand Up @@ -90,6 +91,12 @@ public async Task launchCertificateManagementConsole() {
mostRecentMscFilename = null;
}

public void openCertificate(Certificate certificate) {
if (certificate.windowsCertificate is not null) {
X509Certificate2UI.DisplayCertificate(certificate.windowsCertificate);
}
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,5 @@
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.1.0")]
[assembly: AssemblyFileVersion("1.0.1.0")]
[assembly: AssemblyVersion("1.1.0.0")]
[assembly: AssemblyFileVersion("1.1.0.0")]
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Drawing" />
<Reference Include="System.Security" />
<Reference Include="System.Xml" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Core" />
Expand Down Expand Up @@ -85,6 +86,7 @@
<SubType>Code</SubType>
</Compile>
<Compile Include="Data\Certificate.cs" />
<Compile Include="Views\Converters\EnabledToOpacityConverter.cs" />
<Compile Include="Managers\CertificateManager.cs" />
<Compile Include="ViewModels\CertificateViewModel.cs" />
<Compile Include="ViewModels\MainWindowViewModel.cs" />
Expand All @@ -108,6 +110,7 @@
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
<SubType>Designer</SubType>
</EmbeddedResource>
<None Include="ILRepack.targets" />
<None Include="Properties\app.manifest" />
Expand All @@ -125,14 +128,19 @@
<ItemGroup>
<Resource Include="Resources\terminalServicesManagement.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\certs.png" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\copy.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\save.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\cert.ico" />
<Resource Include="Resources\certs.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\refresh.png" />
</ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
using System.Windows;
using Prism.Commands;
using RemoteDesktopServicesCertificateSelector.Data;
using RemoteDesktopServicesCertificateSelector.Managers;

#nullable enable

namespace RemoteDesktopServicesCertificateSelector.ViewModels {

public class CertificateViewModel: INotifyPropertyChanged {

private readonly CertificateManager certificateManager;

public Certificate certificate { get; }

public string? name => certificate.name;
Expand All @@ -32,9 +35,17 @@ public bool isActive {

public DelegateCommand copyThumbprintCommand { get; }

public CertificateViewModel(Certificate certificate) {
this.certificate = certificate;
copyThumbprintCommand = new DelegateCommand(copyThumbprint);
public DelegateCommand openCertificateCommand { get; }

public CertificateViewModel(Certificate certificate, CertificateManager certificateManager) {
this.certificateManager = certificateManager;
this.certificate = certificate;
copyThumbprintCommand = new DelegateCommand(copyThumbprint);
openCertificateCommand = new DelegateCommand(openCertificate);
}

private void openCertificate() {
certificateManager.openCertificate(certificate);
}

private void copyThumbprint() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#nullable enable

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using Microsoft.Management.Infrastructure;
Expand All @@ -19,6 +20,7 @@ public class MainWindowViewModel: BindableBase {

public DelegateCommand manageCertificatesCommand { get; }
public DelegateCommand saveCommand { get; }
public DelegateCommand refreshCommand { get; }

public string title { get; } = "Remote Desktop Services Certificate Selector";

Expand All @@ -27,17 +29,17 @@ public class MainWindowViewModel: BindableBase {
public MainWindowViewModel(CertificateManager certificateManager) {
this.certificateManager = certificateManager;

Certificate activeCertificate = certificateManager.activeTerminalServicesCertificate;
installedCertificates.AddRange(certificateManager.installedCertificates
.Select(certificate => new CertificateViewModel(certificate) { isActive = certificate == activeCertificate }));

manageCertificatesCommand = new DelegateCommand(manageCertificates);
saveCommand = new DelegateCommand(save);
saveCommand = new DelegateCommand(save, isDirty);
refreshCommand = new DelegateCommand(refresh);

refresh();
}

public void setActive(CertificateViewModel selectedItem) {
if (activeCertificateViewModel is not null) {
activeCertificateViewModel.isActive = false;
CertificateViewModel? oldActiveCertificate = activeCertificateViewModel;
if (oldActiveCertificate is not null) {
oldActiveCertificate.isActive = false;
}

selectedItem.isActive = true;
Expand All @@ -51,7 +53,8 @@ private void save() {
if (activeCertificateViewModel?.certificate is { } selectedCertificate) {
try {
certificateManager.activeTerminalServicesCertificate = selectedCertificate;
MessageBox.Show($"Remote Desktop Connection is now using the \"{selectedCertificate.name}\" certificate.",
saveCommand.RaiseCanExecuteChanged();
MessageBox.Show($"Remote Desktop Services are now using the \"{selectedCertificate.name}\" certificate, which expires on {selectedCertificate.expirationDate:d}.",
"Connection updated", MessageBoxButton.OK, MessageBoxImage.Information);
} catch (CimException e) {
MessageBox.Show(e.Message, "Failed to change certificate", MessageBoxButton.OK, MessageBoxImage.Error);
Expand All @@ -61,6 +64,34 @@ private void save() {
}
}

private bool isDirty() {
return !certificateManager.activeTerminalServicesCertificate.Equals(activeCertificateViewModel?.certificate);
}

private void refresh() {
Certificate activeCertificate = certificateManager.activeTerminalServicesCertificate;

foreach (CertificateViewModel installedCertificate in installedCertificates) {
installedCertificate.PropertyChanged -= onCertificateViewModelChanged;
}

installedCertificates.Clear();
installedCertificates.AddRange(certificateManager.installedCertificates
.Select(certificate => {
var certificateViewModel = new CertificateViewModel(certificate, certificateManager) { isActive = certificate == activeCertificate };
certificateViewModel.PropertyChanged += onCertificateViewModelChanged;
return certificateViewModel;
}));

saveCommand.RaiseCanExecuteChanged();
}

private void onCertificateViewModelChanged(object sender, PropertyChangedEventArgs args) {
if (args.PropertyName == nameof(CertificateViewModel.isActive)) {
saveCommand.RaiseCanExecuteChanged();
}
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;
using System.Globalization;
using System.Windows.Data;

#nullable enable

namespace RemoteDesktopServicesCertificateSelector.Views.Converters {

public class EnabledToOpacityConverter: IValueConverter {

private const double ENABLED_OPACITY = 1.0;
private const double DISABLED_OPACITY = 0.5;

public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return value switch {
bool v => v ? ENABLED_OPACITY : DISABLED_OPACITY,
_ => ENABLED_OPACITY
};
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return value switch {
double v => v >= ENABLED_OPACITY,
_ => true
};
}

}

}
14 changes: 10 additions & 4 deletions RemoteDesktopServicesCertificateSelector/Views/IconButton.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:converters="clr-namespace:RemoteDesktopServicesCertificateSelector.Views.Converters"
mc:Ignorable="d"
x:Name="root"
x:Name="root"
d:DesignHeight="24" d:DesignWidth="200">
<Button Height="24" HorizontalContentAlignment="Left" Padding="5,1,5,1" Command="{Binding ElementName=root, Path=Command}" Click="onButtonClick">
<UserControl.Resources>
<converters:EnabledToOpacityConverter x:Key="enabledToOpacityConverter" />
</UserControl.Resources>

<Button Height="24" HorizontalContentAlignment="Left" Padding="5,1,6,1" Command="{Binding ElementName=root, Path=Command}" Click="onButtonClick">
<StackPanel Orientation="Horizontal">
<Image Height="16" Margin="0,0,5,0" Source="{Binding ElementName=root, Path=Icon}" />
<!-- Could make the icon grayscale on disabled using https://www.renebergelt.de/blog/2019/10/automatically-grayscale-images-on-disabled-wpf-buttons/ -->
<Image Height="16" Margin="0,0,6,0" Source="{Binding ElementName=root, Path=Icon}" Opacity="{Binding RelativeSource={RelativeSource AncestorType=Button, Mode=FindAncestor}, Path=IsEnabled, Converter={StaticResource enabledToOpacityConverter}}" />
<TextBlock Text="{Binding ElementName=root, Path=Text}" />
</StackPanel>
</Button>
Expand Down
Loading

0 comments on commit 1cda6e0

Please sign in to comment.