public void displayOptions(IConfig config) { if (!config.isThereConfigParam("accountKey")) { Console.WriteLine("WinCertes is not configured yet"); return; } IDNSChallengeValidator dnsChallengeValidator = DNSChallengeValidatorFactory.GetDNSChallengeValidator(config); Console.WriteLine("Service URI:\t" + ((ServiceUri == null) ? Certes.Acme.WellKnownServers.LetsEncryptV2.ToString() : ServiceUri)); Console.WriteLine("Account Email:\t" + Email); Console.WriteLine("Registered:\t" + (config.ReadIntParameter("registered") == 1 ? "yes" : "no")); if (dnsChallengeValidator != null) { Console.WriteLine("Auth. Mode:\tdns-01 validation"); } else { Console.WriteLine("Auth. Mode:\t" + (Standalone ? "http-01 validation standalone" : "http-01 validation with external web server")); if (Standalone) { Console.WriteLine("HTTP Port:\t" + HttpPort); } else { Console.WriteLine("Web Root:\t" + WebRoot); } } Console.WriteLine("IIS Bind Name:\t" + (BindName ?? "none")); Console.WriteLine("Import in CSP:\t" + (config.isThereConfigParam("noCsp") ? "no" : "yes")); Console.WriteLine("PS Script File:\t" + (ScriptFile ?? "none")); Console.WriteLine("Renewal Delay:\t" + RenewalDelay + " days"); Console.WriteLine("Task Scheduled:\t" + (Utils.IsScheduledTaskCreated() ? "yes" : "no")); Console.WriteLine("Cert Enrolled:\t" + (config.isThereConfigParam("certSerial") ? "yes" : "no")); }
/// <summary> /// Builds the DNS Challenge validator. For now only ACME DNS is supported. /// </summary> /// <param name="config"></param> /// <returns></returns> public static IDNSChallengeValidator GetDNSChallengeValidator(IConfig config) { IDNSChallengeValidator challengeValidator = null; if (config.ReadStringParameter("DNSValidatorType") == null) { return(null); } if (config.ReadStringParameter("DNSValidatorType") == "acme-dns") { challengeValidator = new DNSChallengeAcmeDnsValidator(config); } if (config.ReadStringParameter("DNSValidatorType") == "win-dns") { challengeValidator = new DNSChallengeWinDnsValidator(config); } return(challengeValidator); }
/// <summary> /// Builds the DNS Challenge validator. For now only ACME DNS is supported. /// </summary> /// <returns>challengeValidator instance</returns> public static IDNSChallengeValidator GetDNSChallengeValidator() { string dnsValidatorType = Program._winCertesOptions.DNSValidatorType; IDNSChallengeValidator challengeValidator = null; if (dnsValidatorType == null || dnsValidatorType.Length < 1) { return(null); } if (dnsValidatorType == "acme-dns") { challengeValidator = new DNSChallengeAcmeDnsValidator(); } if (dnsValidatorType == "win-dns") { challengeValidator = new DNSChallengeWinDnsValidator(); } return(challengeValidator); }
/// <summary> /// Display the active options/settings for this Certificate as stored in the registry /// </summary> public void DisplayOptions() { _logger.Info("Displaying WinCertes current configuration:"); _logger.Info("[{0}]", Registry.FullRegistryKey); IDNSChallengeValidator dnsChallengeValidator = DNSChallengeValidatorFactory.GetDNSChallengeValidator(); string ui = ServiceUri.ToString(); _logger.Info("Service URI:\t\t{0}", (ui == null) ? Certes.Acme.WellKnownServers.LetsEncryptV2.ToString() : ui); _logger.Info("Domain(s):\t\t{0}", Domains.Count > 0 ? string.Join(",", Domains) : "ERROR none specified"); _logger.Info("Account Email:\t{0}", AccountEmail == null ? "ERROR not set" : AccountEmail); string accountKey = Registry.ReadStringParameter("AccountKey"); _logger.Info("AccountKey:\t\t{0}", (accountKey == null || accountKey.Length < 1) ? "Account not registered" : "PrivateKey stored in registry"); _logger.Info("Registered:\t\t{0}", Registry.ReadIntParameter("Registered") == 1 ? "yes" : "no"); _logger.Info("Generated:\t\t{0}", Registry.ReadIntParameter("Generated") == 1 ? "yes" : "no"); if (dnsChallengeValidator != null) { _logger.Info("Auth. Mode:\t\tdns-01 validation"); } else { _logger.Info("Auth. Mode:\t\t{0}", Standalone ? "http-01 validation standalone" : "http-01 validation with external web server"); if (Standalone) { _logger.Info("HTTP Port:\t\t{0}", HttpPort); } else { _logger.Info("Web Root:\t\t{0}", WebRoot != null ? WebRoot : Standalone ? "NA" : "ERROR: Missing"); } } _logger.Info("IIS Bind Name:\t{0}", BindName ?? "none"); _logger.Info("Import in CSP:\t{0}", Registry.IsThereConfigParam("noCsp") ? "no" : "yes"); _logger.Info("PS Script File:\t{0}", ScriptFile ?? "none"); _logger.Info("Renewal Delay:\t{0}", RenewalDelay + " days"); _logger.Info("Task Scheduled:\t{0}", Utils.IsScheduledTaskCreated() ? "yes" : "no"); _logger.Info("Cert Enrolled:\t{0}", Registry.IsThereConfigParam("certSerial") ? "yes" : "no"); }
/// <summary> /// Validates a DNS challenge. Similar to HTTP Validation, but different because of DNSChallenge value which is signed by account key /// </summary> /// <param name="dnsChallenge"></param> /// <returns></returns> private async Task <bool> ValidateDNSChallenge(String domain, IChallengeContext dnsChallenge, IDNSChallengeValidator dnsChallengeValidator) { if (dnsChallenge == null) { throw new Exception("DNS Validation mode setup, but server returned no DNS challenge."); } // We get the resource fresh var dnsChallengeStatus = await dnsChallenge.Resource(); // If it's invalid, we stop right away. Should not happen, but anyway... if (dnsChallengeStatus.Status == ChallengeStatus.Invalid) { throw new Exception("DNS challenge has an invalid status"); } // Let's prepare for ACME-DNS validation var dnsValue = _acme.AccountKey.DnsTxt(dnsChallenge.Token); var dnsKey = $"_acme-challenge.{domain}".Replace("*.", ""); if (!dnsChallengeValidator.PrepareChallengeForValidation(dnsKey, dnsValue)) { return(false); } // Now let's ping the ACME service to validate the challenge token Challenge challengeRes = await dnsChallenge.Validate(); // We need to loop, because ACME service might need some time to validate the challenge token int retry = 0; while (((challengeRes.Status == ChallengeStatus.Pending) || (challengeRes.Status == ChallengeStatus.Processing)) && (retry < 10)) { // We sleep 2 seconds between each request, to leave time to ACME service to refresh System.Threading.Thread.Sleep(2000); // We refresh the challenge object from ACME service challengeRes = await dnsChallenge.Resource(); retry++; } // If challenge is Invalid, Pending or Processing, something went wrong... if (challengeRes.Status != ChallengeStatus.Valid) { return(false); } return(true); }
/// <summary> /// Validates an Authorization, switching between DNS and HTTP challenges /// </summary> /// <param name="authz"></param> /// <param name="httpChallengeValidator"></param> /// <returns></returns> private async Task ValidateAuthz(IAuthorizationContext authz, IHTTPChallengeValidator httpChallengeValidator, IDNSChallengeValidator dnsChallengeValidator) { // For each authorization, get the challenges var allChallenges = await authz.Challenges(); var res = await authz.Resource(); if (dnsChallengeValidator != null) { // Get the DNS challenge var dnsChallenge = await authz.Dns(); if (dnsChallenge != null) { logger.Debug($"Initiating DNS Validation for {res.Identifier.Value}"); var resValidation = await ValidateDNSChallenge(res.Identifier.Value, dnsChallenge, dnsChallengeValidator); if (!resValidation) { throw new Exception($"Could not validate DNS challenge:\n {dnsChallenge.Resource().Result.Error.Detail}"); } } else { throw new Exception("DNS Challenge Validation set up, but server sent no DNS Challenge"); } } else { // Get the HTTP challenge var httpChallenge = await authz.Http(); if (httpChallenge != null) { logger.Debug($"Initiating HTTP Validation for {res.Identifier.Value}"); var resValidation = await ValidateHTTPChallenge(httpChallenge, httpChallengeValidator); if (!resValidation) { throw new Exception($"Could not validate HTTP challenge:\n {httpChallenge.Resource().Result.Error.Detail}"); } } else { throw new Exception("HTTP Challenge Validation set up, but server sent no HTTP Challenge"); } } }
/// <summary> /// Register a new order on the ACME service, for the specified domains. Challenges will be automatically verified. /// This method manages automatically the creation of necessary directory and files. /// </summary> /// <remarks> /// When using HTTP Validation, the ACME directory will access to http://__domain__/.well-known/acme-challenge/token, that should be served /// by a local web server when not using built-in, and translated into local path {challengeVerifyPath}\.well-known\acme-challenge\token. /// Important Note: currently WinCertes supports only http-01 validation mode, and dns-01 validation mode with limitations. /// </remarks> /// <param name="domains">The list of domains to be registered and validated</param> /// <param name="httpChallengeValidator">The object used for challenge validation</param> /// <returns>True if successful</returns> public async Task <bool> RegisterNewOrderAndVerify(IList <string> domains, IHTTPChallengeValidator httpChallengeValidator, IDNSChallengeValidator dnsChallengeValidator) { try { // Re-init to be sure to get a fresh Nonce InitCertes(); // Creating the order _orderCtx = await _acme.NewOrder(domains); if (_orderCtx == null) { throw new Exception("Could not create certificate order."); } // And fetching authorizations var orderAuthz = await _orderCtx.Authorizations(); // Looping through authorizations foreach (IAuthorizationContext authz in orderAuthz) { InitCertes(); await ValidateAuthz(authz, httpChallengeValidator, dnsChallengeValidator); } _options.CertificateChallenged = true; // If we are here, it means order was properly created, and authorizations & challenges were properly verified. logger.Info($"Generated orders and validated challenges for domains: {String.Join(",", domains)}"); return(true); } catch (Exception exp) { logger.Debug(exp, "Error while trying to register and validate order"); logger.Error($"Failed to register and validate order with CA: {ProcessCertesException(exp)}"); _options.CertificateChallenged = false; return(false); } }
static int Main(string[] args) { // Main parameters with their default values string taskName = null; _winCertesOptions = new WinCertesOptions(); if (!Utils.IsAdministrator()) { Console.WriteLine("WinCertes.exe must be launched as Administrator"); return(ERROR); } // Command line options handling and initialization stuff if (!HandleOptions(args)) { return(ERROR_INCORRECT_PARAMETER); } if (_periodic) { taskName = Utils.DomainsToFriendlyName(_domains); } InitWinCertesDirectoryPath(); Utils.ConfigureLogger(_winCertesPath); _config = new RegistryConfig(_extra); _winCertesOptions.WriteOptionsIntoConfiguration(_config); if (_show) { _winCertesOptions.displayOptions(_config); return(0); } // Reset is a full reset ! if (_reset) { IConfig baseConfig = new RegistryConfig(false); baseConfig.DeleteAllParameters(); Utils.DeleteScheduledTasks(); return(0); } // Initialization and renewal/revocation handling try { InitCertesWrapper(_winCertesOptions.ServiceUri, _winCertesOptions.Email); } catch (Exception e) { _logger.Error(e.Message); return(ERROR); } if (_winCertesOptions.Revoke > -1) { RevokeCert(_domains, _winCertesOptions.Revoke); return(0); } // default mode: enrollment/renewal. check if there's something to be done // note that in any case, we want to be able to set the scheduled task (won't do anything if taskName is null) if (!IsThereCertificateAndIsItToBeRenewed(_domains)) { Utils.CreateScheduledTask(taskName, _domains, _extra); return(0); } // Now the real stuff: we register the order for the domains, and have them validated by the ACME service IHTTPChallengeValidator httpChallengeValidator = HTTPChallengeValidatorFactory.GetHTTPChallengeValidator(_winCertesOptions.Standalone, _winCertesOptions.HttpPort, _winCertesOptions.WebRoot); IDNSChallengeValidator dnsChallengeValidator = DNSChallengeValidatorFactory.GetDNSChallengeValidator(_config); if ((httpChallengeValidator == null) && (dnsChallengeValidator == null)) { WriteErrorMessageWithUsage(_options, "Specify either an HTTP or a DNS validation method."); return(ERROR_INCORRECT_PARAMETER); } if (!(Task.Run(() => _certesWrapper.RegisterNewOrderAndVerify(_domains, httpChallengeValidator, dnsChallengeValidator)).GetAwaiter().GetResult())) { if (httpChallengeValidator != null) { httpChallengeValidator.EndAllChallengeValidations(); } return(ERROR); } if (httpChallengeValidator != null) { httpChallengeValidator.EndAllChallengeValidations(); } // We get the certificate from the ACME service var pfxName = Task.Run(() => _certesWrapper.RetrieveCertificate(_domains, _winCertesPath, Utils.DomainsToFriendlyName(_domains))).GetAwaiter().GetResult(); if (pfxName == null) { return(ERROR); } AuthenticatedPFX pfx = new AuthenticatedPFX(_winCertesPath + "\\" + pfxName, _certesWrapper.PfxPassword); CertificateStorageManager certificateStorageManager = new CertificateStorageManager(pfx, ((_winCertesOptions.Csp == null) && (!_winCertesOptions.noCsp))); // Let's process the PFX into Windows Certificate objet. certificateStorageManager.ProcessPFX(); // and we write its information to the WinCertes configuration RegisterCertificateIntoConfiguration(certificateStorageManager.Certificate, _domains); // Import the certificate into the Windows store if (!_winCertesOptions.noCsp) { certificateStorageManager.ImportCertificateIntoCSP(_winCertesOptions.Csp); } // Bind certificate to IIS Site (won't do anything if option is null) Utils.BindCertificateForIISSite(certificateStorageManager.Certificate, _winCertesOptions.BindName); // Execute PowerShell Script (won't do anything if option is null) Utils.ExecutePowerShell(_winCertesOptions.ScriptFile, pfx); // Create the AT task that will execute WinCertes periodically (won't do anything if taskName is null) Utils.CreateScheduledTask(taskName, _domains, _extra); // Let's delete the PFX file RemoveFileAndLog(pfx.PfxFullPath); return(0); }
/// <summary> /// Main programme /// </summary> /// <param name="args">WinCertes command line arguments</param> /// <returns>Zero if successul, error code otherwise</returns> private static int Main(string[] args) { // WinCertes Certificate path... InitWinCertesDirectoryPath(); Utils.ConfigureLogger(_logPath); if (!Utils.IsAdministrator()) { string message = "WinCertes.exe must be launched as Administrator with elevated permissions"; _logger.Error(message); Thread.Sleep(1000); Utils.AdminRelauncher(); } // Merge command line parameters with registry defaults // TODO: Revamp this completely to use simple command line (like aloopkin\WinCertes) or configuration file int result = HandleOptions(args); if (result != 0) { return(MainExit(result)); } // Display settings, don't create or renew the certificate if (_show) { _winCertesOptions.DisplayOptions(); return(MainExit(SUCCESS)); } // Helper to create the DNS keys if (_creatednskeys) { _winCertesOptions.WriteDnsOptions(); return(MainExit(SUCCESS)); } // Reset is a full reset! if (_reset) { Console.WriteLine("\nWARNING: You should revoke the certificate before deleting it from the registry\nDelete [{0}]?\nPress Enter when ready...", _winCertesOptions.Registry.FullRegistryKey); Console.ReadLine(); _winCertesOptions.Registry.DeleteAllParameters(); Utils.DeleteScheduledTasks(); return(MainExit(SUCCESS)); } _logger.Info("Initialisation successful, processing your request..."); string taskName = null; if (_periodic) { taskName = Utils.DomainsToFriendlyName(_winCertesOptions.Domains); } // Initialization and renewal/revocation handling try { InitCertesWrapper(_winCertesOptions); } catch (Exception e) { _logger.Error(e.Message); return(MainExit(ERROR)); } if (_winCertesOptions.Revoke > -1) { RevokeCert(_winCertesOptions.Domains, _winCertesOptions.Revoke); return(MainExit(SUCCESS)); } // default mode: enrollment/renewal. check if there's something to be done // note that in any case, we want to be able to set the scheduled task (won't do anything if taskName is null) if (!IsThereCertificateAndIsItToBeRenewed(_winCertesOptions.Domains)) { Utils.CreateScheduledTask(taskName, _winCertesOptions.Domains, _extra); return(MainExit(SUCCESS)); } // Now the real stuff: we register the order for the domains, and have them validated by the ACME service IHTTPChallengeValidator httpChallengeValidator = HTTPChallengeValidatorFactory.GetHTTPChallengeValidator(_winCertesOptions.Standalone, _winCertesOptions.HttpPort, _winCertesOptions.WebRoot); IDNSChallengeValidator dnsChallengeValidator = DNSChallengeValidatorFactory.GetDNSChallengeValidator(); if ((httpChallengeValidator == null) && (dnsChallengeValidator == null)) { WriteErrorMessageWithUsage(_options, "Specify either an HTTP or a DNS validation method."); return(MainExit(ERROR_MISSING_HTTP_DNS)); } if (!(Task.Run(() => _certesWrapper.RegisterNewOrderAndVerify(_winCertesOptions.Domains, httpChallengeValidator, dnsChallengeValidator)).GetAwaiter().GetResult())) { if (httpChallengeValidator != null) { httpChallengeValidator.EndAllChallengeValidations(); } return(MainExit(ERROR)); } if (httpChallengeValidator != null) { httpChallengeValidator.EndAllChallengeValidations(); } // We get the certificate from the ACME service string pfxFullFileName = _winCertesPath + "\\" + _winCertesOptions.CertificateName; var pfx = Task.Run(() => _certesWrapper.RetrieveCertificate(_winCertesOptions.Domains, pfxFullFileName, Utils.DomainsToFriendlyName(_winCertesOptions.Domains), _winCertesOptions.ExportPem) ).GetAwaiter().GetResult(); if (pfx == null) { return(MainExit(ERROR)); } CertificateStorageManager certificateStorageManager = new CertificateStorageManager(pfx, (_winCertesOptions.Csp == null) && (!_winCertesOptions.noCsp)); // Let's process the PFX into Windows Certificate object. certificateStorageManager.ProcessPFX(); // and we write its information to the WinCertes configuration RegisterCertificateIntoConfiguration(certificateStorageManager.Certificate, _winCertesOptions.Domains); // Import the certificate into the Windows store if (!_winCertesOptions.noCsp) { certificateStorageManager.ImportCertificateIntoCSP(_winCertesOptions.Csp); } // Bind certificate to IIS Site (won't do anything if option is null) Utils.BindCertificateForIISSite(certificateStorageManager.Certificate, _winCertesOptions.BindName); // Execute PowerShell Script (won't do anything if option is null) Utils.ExecutePowerShell(_winCertesOptions.ScriptFile, pfx); // Create the AT task that will execute WinCertes periodically (won't do anything if taskName is null) Utils.CreateScheduledTask(taskName, _winCertesOptions.Domains, _extra); // Let's delete the PFX file, if export was not enabled if (!_winCertesOptions.ExportPem) { RemoveFileAndLog(pfx); } return(MainExit(SUCCESS)); }