private async Task <bool> IsManagedCertificateRunning(string id, ICertifiedServer iis = null) { var managedCertificate = await _itemManager.GetById(id); if (managedCertificate != null) { if (iis == null) { iis = _serverProvider; } try { return(await iis.IsSiteRunning(managedCertificate.GroupId)); } catch { // by default we assume the site is running return(true); } } else { //site not identified, assume it is running return(true); } }
private Func <bool> PrepareChallengeResponse_TlsSni01(ILog log, ICertifiedServer iisManager, string domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth) { var requestConfig = managedCertificate.RequestConfig; var tlsSniChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI); if (tlsSniChallenge == null) { log.Warning($"No tls-sni-01 challenge to complete for {managedCertificate.Name}. Request cannot continue."); return(() => false); } var sha256 = System.Security.Cryptography.SHA256.Create(); var z = new byte[tlsSniChallenge.HashIterationCount][]; // compute n sha256 hashes, where n=challengedata.iterationcount z[0] = sha256.ComputeHash(Encoding.UTF8.GetBytes(tlsSniChallenge.Value)); for (var i = 1; i < z.Length; i++) { z[i] = sha256.ComputeHash(z[i - 1]); } // generate certs and install iis bindings var cleanupQueue = new List <Action>(); var checkQueue = new List <Func <bool> >(); foreach (var hex in z.Select(b => BitConverter.ToString(b).Replace("-", "").ToLower())) { var sni = $"{hex.Substring(0, 32)}.{hex.Substring(32)}.acme.invalid"; log.Information($"Preparing binding at: https://{domain}, sni: {sni}"); var x509 = CertificateManager.GenerateSelfSignedCertificate(sni); CertificateManager.StoreCertificate(x509); var certStoreName = CertificateManager.GetStore().Name; // iisManager.InstallCertificateforBinding(certStoreName, x509.GetCertHash(), // managedCertificate.ServerSiteId, sni); // add check to the queue checkQueue.Add(() => _netUtil.CheckSNI(domain, sni).Result); // add cleanup actions to queue cleanupQueue.Add(() => iisManager.RemoveHttpsBinding(managedCertificate.ServerSiteId, sni)); cleanupQueue.Add(() => CertificateManager.RemoveCertificate(x509)); } // configure cleanup to execute the cleanup queue pendingAuth.Cleanup = () => cleanupQueue.ForEach(a => a()); // perform our own config checks return(() => checkQueue.All(check => check())); }
public CertifyManager() { var serverConfig = SharedUtils.ServiceConfigManager.GetAppServiceConfig(); SettingsManager.LoadAppSettings(); InitLogging(serverConfig); Util.SetSupportedTLSVersions(); _itemManager = new ItemManager(); _serverProvider = (ICertifiedServer) new ServerProviderIIS(); _progressResults = new ObservableCollection <RequestProgressState>(); _pluginManager = new PluginManager(); _pluginManager.LoadPlugins(new List <string> { "Licensing", "DashboardClient", "DeploymentTasks" }); // TODO: convert providers to plugins, allow for async init var userAgent = Util.GetUserAgent(); var certes = new CertesACMEProvider(Management.Util.GetAppDataFolder() + "\\certes", userAgent); certes.InitProvider(_serviceLog).Wait(); _acmeClientProvider = certes; _vaultProvider = certes; // init remaining utilities and optionally enable telematics _challengeDiagnostics = new ChallengeDiagnostics(CoreAppSettings.Current.EnableValidationProxyAPI); if (CoreAppSettings.Current.EnableAppTelematics) { _tc = new Util().InitTelemetry(); } _httpChallengePort = serverConfig.HttpChallengeServerPort; _httpChallengeServerClient.Timeout = new TimeSpan(0, 0, 5); if (_tc != null) { _tc.TrackEvent("ServiceStarted"); } _serviceLog?.Information("Certify Manager Started"); PerformUpgrades().Wait(); }
public async Task <PendingAuthorization> PerformAutomatedChallengeResponse(ICertifiedServer iisManager, ManagedSite managedSite, PendingAuthorization pendingAuth) { var requestConfig = managedSite.RequestConfig; var domain = pendingAuth.Identifier.Dns; if (pendingAuth.Challenges != null) { // from list of possible challenges, select the one we prefer to attempt var requiredChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == managedSite.RequestConfig.ChallengeType); if (requiredChallenge != null) { pendingAuth.AttemptedChallenge = requiredChallenge; if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { // perform http-01 challenge response var check = PrepareChallengeResponse_Http01(iisManager, domain, managedSite, pendingAuth); if (requestConfig.PerformExtensionlessConfigChecks) { pendingAuth.AttemptedChallenge.ConfigCheckedOK = check(); } } if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { // perform tls-sni-01 challenge response var check = PrepareChallengeResponse_TlsSni01(iisManager, domain, managedSite, pendingAuth); if (requestConfig.PerformTlsSniBindingConfigChecks) { // set config check OK if all checks return true pendingAuth.AttemptedChallenge.ConfigCheckedOK = check(); } } if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { // perform dns-01 challenge response FIXME: var check = PrepareChallengeResponse_Dns01(domain, managedSite, pendingAuth); /*if (requestConfig.PerformTlsSniBindingConfigChecks) * { * // set config check OK if all checks return true * pendingAuth.AttemptedChallenge.ConfigCheckedOK = check(); * }*/ } } } return(pendingAuth); }
public CertifyManager() { _serviceLog = new Loggy( new LoggerConfiguration() .MinimumLevel.Verbose() .WriteTo.Debug() .WriteTo.File(Util.GetAppDataFolder("logs") + "\\sessionlog.txt", shared: true, flushToDiskInterval: new TimeSpan(0, 0, 10)) .CreateLogger() ); Util.SetSupportedTLSVersions(); _itemManager = new ItemManager(); _serverProvider = (ICertifiedServer) new ServerProviderIIS(); _progressResults = new ObservableCollection <RequestProgressState>(); _pluginManager = new PluginManager(); _pluginManager.LoadPlugins(); // TODO: convert providers to plugins var certes = new Certify.Providers.Certes.CertesACMEProvider(Management.Util.GetAppDataFolder() + "\\certes"); _acmeClientProvider = certes; _vaultProvider = certes; // init remaining utilities and optionally enable telematics _challengeDiagnostics = new ChallengeDiagnostics(CoreAppSettings.Current.EnableValidationProxyAPI); if (CoreAppSettings.Current.EnableAppTelematics) { _tc = new Util().InitTelemetry(); } PerformUpgrades(); var serverConfig = Util.GetAppServiceConfig(); _httpChallengePort = serverConfig.HttpChallengeServerPort; _httpChallengeServerClient.Timeout = new TimeSpan(0, 0, 5); if (_tc != null) { _tc.TrackEvent("ServiceStarted"); } }
/// <summary> /// WIP: For current configured environment, show preview of recommended site management (for /// local IIS, scan sites and recommend actions) /// </summary> /// <returns></returns> public async Task <List <ManagedCertificate> > PreviewManagedCertificates(StandardServerTypes serverType, ICertifiedServer serverProvider, ICertifyManager certifyManager) { var sites = new List <ManagedCertificate>(); if (serverType == StandardServerTypes.IIS) { try { var allSites = await serverProvider.GetSiteBindingList(CoreAppSettings.Current.IgnoreStoppedSites); var iisSites = allSites .OrderBy(s => s.SiteId) .ThenBy(s => s.Host); var siteIds = iisSites.GroupBy(x => x.SiteId); foreach (var s in siteIds) { var managedCertificate = new ManagedCertificate { Id = s.Key }; managedCertificate.ItemType = ManagedCertificateType.SSL_ACME; managedCertificate.TargetHost = "localhost"; managedCertificate.Name = iisSites.First(i => i.SiteId == s.Key).SiteName; //TODO: replace site binding with domain options //managedCertificate.SiteBindings = new List<ManagedCertificateBinding>(); /* foreach (var binding in s) * { * var managedBinding = new ManagedCertificateBinding { Hostname = binding.Host, IP = binding.IP, Port = binding.Port, UseSNI = true, CertName = "Certify_" + binding.Host }; * // managedCertificate.SiteBindings.Add(managedBinding); * }*/ sites.Add(managedCertificate); } } catch (Exception) { //can't read sites Debug.WriteLine("Can't get IIS site list."); } } return(sites); }
/// <summary> /// Generate a list of actions which will be performed on the next renewal of this managed certificate, populating /// the description of each action with a Markdown format description /// </summary> /// <param name="item"></param> /// <param name="serverProvider"></param> /// <param name="certifyManager"></param> /// <returns></returns> public async Task <List <ActionStep> > GeneratePreview( ManagedCertificate item, ICertifiedServer serverProvider, ICertifyManager certifyManager, ICredentialsManager credentialsManager ) { var newLine = "\r\n"; var steps = new List <ActionStep>(); var stepIndex = 1; var hasDomains = true; var allTaskProviders = await certifyManager.GetDeploymentProviders(); var certificateAuthorities = await certifyManager.GetCertificateAuthorities(); // ensure defaults are applied for the deployment mode, overwriting any previous selections item.RequestConfig.ApplyDeploymentOptionDefaults(); if (string.IsNullOrEmpty(item.RequestConfig.PrimaryDomain)) { hasDomains = false; } if (hasDomains) { var allCredentials = await credentialsManager.GetCredentials(); var allDomains = new List <string> { item.RequestConfig.PrimaryDomain }; if (item.RequestConfig.SubjectAlternativeNames != null) { allDomains.AddRange(item.RequestConfig.SubjectAlternativeNames); } allDomains = allDomains.Distinct().OrderBy(d => d).ToList(); // certificate summary var certDescription = new StringBuilder(); var ca = certificateAuthorities.FirstOrDefault(c => c.Id == item.CertificateAuthorityId); certDescription.AppendLine( $"A new certificate will be requested from the *{ca?.Title ?? "Default"}* certificate authority for the following domains:" ); certDescription.AppendLine($"\n**{item.RequestConfig.PrimaryDomain}** (Primary Domain)"); if (item.RequestConfig.SubjectAlternativeNames.Any(s => s != item.RequestConfig.PrimaryDomain)) { certDescription.AppendLine($" and will include the following *Subject Alternative Names*:"); foreach (var d in item.RequestConfig.SubjectAlternativeNames) { certDescription.AppendLine($"* {d} "); } } steps.Add(new ActionStep { Title = "Summary", Description = certDescription.ToString() }); // validation steps : // TODO: preview description should come from the challenge type provider var challengeInfo = new StringBuilder(); foreach (var challengeConfig in item.RequestConfig.Challenges) { challengeInfo.AppendLine( $"{newLine}Authorization will be attempted using the **{challengeConfig.ChallengeType}** challenge type." + newLine ); var matchingDomains = item.GetChallengeConfigDomainMatches(challengeConfig, allDomains); if (matchingDomains.Any()) { challengeInfo.AppendLine( $"{newLine}The following matching domains will use this challenge: " + newLine ); foreach (var d in matchingDomains) { challengeInfo.AppendLine($"{newLine} * {d}"); } challengeInfo.AppendLine( $"{newLine}**Please review the Deployment section below to ensure this certificate will be applied to the expected website bindings (if any).**" + newLine ); } else { challengeInfo.AppendLine( $"{newLine}*No domains will match this challenge type.* Either the challenge is not required or domain matches are not fully configured." ); } challengeInfo.AppendLine(newLine); if (challengeConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { challengeInfo.AppendLine( $"This will involve the creation of a randomly named (extensionless) text file for each domain (website) included in the certificate." + newLine ); if (CoreAppSettings.Current.EnableHttpChallengeServer) { challengeInfo.AppendLine( $"The *Http Challenge Server* option is enabled. This will create a temporary web service on port 80 during validation. " + $"This process co-exists with your main web server and listens for http challenge requests to /.well-known/acme-challenge/. " + $"If you are using a web server on port 80 other than IIS (or other http.sys enabled server), that will be used instead." + newLine ); } if (!string.IsNullOrEmpty(item.RequestConfig.WebsiteRootPath) && string.IsNullOrEmpty(challengeConfig.ChallengeRootPath)) { challengeInfo.AppendLine( $"The file will be created at the path `{item.RequestConfig.WebsiteRootPath}\\.well-known\\acme-challenge\\` " + newLine ); } if (!string.IsNullOrEmpty(challengeConfig.ChallengeRootPath)) { challengeInfo.AppendLine( $"The file will be created at the path `{challengeConfig.ChallengeRootPath}\\.well-known\\acme-challenge\\` " + newLine ); } challengeInfo.AppendLine( $"The text file will need to be accessible from the URL `http://<yourdomain>/.well-known/acme-challenge/<randomfilename>` " + newLine); challengeInfo.AppendLine( $"The issuing Certificate Authority will follow any redirection in place (such as rewriting the URL to *https*) but the initial request will be made via *http* on port 80. " + newLine); } if (challengeConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { challengeInfo.AppendLine( $"This will involve the creation of a DNS TXT record named `_acme-challenge.yourdomain.com` for each domain or subdomain included in the certificate. " + newLine); if (!string.IsNullOrEmpty(challengeConfig.ChallengeCredentialKey)) { var creds = allCredentials.FirstOrDefault(c => c.StorageKey == challengeConfig.ChallengeCredentialKey); if (creds != null) { challengeInfo.AppendLine( $"The following DNS API Credentials will be used: **{creds.Title}** " + newLine); } else { challengeInfo.AppendLine( $"**Invalid credential settngs.** The currently selected credential does not exist." ); } } else { challengeInfo.AppendLine( $"No DNS API Credentials have been set. API Credentials are normally required to make automatic updates to DNS records." ); } challengeInfo.AppendLine( newLine + $"The issuing Certificate Authority will follow any redirection in place (such as a substitute CNAME pointing to another domain) but the initial request will be made against any of the domain's nameservers. " ); } if (!string.IsNullOrEmpty(challengeConfig.DomainMatch)) { challengeInfo.AppendLine( $"{newLine}This challenge type will be selected based on matching domain **{challengeConfig.DomainMatch}** "); } else { if (item.RequestConfig.Challenges.Count > 1) { challengeInfo.AppendLine( $"{newLine}This challenge type will be selected for any domain not matched by another challenge. "); } else { challengeInfo.AppendLine( $"{newLine}**This challenge type will be selected for all domains.**"); } } challengeInfo.AppendLine($"{newLine}---{newLine}"); } steps.Add(new ActionStep { Title = $"{stepIndex}. Domain Validation", Category = "Validation", Description = challengeInfo.ToString() }); stepIndex++; // pre request tasks if (item.PreRequestTasks?.Any() == true) { var substeps = item.PreRequestTasks.Select(t => new ActionStep { Key = t.Id, Title = $"{t.TaskName} ({allTaskProviders.FirstOrDefault(p => p.Id == t.TaskTypeId)?.Title})", Description = t.Description }); steps.Add(new ActionStep { Title = $"{stepIndex}. Pre-Request Tasks", Category = "PreRequestTasks", Description = $"Execute {substeps.Count()} Pre-Request Tasks", Substeps = substeps.ToList() }); stepIndex++; } // cert request step var certRequest = $"A Certificate Signing Request (CSR) will be submitted to the Certificate Authority, using the **{item.RequestConfig.CSRKeyAlg}** signing algorithm."; steps.Add(new ActionStep { Title = $"{stepIndex}. Certificate Request", Category = "CertificateRequest", Description = certRequest }); stepIndex++; // deployment & binding steps var deploymentDescription = new StringBuilder(); var deploymentStep = new ActionStep { Title = $"{stepIndex}. Deployment", Category = "Deployment", Description = "" }; if ( item.RequestConfig.DeploymentSiteOption == DeploymentOption.Auto || item.RequestConfig.DeploymentSiteOption == DeploymentOption.AllSites || item.RequestConfig.DeploymentSiteOption == DeploymentOption.SingleSite ) { // deploying to single or multiple Site if (item.RequestConfig.DeploymentBindingMatchHostname) { deploymentDescription.AppendLine( "* Deploy to hostname bindings matching certificate domains."); } if (item.RequestConfig.DeploymentBindingBlankHostname) { deploymentDescription.AppendLine("* Deploy to bindings with blank hostname."); } if (item.RequestConfig.DeploymentBindingReplacePrevious) { deploymentDescription.AppendLine("* Deploy to bindings with previous certificate."); } if (item.RequestConfig.DeploymentBindingOption == DeploymentBindingOption.AddOrUpdate) { deploymentDescription.AppendLine("* Add or Update https bindings as required"); } if (item.RequestConfig.DeploymentBindingOption == DeploymentBindingOption.UpdateOnly) { deploymentDescription.AppendLine("* Update https bindings as required (no auto-created https bindings)"); } if (item.RequestConfig.DeploymentSiteOption == DeploymentOption.SingleSite) { if (!string.IsNullOrEmpty(item.ServerSiteId)) { try { var siteInfo = await serverProvider.GetSiteById(item.ServerSiteId); deploymentDescription.AppendLine($"## Deploying to Site" + newLine + newLine + $"`{siteInfo.Name}`" + newLine); } catch (Exception exp) { deploymentDescription.AppendLine($"Error: **cannot identify selected site.** {exp.Message} "); } } } else { deploymentDescription.AppendLine($"## Deploying to all matching sites:"); } // add deployment sub-steps (if any) var bindingRequest = await certifyManager.DeployCertificate(item, null, true); if (bindingRequest.Actions == null || !bindingRequest.Actions.Any()) { deploymentStep.Substeps = new List <ActionStep> { new ActionStep { Description = newLine + "**There are no matching targets to deploy to. Certificate will be stored but currently no bindings will be updated.**" } }; } else { deploymentStep.Substeps = bindingRequest.Actions; deploymentDescription.AppendLine(" Action | Site | Binding "); deploymentDescription.Append(" ------ | ---- | ------- "); } } else if (item.RequestConfig.DeploymentSiteOption == DeploymentOption.DeploymentStoreOnly) { deploymentDescription.AppendLine("* The certificate will be saved to the local machines Certificate Store only (Personal/My Store)"); } else if (item.RequestConfig.DeploymentSiteOption == DeploymentOption.NoDeployment) { deploymentDescription.AppendLine("* The certificate will be saved to local disk only."); } deploymentStep.Description = deploymentDescription.ToString(); steps.Add(deploymentStep); stepIndex++; // post request deployment tasks if (item.PostRequestTasks?.Any() == true) { var substeps = item.PostRequestTasks.Select(t => new ActionStep { Key = t.Id, Title = $"{t.TaskName} ({allTaskProviders.FirstOrDefault(p => p.Id == t.TaskTypeId)?.Title})", Description = t.Description }); steps.Add(new ActionStep { Title = $"{stepIndex}. Post-Request (Deployment) Tasks", Category = "PostRequestTasks", Description = $"Execute {substeps.Count()} Post-Request Tasks", Substeps = substeps.ToList() }); stepIndex++; } stepIndex = steps.Count; } else { steps.Add(new ActionStep { Title = "Certificate has no domains", Description = "No domains have been added to this certificate, so a certificate cannot be requested. Each certificate requires a primary domain (a 'subject') and an optional list of additional domains (subject alternative names)." }); } return(steps); }
/// <summary> /// Prepares IIS to respond to a http-01 challenge /// </summary> /// <returns> Test the challenge response locally. </returns> private async Task <ActionResult> PerformChallengeResponse_Http01(ILog log, ICertifiedServer iisManager, string domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth) { var requestConfig = managedCertificate.RequestConfig; var httpChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP); if (httpChallenge == null) { var msg = $"No http challenge to complete for {managedCertificate.Name}. Request cannot continue."; log.Warning(msg); return(new ActionResult { IsSuccess = false, Message = msg }); } log.Information($"Preparing challenge response for the issuing Certificate Authority to check at: {httpChallenge.ResourceUri} with content {httpChallenge.Value}"); log.Information("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued."); // get website root path (from challenge config or fallback to deprecated // WebsiteRootPath), expand environment variables if required var websiteRootPath = requestConfig.WebsiteRootPath; var challengeConfig = managedCertificate.GetChallengeConfig(domain); if (!string.IsNullOrEmpty(challengeConfig.ChallengeRootPath)) { websiteRootPath = challengeConfig.ChallengeRootPath; } if (!string.IsNullOrEmpty(managedCertificate.ServerSiteId)) { var siteInfo = await iisManager.GetSiteById(managedCertificate.ServerSiteId); if (siteInfo == null) { return(new ActionResult { IsSuccess = false, Message = "IIS Website unavailable. Site may be removed or IIS is unavailable" }); } // if website root path not specified, determine it now if (string.IsNullOrEmpty(websiteRootPath)) { websiteRootPath = siteInfo.Path; } if (!string.IsNullOrEmpty(websiteRootPath) && websiteRootPath.Contains("%")) { // if websiteRootPath contains %websiteroot% variable, replace that with the // current physical path for the site if (websiteRootPath.Contains("%websiteroot%")) { // sets env variable for this process only Environment.SetEnvironmentVariable("websiteroot", siteInfo.Path); } // expand any environment variables present in site path websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath); } } log.Information("Using website path {path}", websiteRootPath ?? "[Auto]"); if (string.IsNullOrEmpty(websiteRootPath) || !Directory.Exists(websiteRootPath)) { // our website no longer appears to exist on disk, continuing would potentially // create unwanted folders, so it's time for us to give up var msg = $"The website root path for {managedCertificate.Name} could not be determined. Request cannot continue."; log.Error(msg); return(new ActionResult { IsSuccess = false, Message = msg }); } // copy temp file to path challenge expects in web folder var destFile = Path.Combine(websiteRootPath, httpChallenge.ResourcePath); var destPath = Path.GetDirectoryName(destFile); if (!Directory.Exists(destPath)) { try { Directory.CreateDirectory(destPath); } catch (Exception exp) { // failed to create directory, probably permissions or may be invalid config var msg = $"Pre-config check failed: Could not create directory: {destPath}"; log.Error(exp, msg); return(new ActionResult { IsSuccess = false, Message = msg }); } } // copy challenge response to web folder /.well-known/acme-challenge. Check if it already // exists (as in 'configcheck' file) as can cause conflicts. if (!File.Exists(destFile) || !destFile.EndsWith("configcheck")) { try { File.WriteAllText(destFile, httpChallenge.Value); } catch (Exception exp) { // failed to create configcheck file, probably permissions or may be invalid config var msg = $"Pre-config check failed: Could not create file: {destFile}"; log.Error(exp, msg); return(new ActionResult { IsSuccess = false, Message = msg }); } } // prepare cleanup - should this be configurable? Because in some case many sites // renewing may all point to the same web root, we keep the configcheck file pendingAuth.Cleanup = () => { if (!destFile.EndsWith("configcheck") && File.Exists(destFile)) { log.Debug("Challenge Cleanup: Removing {file}", destFile); try { File.Delete(destFile); } catch { } } }; // if config checks are enabled but our last renewal was successful, skip auto config // until we have failed twice if (requestConfig.PerformExtensionlessConfigChecks) { if (managedCertificate.DateRenewed != null && managedCertificate.RenewalFailureCount < 2) { return(new ActionResult { IsSuccess = true, Message = $"Skipping URL access checks and auto config (if applicable): {httpChallenge.ResourceUri}. Will resume checks if renewal failure count exceeds 2 attempts." }); } // first check if it already works with no changes if (await _netUtil.CheckURL(log, httpChallenge.ResourceUri)) { return(new ActionResult { IsSuccess = true, Message = $"Verified URL is accessible: {httpChallenge.ResourceUri}" }); } // initial check didn't work, if auto config enabled attempt to find a working config if (requestConfig.PerformAutoConfig) { // FIXME: need to only overwrite config we have auto populated, not user // specified config, compare to our preconfig and only overwrite if same as ours? // Or include preset key in our config, or make behaviour configurable LogAction($"Pre-config check failed: Auto-config will overwrite existing config: {destPath}\\web.config"); var configOptions = Directory.EnumerateFiles(Path.Combine(Environment.CurrentDirectory, "Scripts", "Web.config"), "*.config"); foreach (var configFile in configOptions) { // create a web.config for extensionless files, then test it (make a request // for the extensionless configcheck file over http) var webConfigContent = File.ReadAllText(configFile); // no existing config, attempt auto config and perform test LogAction($"Testing config alternative: " + configFile); try { System.IO.File.WriteAllText(Path.Combine(destPath, "web.config"), webConfigContent); } catch (Exception exp) { LogAction($"Failed to write config: " + exp.Message); } if (await _netUtil.CheckURL(log, httpChallenge.ResourceUri)) { return(new ActionResult { IsSuccess = true, Message = $"Verified URL is accessible: {httpChallenge.ResourceUri}" }); } } } // failed to auto configure or confirm resource is accessible return(new ActionResult { IsSuccess = false, Message = $"Could not verify URL is accessible: {httpChallenge.ResourceUri}" }); } else { return(new ActionResult { IsSuccess = false, Message = $"Config checks disabled. Did not verify URL access: {httpChallenge.ResourceUri}" }); } }
/// <summary> /// Simulates responding to a challenge, performs a sample configuration and attempts to /// verify it. /// </summary> /// <param name="serverManager"> </param> /// <param name="managedCertificate"> </param> /// <returns> APIResult </returns> /// <remarks> /// The purpose of this method is to test the options (permissions, configuration) before /// submitting a request to the ACME server, to avoid creating failed requests and hitting /// usage limits. /// </remarks> public async Task <List <StatusMessage> > TestChallengeResponse( ILog log, ICertifiedServer serverManager, ManagedCertificate managedCertificate, bool isPreviewMode, bool enableDnsChecks, ICredentialsManager credentialsManager, IProgress <RequestProgressState> progress = null ) { var results = new List <StatusMessage>(); var requestConfig = managedCertificate.RequestConfig; var domains = new List <string> { requestConfig.PrimaryDomain }; if (requestConfig.SubjectAlternativeNames != null) { domains.AddRange(requestConfig.SubjectAlternativeNames); } domains = domains.Distinct().ToList(); // if wildcard domain included, check first level labels not also specified, i.e. // *.example.com & www.example.com cannot be mixed, but example.com, *.example.com & // test.wwww.example.com can var invalidLabels = new List <string>(); if (domains.Any(d => d.StartsWith("*."))) { foreach (var wildcard in domains.Where(d => d.StartsWith("*."))) { var rootDomain = wildcard.Replace("*.", ""); // add list of domains where label count exceeds root domain label count invalidLabels.AddRange(domains.Where(domain => domain != wildcard && domain.EndsWith(rootDomain) && domain.Count(s => s == '.') == wildcard.Count(s => s == '.'))); if (invalidLabels.Any()) { results.Add(new StatusMessage { IsOK = false, Message = $"Wildcard domain certificate requests (e.g. {wildcard}) cannot be mixed with requests including immediate subdomains (e.g. {invalidLabels[0]})." }); return(results); } } } var generatedAuthorizations = new List <PendingAuthorization>(); try { // if DNS checks enabled, attempt them here if (isPreviewMode && enableDnsChecks) { var includeIPResolution = false; if (managedCertificate.RequestConfig.Challenges.Any(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP)) { includeIPResolution = true; } log.Information("Performing preview DNS tests. {managedItem}", managedCertificate); var tasks = new List <Task <List <ActionResult> > >(); foreach (var d in domains) { tasks.Add(_netUtil.CheckDNS(log, d.Replace("*.", ""), includeIPResolution)); } var allResults = await Task.WhenAll(tasks); // add DNS check results. DNS check fails are considered a warning instead of an error. foreach (var checkResults in allResults) { foreach (var c in checkResults) { results.Add(new StatusMessage { IsOK = true, HasWarning = !c.IsSuccess, Message = c.Message }); } } } foreach (var domain in domains) { var challengeConfig = managedCertificate.GetChallengeConfig(domain); var challengeType = challengeConfig.ChallengeType; if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { log.Warning("tls-sni-01 challenge type is no longer supported by the Certificate Authority. Falling back to http-01"); challengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP; } if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { // if dns validation not selected but one or more domains is a wildcard, reject if (domain.StartsWith("*.")) { results.Add(new StatusMessage { IsOK = false, Message = $"http-01 authorization cannot be used for wildcard domains: {domain}. Use DNS (dns-01) validation instead." }); return(results); } var challengeFileUrl = $"http://{domain}/.well-known/acme-challenge/configcheck"; var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP, ResourcePath = Path.Combine(".well-known", "acme-challenge", "configcheck"), ResourceUri = challengeFileUrl, Value = "Extensionless File Config Test - OK" } } }; generatedAuthorizations.Add(simulatedAuthorization); var httpChallengeResult = await PerformChallengeResponse_Http01( log, serverManager, domain, managedCertificate, simulatedAuthorization ); if (!httpChallengeResult.IsSuccess) { var result = new StatusMessage(); result.IsOK = false; result.FailedItemSummary.Add($"Config checks failed to verify http://{domain} is both publicly accessible and can serve extensionless files e.g. {challengeFileUrl}"); result.Message = httpChallengeResult.Message; results.Add(result); // don't check any more after first failure break; } else { results.Add(new StatusMessage { IsOK = true, Message = httpChallengeResult.Message, Result = httpChallengeResult }); } } else if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { var result = new StatusMessage(); result.IsOK = false; result.FailedItemSummary.Add($"The {SupportedChallengeTypes.CHALLENGE_TYPE_SNI} challenge type is no longer available."); results.Add(result); return(results); /* * * var serverVersion = await serverManager.GetServerVersion(); * * if (serverVersion.Major < 8) * { * result.IsOK = false; * result.FailedItemSummary.Add($"The {SupportedChallengeTypes.CHALLENGE_TYPE_SNI} challenge is only available for IIS versions 8+."); * results.Add(result); * * return results; * } * * var simulatedAuthorization = new PendingAuthorization * { * Challenges = new List<AuthorizationChallengeItem> { * new AuthorizationChallengeItem * { * ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_SNI, * HashIterationCount= 1, * Value = GenerateSimulatedKeyAuth() * } * } * }; * * generatedAuthorizations.Add(simulatedAuthorization); * * result.IsOK = * PrepareChallengeResponse_TlsSni01( * log, serverManager, domain, managedCertificate, simulatedAuthorization * )(); * * results.Add(result); */ } else if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { var recordName = $"_acme-challenge-test.{domain}".Replace("*.", ""); // ISSUE: dependency on changing behavior for a specific plugin if (challengeConfig.ChallengeProvider == "DNS01.API.AcmeDns") { // use real cname to avoid having to setup different records recordName = $"_acme-challenge.{domain}".Replace("*.", ""); } var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, Key = recordName, Value = GenerateSimulatedDnsAuthValue() } } }; generatedAuthorizations.Add(simulatedAuthorization); var dnsResult = await PerformChallengeResponse_Dns01( log, domain, managedCertificate, simulatedAuthorization, isTestMode : true, credentialsManager ); var result = new StatusMessage(); result.Message = dnsResult.Result.Message; result.IsOK = dnsResult.Result.IsSuccess; if (!result.IsOK) { result.FailedItemSummary.Add(dnsResult.Result.Message); } results.Add(result); } else { throw new NotSupportedException($"ChallengeType not supported: {challengeConfig.ChallengeType}"); } } } finally { //FIXME: needs to be filtered by managed site: result.Message = String.Join("\r\n", GetActionLogSummary()); generatedAuthorizations.ForEach(ga => ga.Cleanup()); } return(results); }
public async Task <PendingAuthorization> PerformAutomatedChallengeResponse(ILog log, ICertifiedServer iisManager, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth, ICredentialsManager credentialsManager) { var requestConfig = managedCertificate.RequestConfig; var domain = pendingAuth.Identifier.Dns; var challengeConfig = managedCertificate.GetChallengeConfig(domain); if (pendingAuth.Challenges != null) { // from list of possible challenges, select the one we prefer to attempt var requiredChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == challengeConfig.ChallengeType); if (requiredChallenge != null) { pendingAuth.AttemptedChallenge = requiredChallenge; if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { // perform http-01 challenge response var check = await PerformChallengeResponse_Http01(log, iisManager, domain, managedCertificate, pendingAuth); if (requestConfig.PerformExtensionlessConfigChecks) { pendingAuth.AttemptedChallenge.ConfigCheckedOK = check.IsSuccess; } } if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { // perform tls-sni-01 challenge response var check = PrepareChallengeResponse_TlsSni01(log, iisManager, domain, managedCertificate, pendingAuth); if (requestConfig.PerformTlsSniBindingConfigChecks) { // set config check OK if all checks return true pendingAuth.AttemptedChallenge.ConfigCheckedOK = check(); } } if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { // perform dns-01 challenge response var check = await PerformChallengeResponse_Dns01(log, domain, managedCertificate, pendingAuth, isTestMode : false, credentialsManager); pendingAuth.AttemptedChallenge.IsFailure = !check.Result.IsSuccess; pendingAuth.AttemptedChallenge.ChallengeResultMsg = check.Result.Message; pendingAuth.AttemptedChallenge.IsAwaitingUser = check.IsAwaitingUser; pendingAuth.AttemptedChallenge.PropagationSeconds = check.PropagationSeconds; pendingAuth.IsFailure = !check.Result.IsSuccess; pendingAuth.AuthorizationError = check.Result.Message; } } } return(pendingAuth); }
public Task <StatusMessage> TestChallengeResponse(ICertifiedServer iisManager, ManagedSite managedSite, bool isPreviewMode) { throw new System.NotImplementedException(); }
public CertifyManager() { var serverConfig = SharedUtils.ServiceConfigManager.GetAppServiceConfig(); SettingsManager.LoadAppSettings(); InitLogging(serverConfig); Util.SetSupportedTLSVersions(); _itemManager = new ItemManager(); _credentialsManager = new CredentialsManager(); _serverProvider = (ICertifiedServer) new ServerProviderIIS(); _progressResults = new ObservableCollection <RequestProgressState>(); _pluginManager = new PluginManager(); _pluginManager.LoadPlugins(new List <string> { "Licensing", "DashboardClient", "DeploymentTasks", "CertificateManagers" }); // load core CAs and custom CAs foreach (var ca in CertificateAuthority.CoreCertificateAuthorities) { _certificateAuthorities.TryAdd(ca.Id, ca); } try { var customCAs = SettingsManager.GetCustomCertificateAuthorities(); foreach (var ca in customCAs) { _certificateAuthorities.TryAdd(ca.Id, ca); } } catch (Exception exp) { // failed to load custom CAs _serviceLog.Error(exp.Message); } // init remaining utilities and optionally enable telematics _challengeDiagnostics = new ChallengeDiagnostics(CoreAppSettings.Current.EnableValidationProxyAPI); if (CoreAppSettings.Current.EnableAppTelematics) { _tc = new Util().InitTelemetry(); } _httpChallengePort = serverConfig.HttpChallengeServerPort; _httpChallengeServerClient.Timeout = new TimeSpan(0, 0, 20); if (_tc != null) { _tc.TrackEvent("ServiceStarted"); } _serviceLog?.Information("Certify Manager Started"); PerformAccountUpgrades().Wait(); PerformManagedCertificateMigrations().Wait(); }
/// <summary> /// Simulates responding to a challenge, performs a sample configuration and attempts to /// verify it. /// </summary> /// <param name="iisManager"></param> /// <param name="managedSite"></param> /// <returns> APIResult </returns> /// <remarks> /// The purpose of this method is to test the options (permissions, configuration) before /// submitting a request to the ACME server, to avoid creating failed requests and hitting /// usage limits. /// </remarks> public async Task <StatusMessage> TestChallengeResponse(ICertifiedServer iisManager, ManagedSite managedSite, bool isPreviewMode, bool enableDnsChecks) { return(await Task.Run(() => { _actionLogs.Clear(); // reset action logs var requestConfig = managedSite.RequestConfig; var result = new StatusMessage { IsOK = true }; var domains = new List <string> { requestConfig.PrimaryDomain }; if (requestConfig.SubjectAlternativeNames != null) { domains.AddRange(requestConfig.SubjectAlternativeNames); } var generatedAuthorizations = new List <PendingAuthorization>(); try { // if DNS checks enabled, attempt them here if (isPreviewMode && enableDnsChecks) { // check all domain configs Parallel.ForEach(domains.Distinct(), new ParallelOptions { // check 8 domains at a time MaxDegreeOfParallelism = 8 }, domain => { var(ok, message) = _netUtil.CheckDNS(domain); if (!ok) { result.IsOK = false; result.FailedItemSummary.Add(message); } }); if (!result.IsOK) { return result; } } if (requestConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { foreach (var domain in domains.Distinct()) { string challengeFileUrl = $"http://{domain}/.well-known/acme-challenge/configcheck"; var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP, ResourcePath = ".well-known\\acme-challenge\\configcheck", ResourceUri = challengeFileUrl, Value = "Extensionless File Config Test - OK" } } }; generatedAuthorizations.Add(simulatedAuthorization); var resultOK = PrepareChallengeResponse_Http01( iisManager, domain, managedSite, simulatedAuthorization )(); if (!resultOK) { result.IsOK = false; result.FailedItemSummary.Add($"Config checks failed to verify http://{domain} is both publicly accessible and can serve extensionless files e.g. {challengeFileUrl}"); // don't check any more after first failure break; } } } else if (requestConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { if (iisManager.GetServerVersion().Major < 8) { result.IsOK = false; result.FailedItemSummary.Add($"The {SupportedChallengeTypes.CHALLENGE_TYPE_SNI} challenge is only available for IIS versions 8+."); return result; } result.IsOK = domains.Distinct().All(domain => { var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_SNI, HashIterationCount = 1, Value = GenerateSimulatedKeyAuth() } } }; generatedAuthorizations.Add(simulatedAuthorization); return PrepareChallengeResponse_TlsSni01( iisManager, domain, managedSite, simulatedAuthorization )(); }); } else if (requestConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { result.IsOK = domains.Distinct().All(domain => { var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, Key = "_acme-challenge.test." + domain, Value = GenerateSimulatedKeyAuth() } } }; generatedAuthorizations.Add(simulatedAuthorization); return PrepareChallengeResponse_Dns01( domain, managedSite, simulatedAuthorization )(); }); } else { throw new NotSupportedException($"ChallengeType not supported: {requestConfig.ChallengeType}"); } } finally { //FIXME: needs to be filtered by managed site: result.Message = String.Join("\r\n", GetActionLogSummary()); generatedAuthorizations.ForEach(ga => ga.Cleanup()); } return result; })); }
/// <summary> /// Prepares IIS to respond to a http-01 challenge /// </summary> /// <returns> /// A Boolean returning Func. Invoke the Func to test the challenge response locally. /// </returns> private Func <bool> PrepareChallengeResponse_Http01(ICertifiedServer iisManager, string domain, ManagedSite managedSite, PendingAuthorization pendingAuth) { var requestConfig = managedSite.RequestConfig; var httpChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP); if (httpChallenge == null) { this.LogAction($"No http challenge to complete for {managedSite.Name}. Request cannot continue."); return(() => false); } this.LogAction("Preparing challenge response for Let's Encrypt server to check at: " + httpChallenge.ResourceUri); this.LogAction("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued."); // get website root path, expand environment variables if required string websiteRootPath = requestConfig.WebsiteRootPath; // if website root path not specified, determine it now if (String.IsNullOrEmpty(websiteRootPath)) { websiteRootPath = iisManager.GetSitePhysicalPath(managedSite); } if (!String.IsNullOrEmpty(websiteRootPath) && websiteRootPath.Contains("%")) { // if websiteRootPath contains %websiteroot% variable, replace that with the current // physical path for the site if (websiteRootPath.Contains("%websiteroot%")) { // sets env variable for this process only Environment.SetEnvironmentVariable("websiteroot", iisManager.GetSitePhysicalPath(managedSite)); } // expand any environment variables present in site path websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath); } if (String.IsNullOrEmpty(websiteRootPath) || !Directory.Exists(websiteRootPath)) { // our website no longer appears to exist on disk, continuing would potentially // create unwanted folders, so it's time for us to give up this.LogAction($"The website root path for {managedSite.Name} could not be determined. Request cannot continue."); return(() => false); } // copy temp file to path challenge expects in web folder var destFile = Path.Combine(websiteRootPath, httpChallenge.ResourcePath); var destPath = Path.GetDirectoryName(destFile); if (!Directory.Exists(destPath)) { try { Directory.CreateDirectory(destPath); } catch (Exception) { // failed to create directory, probably permissions or may be invalid config this.LogAction($"Pre-config check failed: Could not create directory: {destPath}"); return(() => { return false; }); } } // copy challenge response to web folder /.well-known/acme-challenge. Check if it already // exists (as in 'configcheck' file) as can cause conflicts. if (!File.Exists(destFile)) { try { File.WriteAllText(destFile, httpChallenge.Value); } catch (Exception) { // failed to create configcheck file, probably permissions or may be invalid config this.LogAction($"Pre-config check failed: Could not create file: {destFile}"); return(() => { return false; }); } } // configure cleanup - should this be configurable? Because in some case many sites // renewing may all point to the same web root, we keep the configcheck file pendingAuth.Cleanup = () => { if (!destFile.EndsWith("configcheck") && File.Exists(destFile)) { File.Delete(destFile); } }; // create a web.config for extensionless files, then test it (make a request for the // extensionless configcheck file over http) string webConfigContent = ConfigResources.IISWebConfig; if (!File.Exists(destPath + "\\web.config")) { // no existing config, attempt auto config and perform test this.LogAction($"Config does not exist, writing default config to: {destPath}\\web.config"); System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); return(() => _netUtil.CheckURL($"http://{domain}/{httpChallenge.ResourcePath}")); } else { // web config already exists, don't overwrite it, just test it return(() => { if (_netUtil.CheckURL(httpChallenge.ResourceUri)) { return true; } if (requestConfig.PerformAutoConfig) { this.LogAction($"Pre-config check failed: Auto-config will overwrite existing config: {destPath}\\web.config"); // didn't work, try our default config try { System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); } catch (System.IO.IOException) { this.LogAction($"Failed to update alternative web config: {destPath}\\web.config"); } if (_netUtil.CheckURL(httpChallenge.ResourceUri)) { return true; } } return false; }); } }
public CertifyManager(bool useWindowsNativeFeatures = true) { _useWindowsNativeFeatures = useWindowsNativeFeatures; _serverConfig = SharedUtils.ServiceConfigManager.GetAppServiceConfig(); SettingsManager.LoadAppSettings(); InitLogging(_serverConfig); Util.SetSupportedTLSVersions(); try { _itemManager = new ItemManager(null, _serviceLog); if (!_itemManager.IsInitialised()) { _serviceLog.Error($"Item Manager failed to initialise properly. Check service logs for more information."); } } catch (Exception exp) { _serviceLog.Error($"Failed to open or upgrade the managed items database. Check service has required file access permissions. :: {exp}"); } _credentialsManager = new CredentialsManager(useWindowsNativeFeatures); _serverProvider = (ICertifiedServer) new ServerProviderIIS(_serviceLog); _progressResults = new ObservableCollection <RequestProgressState>(); _pluginManager = new PluginManager(); _pluginManager.EnableExternalPlugins = CoreAppSettings.Current.IncludeExternalPlugins; _pluginManager.LoadPlugins(new List <string> { "Licensing", "DashboardClient", "DeploymentTasks", "CertificateManagers", "DnsProviders" }); _migrationManager = new MigrationManager(_itemManager, _credentialsManager, _serverProvider); LoadCertificateAuthorities(); // init remaining utilities and optionally enable telematics _challengeDiagnostics = new ChallengeDiagnostics(CoreAppSettings.Current.EnableValidationProxyAPI); if (CoreAppSettings.Current.EnableAppTelematics) { _tc = new Util().InitTelemetry(Locales.ConfigResources.AIInstrumentationKey); } _httpChallengePort = _serverConfig.HttpChallengeServerPort; _httpChallengeServerClient.Timeout = new TimeSpan(0, 0, 20); if (_tc != null) { _tc.TrackEvent("ServiceStarted"); } _serviceLog?.Information("Certify Manager Started"); try { PerformAccountUpgrades().Wait(); } catch (Exception exp) { _serviceLog.Error($"Failed to perform ACME account upgrades. :: {exp}"); } PerformManagedCertificateMigrations().Wait(); }
public MigrationManager(IItemManager itemManager, ICredentialsManager credentialsManager, ICertifiedServer targetServer) { _itemManager = itemManager; _credentialsManager = credentialsManager; _targetServer = targetServer; }
public PendingAuthorization PerformAutomatedChallengeResponse(ICertifiedServer iisManager, ManagedSite managedSite, PendingAuthorization pendingAuth) { throw new System.NotImplementedException(); }