diff --git a/Samples/GDS/Client/Controls/ApplicationCertificateControl.Designer.cs b/Samples/GDS/Client/Controls/ApplicationCertificateControl.Designer.cs
index 8c208f972..7bf92f54d 100644
--- a/Samples/GDS/Client/Controls/ApplicationCertificateControl.Designer.cs
+++ b/Samples/GDS/Client/Controls/ApplicationCertificateControl.Designer.cs
@@ -38,6 +38,7 @@ private void InitializeComponent()
this.PrivateKeyPasswordLabel = new System.Windows.Forms.Label();
this.ApplyChangesButton = new System.Windows.Forms.Button();
this.RequestNewButton = new System.Windows.Forms.Button();
+ this.NewKeyPairFromDerButton = new System.Windows.Forms.Button();
this.CertificateRequestTimer = new System.Windows.Forms.Timer(this.components);
this.RegistrationPanel.SuspendLayout();
this.RegistrationButtonsPanel.SuspendLayout();
@@ -98,6 +99,7 @@ private void InitializeComponent()
this.RegistrationButtonsPanel.BackColor = System.Drawing.Color.MidnightBlue;
this.RegistrationButtonsPanel.Controls.Add(this.PrivateKeyPasswordTextBox);
this.RegistrationButtonsPanel.Controls.Add(this.PrivateKeyPasswordLabel);
+ this.RegistrationButtonsPanel.Controls.Add(this.NewKeyPairFromDerButton);
this.RegistrationButtonsPanel.Controls.Add(this.ApplyChangesButton);
this.RegistrationButtonsPanel.Controls.Add(this.RequestNewButton);
this.RegistrationButtonsPanel.Dock = System.Windows.Forms.DockStyle.Bottom;
@@ -159,6 +161,22 @@ private void InitializeComponent()
this.RequestNewButton.MouseEnter += new System.EventHandler(this.Button_MouseEnter);
this.RequestNewButton.MouseLeave += new System.EventHandler(this.Button_MouseLeave);
//
+ // NewKeyPairFromDerButton
+ //
+ this.NewKeyPairFromDerButton.BackColor = System.Drawing.Color.MidnightBlue;
+ this.NewKeyPairFromDerButton.Dock = System.Windows.Forms.DockStyle.Left;
+ this.NewKeyPairFromDerButton.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
+ this.NewKeyPairFromDerButton.ForeColor = System.Drawing.Color.White;
+ this.NewKeyPairFromDerButton.Location = new System.Drawing.Point(258, 0);
+ this.NewKeyPairFromDerButton.Name = "NewKeyPairFromDerButton";
+ this.NewKeyPairFromDerButton.Size = new System.Drawing.Size(160, 32);
+ this.NewKeyPairFromDerButton.TabIndex = 7;
+ this.NewKeyPairFromDerButton.Text = "New Key Pair (.pfx)";
+ this.NewKeyPairFromDerButton.UseVisualStyleBackColor = false;
+ this.NewKeyPairFromDerButton.Click += new System.EventHandler(this.NewKeyPairFromDerButton_Click);
+ this.NewKeyPairFromDerButton.MouseEnter += new System.EventHandler(this.Button_MouseEnter);
+ this.NewKeyPairFromDerButton.MouseLeave += new System.EventHandler(this.Button_MouseLeave);
+ //
// CertificateRequestTimer
//
this.CertificateRequestTimer.Interval = 5000;
@@ -183,6 +201,7 @@ private void InitializeComponent()
private System.Windows.Forms.Panel RegistrationPanel;
private System.Windows.Forms.Panel RegistrationButtonsPanel;
private System.Windows.Forms.Button RequestNewButton;
+ private System.Windows.Forms.Button NewKeyPairFromDerButton;
private Opc.Ua.Gds.Client.Controls.EditValueCtrl CertificateControl;
private System.Windows.Forms.Label WarningLabel;
private System.Windows.Forms.Timer CertificateRequestTimer;
diff --git a/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs b/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs
index 1abcd0ae8..bf9073a43 100644
--- a/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs
+++ b/Samples/GDS/Client/Controls/ApplicationCertificateControl.cs
@@ -29,6 +29,7 @@
using Opc.Ua.Security.Certificates;
using System;
+using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
@@ -195,6 +196,146 @@ private async void RequestNewButton_Click(object sender, EventArgs e)
}
}
+ private async void NewKeyPairFromDerButton_Click(object sender, EventArgs e)
+ {
+ await CreatePfxFromCertificateInfoAsync().ConfigureAwait(true);
+ }
+
+ ///
+ /// Creates a new .pfx (with a fresh public/private key pair) from the information
+ /// contained in the currently loaded certificate.
+ ///
+ ///
+ /// The subject name, application URI(s) and domain names (Subject Alternative Name) of the
+ /// loaded certificate are reused, while a brand new RSA key pair is generated. This makes it
+ /// possible to turn a public-only certificate (e.g. loaded from a .der file) into a
+ /// usable .pfx that carries a private key.
+ ///
+ private async Task CreatePfxFromCertificateInfoAsync()
+ {
+ try
+ {
+ if (m_certificate == null)
+ {
+ MessageBox.Show(
+ Parent,
+ "No certificate is loaded. Load a public certificate (e.g. a .der file) first.",
+ Parent?.Text,
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Warning);
+ return;
+ }
+
+ // Clone the subject and the domain/application URI information from the loaded certificate.
+ string subjectName = m_certificate.Subject;
+ Certificate certificateInfo = Certificate.From(m_certificate);
+ IList domainNames = X509Utils.GetDomainsFromCertificate(certificateInfo).ToList();
+ IReadOnlyList applicationUris = X509Utils.GetApplicationUrisFromCertificate(certificateInfo);
+ ushort keySize = (ushort)(m_certificate.GetRSAPublicKey()?.KeySize ?? X509Defaults.RSAKeySize);
+
+ ICertificateBuilder builder = DefaultCertificateFactory.Instance.CreateCertificate(subjectName)
+ .SetNotBefore(DateTime.Today.AddDays(-1))
+ .SetNotAfter(DateTime.Today.AddYears(1));
+
+ // Reuse the Subject Alternative Name (application URI + domains) of the original certificate.
+ if (domainNames.Count > 0 || applicationUris.Count > 0)
+ {
+ builder = builder.AddExtension(
+ new X509SubjectAltNameExtension(
+ applicationUris.Count > 0 ? applicationUris[0] : string.Empty,
+ domainNames));
+ }
+
+ // Generate a new key pair for the cloned certificate information.
+ X509Certificate2 newCertificate = builder
+ .SetRSAKeySize(keySize)
+ .CreateForRSA()
+ .AsX509Certificate2();
+
+ string savePath;
+ #pragma warning disable CA1849 // Justification: Synchronous WinForms sample handler preserves existing behavior.
+ using (SaveFileDialog dialog = new SaveFileDialog {
+ Title = "Save new PFX certificate",
+ Filter = "PKCS#12 files (*.pfx)|*.pfx|All files (*.*)|*.*",
+ DefaultExt = "pfx",
+ FileName = GetDefaultPfxFileName(subjectName),
+ OverwritePrompt = true
+ })
+ {
+ if (dialog.ShowDialog(this) != DialogResult.OK)
+ {
+ newCertificate.Dispose();
+ return;
+ }
+
+ savePath = dialog.FileName;
+ }
+
+ string password = string.IsNullOrEmpty(m_certificatePassword) ? null : m_certificatePassword;
+ byte[] pfx = newCertificate.Export(X509ContentType.Pfx, password);
+ File.WriteAllBytes(savePath, pfx);
+ #pragma warning restore CA1849
+
+ if (m_temporaryCertificateCreated)
+ {
+ m_certificate.Dispose();
+ m_temporaryCertificateCreated = false;
+ }
+ m_certificate = newCertificate;
+
+ var wrapper = new CertificateWrapper() { Certificate = Certificate.From(newCertificate) };
+ CertificateControl.ShowValue(TypeInfo.Construct(wrapper), "Application Certificate", wrapper, true);
+ WarningLabel.Visible = false;
+
+ MessageBox.Show(
+ Parent,
+ "A new .pfx with a fresh key pair was created from the certificate information and saved to:\n" + savePath,
+ Parent?.Text,
+ MessageBoxButtons.OK,
+ MessageBoxIcon.Information);
+
+ await Task.CompletedTask.ConfigureAwait(true);
+ }
+ catch (Exception ex)
+ {
+ #pragma warning disable CA1849 // Justification: Synchronous WinForms sample handler preserves existing behavior.
+ Opc.Ua.Client.Controls.ExceptionDlg.Show(m_telemetry, Text, ex);
+ #pragma warning restore CA1849
+ }
+ }
+
+ ///
+ /// Builds a sensible default file name for the exported .pfx based on the certificate subject.
+ ///
+ private static string GetDefaultPfxFileName(string subjectName)
+ {
+ string commonName = null;
+ if (!String.IsNullOrEmpty(subjectName))
+ {
+ foreach (string part in subjectName.Split(','))
+ {
+ string trimmed = part.Trim();
+ if (trimmed.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
+ {
+ commonName = trimmed.Substring(3).Trim();
+ break;
+ }
+ }
+ }
+
+ if (String.IsNullOrEmpty(commonName))
+ {
+ commonName = "certificate";
+ }
+
+ foreach (char invalid in Path.GetInvalidFileNameChars())
+ {
+ commonName = commonName.Replace(invalid, '_');
+ }
+
+ return commonName + ".pfx";
+ }
+
private async Task RequestNewCertificatePushModeAsync(object sender, EventArgs e)
{
try