internal async Task <bool> CreateCertificate(string TabID) { try { string URL = this.customCA ? this.acmeDirectory : "https://acme-v02.api.letsencrypt.org/directory"; RSAParameters Parameters; CspParameters CspParams = new CspParameters() { Flags = CspProviderFlags.UseMachineKeyStore, KeyContainerName = "IoTGateway:" + URL }; try { bool Ok; using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { Parameters = RSA.ExportParameters(true); if (RSA.KeySize < 4096) { RSA.PersistKeyInCsp = false; RSA.Clear(); Ok = false; } else { Ok = true; } } if (!Ok) { using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { Parameters = RSA.ExportParameters(true); } } } catch (CryptographicException ex) { throw new CryptographicException("Unable to get access to cryptographic key for \"IoTGateway:" + URL + "\". Was the database created using another user?", ex); } using (AcmeClient Client = new AcmeClient(new Uri(URL), Parameters)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Connecting to directory.", false, "User"); AcmeDirectory AcmeDirectory = await Client.GetDirectory(); if (AcmeDirectory.ExternalAccountRequired) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "An external account is required.", false, "User"); } if (AcmeDirectory.TermsOfService != null) { URL = AcmeDirectory.TermsOfService.ToString(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Terms of service available on: " + URL, false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "TermsOfService", URL, false, "User"); this.urlToS = URL; if (!this.acceptToS) { ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "You need to accept the terms of service.", false, "User"); return(false); } } if (AcmeDirectory.Website != null) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Web site available on: " + AcmeDirectory.Website.ToString(), false, "User"); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Getting account.", false, "User"); List <string> Names = new List <string>(); if (!string.IsNullOrEmpty(this.domain)) { Names.Add(this.domain); } if (this.alternativeDomains != null) { foreach (string Name in this.alternativeDomains) { if (!Names.Contains(Name)) { Names.Add(Name); } } } string[] DomainNames = Names.ToArray(); AcmeAccount Account; try { Account = await Client.GetAccount(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account found.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Created: " + Account.CreatedAt.ToString(), false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Initial IP: " + Account.InitialIp, false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Status: " + Account.Status.ToString(), false, "User"); if (string.IsNullOrEmpty(this.contactEMail)) { if (Account.Contact != null && Account.Contact.Length != 0) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Updating contact URIs in account.", false, "User"); Account = await Account.Update(new string[0]); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account updated.", false, "User"); } } else { if (Account.Contact is null || Account.Contact.Length != 1 || Account.Contact[0] != "mailto:" + this.contactEMail) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Updating contact URIs in account.", false, "User"); Account = await Account.Update(new string[] { "mailto:" + this.contactEMail }); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account updated.", false, "User"); } } } catch (AcmeAccountDoesNotExistException) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account not found.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Creating account.", false, "User"); Account = await Client.CreateAccount(string.IsNullOrEmpty(this.contactEMail)?new string[0] : new string[] { "mailto:" + this.contactEMail }, this.acceptToS); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Account created.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Status: " + Account.Status.ToString(), false, "User"); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Generating new key.", false, "User"); await Account.NewKey(); using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096, CspParams)) { RSA.ImportParameters(Client.ExportAccountKey(true)); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "New key generated.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Creating order.", false, "User"); AcmeOrder Order = await Account.OrderCertificate(DomainNames, null, null); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Order created.", false, "User"); foreach (AcmeAuthorization Authorization in await Order.GetAuthorizations()) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Processing authorization for " + Authorization.Value, false, "User"); AcmeChallenge Challenge; bool Acknowledged = false; int Index = 1; int NrChallenges = Authorization.Challenges.Length; for (Index = 1; Index <= NrChallenges; Index++) { Challenge = Authorization.Challenges[Index - 1]; if (Challenge is AcmeHttpChallenge HttpChallenge) { this.challenge = "/" + HttpChallenge.Token; this.token = HttpChallenge.KeyAuthorization; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Acknowleding challenge.", false, "User"); Challenge = await HttpChallenge.AcknowledgeChallenge(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Challenge acknowledged: " + Challenge.Status.ToString(), false, "User"); Acknowledged = true; } } if (!Acknowledged) { ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "No automated method found to respond to any of the authorization challenges.", false, "User"); return(false); } AcmeAuthorization Authorization2 = Authorization; do { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Waiting to poll authorization status.", false, "User"); await Task.Delay(5000); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Polling authorization.", false, "User"); Authorization2 = await Authorization2.Poll(); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Authorization polled: " + Authorization2.Status.ToString(), false, "User"); }while (Authorization2.Status == AcmeAuthorizationStatus.pending); if (Authorization2.Status != AcmeAuthorizationStatus.valid) { switch (Authorization2.Status) { case AcmeAuthorizationStatus.deactivated: throw new Exception("Authorization deactivated."); case AcmeAuthorizationStatus.expired: throw new Exception("Authorization expired."); case AcmeAuthorizationStatus.invalid: throw new Exception("Authorization invalid."); case AcmeAuthorizationStatus.revoked: throw new Exception("Authorization revoked."); default: throw new Exception("Authorization not validated."); } } } using (RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(4096)) // TODO: Make configurable { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Finalizing order.", false, "User"); SignatureAlgorithm SignAlg = new RsaSha256(RSA); Order = await Order.FinalizeOrder(new CertificateRequest(SignAlg) { CommonName = this.domain, SubjectAlternativeNames = DomainNames, EMailAddress = this.contactEMail }); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Order finalized: " + Order.Status.ToString(), false, "User"); if (Order.Status != AcmeOrderStatus.valid) { switch (Order.Status) { case AcmeOrderStatus.invalid: throw new Exception("Order invalid."); default: throw new Exception("Unable to validate oder."); } } if (Order.Certificate is null) { throw new Exception("No certificate URI provided."); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Downloading certificate.", false, "User"); X509Certificate2[] Certificates = await Order.DownloadCertificate(); X509Certificate2 Certificate = Certificates[0]; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Exporting certificate.", false, "User"); this.certificate = Certificate.Export(X509ContentType.Cert); this.privateKey = RSA.ExportCspBlob(true); this.pfx = null; this.password = string.Empty; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Adding private key.", false, "User"); try { Certificate.PrivateKey = RSA; } catch (PlatformNotSupportedException) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Platform does not support adding of private key.", false, "User"); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Searching for OpenSSL on machine.", false, "User"); string[] Files; string Password = Hashes.BinaryToString(Gateway.NextBytes(32)); string CertFileName = null; string CertFileName2 = null; string KeyFileName = null; if (string.IsNullOrEmpty(this.openSslPath) || !File.Exists(this.openSslPath)) { Files = GetFiles(new string(Path.DirectorySeparatorChar, 1), "openssl.exe"); if (Files is null) { List <string> Files2 = new List <string>(); Files = GetFiles(Environment.SpecialFolder.ProgramFiles, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonProgramFilesX86, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.Programs, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.System, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.SystemX86, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.Windows, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonProgramFiles, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonProgramFilesX86, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Environment.SpecialFolder.CommonPrograms, "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Path.DirectorySeparatorChar + "OpenSSL-Win32", "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = GetFiles(Path.DirectorySeparatorChar + "OpenSSL-Win64", "openssl.exe"); if (Files != null) { Files2.AddRange(Files); } Files = Files2.ToArray(); } } else { Files = new string[] { this.openSslPath } }; try { if (Files.Length == 0) { ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "Unable to join certificate with private key. Try installing <a target=\"_blank\" href=\"https://wiki.openssl.org/index.php/Binaries\">OpenSSL</a> and try again.", false, "User"); return(false); } else { foreach (string OpenSslFile in Files) { if (CertFileName is null) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Generating temporary certificate file.", false, "User"); StringBuilder PemOutput = new StringBuilder(); byte[] Bin = Certificate.Export(X509ContentType.Cert); PemOutput.AppendLine("-----BEGIN CERTIFICATE-----"); PemOutput.AppendLine(Convert.ToBase64String(Bin, Base64FormattingOptions.InsertLineBreaks)); PemOutput.AppendLine("-----END CERTIFICATE-----"); CertFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.pem"); File.WriteAllText(CertFileName, PemOutput.ToString(), Encoding.ASCII); ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Generating temporary key file.", false, "User"); DerEncoder KeyOutput = new DerEncoder(); SignAlg.ExportPrivateKey(KeyOutput); PemOutput.Clear(); PemOutput.AppendLine("-----BEGIN RSA PRIVATE KEY-----"); PemOutput.AppendLine(Convert.ToBase64String(KeyOutput.ToArray(), Base64FormattingOptions.InsertLineBreaks)); PemOutput.AppendLine("-----END RSA PRIVATE KEY-----"); KeyFileName = Path.Combine(Gateway.AppDataFolder, "Certificate.key"); File.WriteAllText(KeyFileName, PemOutput.ToString(), Encoding.ASCII); } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Converting to PFX using " + OpenSslFile, false, "User"); Process P = new Process() { StartInfo = new ProcessStartInfo() { FileName = OpenSslFile, Arguments = "pkcs12 -nodes -export -out Certificate.pfx -inkey Certificate.key -in Certificate.pem -password pass:"******"ShowStatus", "Output: " + P.StandardOutput.ReadToEnd(), false, "User"); } if (!P.StandardError.EndOfStream) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Error: " + P.StandardError.ReadToEnd(), false, "User"); } continue; } ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Loading PFX.", false, "User"); CertFileName2 = Path.Combine(Gateway.AppDataFolder, "Certificate.pfx"); this.pfx = File.ReadAllBytes(CertFileName2); this.password = Password; this.openSslPath = OpenSslFile; ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "PFX successfully generated using OpenSSL.", false, "User"); break; } if (this.pfx is null) { this.openSslPath = string.Empty; ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "Unable to convert to PFX using OpenSSL.", false, "User"); return(false); } } } finally { if (CertFileName != null && File.Exists(CertFileName)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Deleting temporary certificate file.", false, "User"); File.Delete(CertFileName); } if (KeyFileName != null && File.Exists(KeyFileName)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Deleting temporary key file.", false, "User"); File.Delete(KeyFileName); } if (CertFileName2 != null && File.Exists(CertFileName2)) { ClientEvents.PushEvent(new string[] { TabID }, "ShowStatus", "Deleting temporary pfx file.", false, "User"); File.Delete(CertFileName2); } } } if (this.Step < 2) { this.Step = 2; } this.Updated = DateTime.Now; await Database.Update(this); ClientEvents.PushEvent(new string[] { TabID }, "CertificateOk", string.Empty, false, "User"); Gateway.UpdateCertificate(this); return(true); } } } catch (Exception ex) { Log.Critical(ex); ClientEvents.PushEvent(new string[] { TabID }, "CertificateError", "Unable to create certificate: " + XML.HtmlValueEncode(ex.Message), false, "User"); return(false); } finally { this.inProgress = false; } }