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