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); }
/// <summary> /// Perform set of test challenges and configuration checks to determine if site appears /// valid for certificate requests /// </summary> /// <param name="managedCertificate"> managed site to check </param> /// <param name="isPreviewMode"> /// If true, perform full set of checks (DNS etc), if false performs minimal/basic checks /// </param> /// <returns> </returns> public async Task <List <StatusMessage> > TestChallenge(ILog log, ManagedCertificate managedCertificate, bool isPreviewMode, IProgress <RequestProgressState> progress = null) { var results = new List <StatusMessage>(); if (managedCertificate.RequestConfig.PerformAutoConfig && managedCertificate.GetChallengeConfig(null).ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { var serverCheck = await _serverProvider.RunConfigurationDiagnostics(managedCertificate.ServerSiteId); results.AddRange(serverCheck.ConvertAll(x => new StatusMessage { IsOK = !x.HasError, HasWarning = x.HasWarning, Message = x.Description })); } if (managedCertificate.GetChallengeConfig(null).ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { if (CoreAppSettings.Current.EnableHttpChallengeServer) { _httpChallengeServerAvailable = await StartHttpChallengeServer(); if (_httpChallengeServerAvailable) { results.Add(new StatusMessage { IsOK = true, Message = "Http Challenge Server process available." }); } else { results.Add(new StatusMessage { IsOK = true, HasWarning = true, Message = "Built-in Http Challenge Server process unavailable or could not start. Challenge responses will fall back to IIS." }); } } } results.AddRange( await _challengeDiagnostics.TestChallengeResponse( log, _serverProvider, managedCertificate, isPreviewMode, CoreAppSettings.Current.EnableDNSValidationChecks, _credentialsManager, progress ) ); if (progress != null) { if (results.Any(r => r.IsOK == false)) { ReportProgress(progress, new RequestProgressState(RequestState.Error, "One or more tests failed", managedCertificate, isPreviewMode)); } else { ReportProgress(progress, new RequestProgressState(RequestState.Success, "All Tests Completed OK", managedCertificate, isPreviewMode)); } } if (CoreAppSettings.Current.EnableHttpChallengeServer) { await StopHttpChallengeServer(); } return(results); }
/// <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 <DnsChallengeHelperResult> DeleteDNSChallenge(ILog log, ManagedCertificate managedcertificate, string domain, string txtRecordName, string txtRecordValue) { // for a given managed site configuration, attempt to delete the TXT record created for // the challenge var credentials = new Dictionary <string, string>(); IDnsProvider dnsAPIProvider = null; var challengeConfig = managedcertificate.GetChallengeConfig(domain); if (challengeConfig == null || challengeConfig.ChallengeProvider == null) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = true, Message = $"The DNS record {txtRecordName} can now be removed." }, PropagationSeconds = 0, IsAwaitingUser = false }); } if (challengeConfig.ChallengeProvider.Contains(".Manual")) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = true, Message = $"The DNS record {txtRecordName} can now be removed." }, PropagationSeconds = 0, IsAwaitingUser = true }); } if (!string.IsNullOrEmpty(challengeConfig.ChallengeCredentialKey)) { // decode credentials string array try { credentials = await _credentialsManager.GetUnlockedCredentialsDictionary(challengeConfig.ChallengeCredentialKey); } catch (Exception) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "DNS Challenge API Credentials could not be decrypted. The original user must be used for decryption." }, PropagationSeconds = 0, IsAwaitingUser = false }); } } var parameters = new Dictionary <string, string>(); if (challengeConfig.Parameters != null) { foreach (var p in challengeConfig.Parameters) { parameters.Add(p.Key, p.Value); } } try { dnsAPIProvider = await ChallengeProviders.GetDnsProvider(challengeConfig.ChallengeProvider, credentials, parameters); } catch (ChallengeProviders.CredentialsRequiredException) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "This DNS Challenge API requires one or more credentials to be specified." }, PropagationSeconds = 0, IsAwaitingUser = false }); } catch (Exception exp) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = $"DNS Challenge API Provider could not be created. Check all required credentials are set. {exp.ToString()}" }, PropagationSeconds = 0, IsAwaitingUser = false }); } if (dnsAPIProvider == null) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "DNS Challenge API Provider not set or not recognised. Select an API to proceed." }, PropagationSeconds = 0, IsAwaitingUser = false }); } string zoneId = null; if (parameters != null && parameters.ContainsKey("zoneid")) { zoneId = parameters["zoneid"]?.Trim(); } else { zoneId = challengeConfig.ZoneId?.Trim(); } if (dnsAPIProvider != null) { //most DNS providers require domains to by ASCII txtRecordName = _idnMapping.GetAscii(txtRecordName).ToLower(); log.Information($"DNS: Deleting TXT Record '{txtRecordName}', in Zone Id '{zoneId}' using API provider '{dnsAPIProvider.ProviderTitle}'"); try { var result = await dnsAPIProvider.DeleteRecord(new DnsRecord { RecordType = "TXT", TargetDomainName = domain, RecordName = txtRecordName, RecordValue = txtRecordValue, ZoneId = zoneId }); result.Message = $"{dnsAPIProvider.ProviderTitle} :: {result.Message}"; return(new DnsChallengeHelperResult { Result = result, PropagationSeconds = dnsAPIProvider.PropagationDelaySeconds, IsAwaitingUser = challengeConfig.ChallengeProvider.Contains(".Manual") }); } catch (Exception exp) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = $"Failed [{dnsAPIProvider.ProviderTitle}]: " + exp.Message }, PropagationSeconds = 0, IsAwaitingUser = false }); } } else { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "Error: Could not determine DNS API Provider." }, PropagationSeconds = 0, IsAwaitingUser = false }); } }
public async Task <DnsChallengeHelperResult> CompleteDNSChallenge(ILog log, ManagedCertificate managedcertificate, string domain, string txtRecordName, string txtRecordValue, bool isTestMode) { // for a given managed site configuration, attempt to complete the required challenge by // creating the required TXT record var credentials = new Dictionary <string, string>(); IDnsProvider dnsAPIProvider = null; var challengeConfig = managedcertificate.GetChallengeConfig(domain); /*if (String.IsNullOrEmpty(challengeConfig.ZoneId)) * { * return new ActionResult { IsSuccess = false, Message = "DNS Challenge Zone Id not set. Set the Zone Id to proceed." }; * }*/ if (!string.IsNullOrEmpty(challengeConfig.ChallengeCredentialKey)) { // decode credentials string array try { credentials = await _credentialsManager.GetUnlockedCredentialsDictionary(challengeConfig.ChallengeCredentialKey); } catch (Exception) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "DNS Challenge API Credentials could not be decrypted. The original user must be used for decryption." }, PropagationSeconds = 0, IsAwaitingUser = false }); } } var parameters = new Dictionary <string, string>(); if (challengeConfig.Parameters != null) { foreach (var p in challengeConfig.Parameters) { parameters.Add(p.Key, p.Value); } } try { dnsAPIProvider = await ChallengeProviders.GetDnsProvider(challengeConfig.ChallengeProvider, credentials, parameters, log); } catch (ChallengeProviders.CredentialsRequiredException) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "This DNS Challenge API requires one or more credentials to be specified." }, PropagationSeconds = 0, IsAwaitingUser = false }); } catch (Exception exp) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = $"DNS Challenge API Provider could not be created. Check all required credentials are set. {exp.ToString()}" }, PropagationSeconds = 0, IsAwaitingUser = false }); } if (dnsAPIProvider == null) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "DNS Challenge API Provider not set or not recognised. Select an API to proceed." }, PropagationSeconds = 0, IsAwaitingUser = false }); } if (isTestMode && !dnsAPIProvider.IsTestModeSupported) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = true, Message = dnsAPIProvider.ProviderTitle + " does not perform any tests." }, PropagationSeconds = 0, IsAwaitingUser = false }); } string zoneId = null; if (parameters != null && parameters.ContainsKey("zoneid")) { zoneId = parameters["zoneid"]?.Trim(); } else { zoneId = challengeConfig.ZoneId?.Trim(); } if (dnsAPIProvider != null) { //most DNS providers require domains to by ASCII txtRecordName = _idnMapping.GetAscii(txtRecordName).ToLower(); log.Information($"DNS: Creating TXT Record '{txtRecordName}' with value '{txtRecordValue}', in Zone Id '{zoneId}' using API provider '{dnsAPIProvider.ProviderTitle}'"); try { var result = await dnsAPIProvider.CreateRecord(new DnsRecord { RecordType = "TXT", TargetDomainName = domain, RecordName = txtRecordName, RecordValue = txtRecordValue, ZoneId = zoneId }); result.Message = $"{dnsAPIProvider.ProviderTitle} :: {result.Message}"; bool isAwaitingUser = false; if (challengeConfig.ChallengeProvider.Contains(".Manual") || result.Message.Contains("[Action Required]")) { isAwaitingUser = true; } return(new DnsChallengeHelperResult { Result = result, PropagationSeconds = dnsAPIProvider.PropagationDelaySeconds, IsAwaitingUser = isAwaitingUser }); } catch (Exception exp) { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = $"Failed [{dnsAPIProvider.ProviderTitle}]: " + exp.Message }, PropagationSeconds = 0, IsAwaitingUser = false }); } //TODO: DNS query to check for new record /* * if (result.IsSuccess) * { * // do our own txt record query before proceeding with challenge completion * * int attempts = 3; * bool recordCheckedOK = false; * var networkUtil = new NetworkUtils(false); * * while (attempts > 0 && !recordCheckedOK) * { * recordCheckedOK = networkUtil.CheckDNSRecordTXT(domain, txtRecordName, txtRecordValue); * attempts--; * if (!recordCheckedOK) * { * await Task.Delay(1000); // hold on a sec * } * } * * // wait for provider specific propogation delay * * // FIXME: perform validation check in DNS nameservers await * // Task.Delay(dnsAPIProvider.PropagationDelaySeconds * 1000); * * return result; * } * else * { * return result; * } */ } else { return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = "Error: Could not determine DNS API Provider." }, PropagationSeconds = 0, IsAwaitingUser = false }); } }
public void MultiChallengeConfigMatch() { var managedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = "TestSite..", GroupId = "test", RequestConfig = new CertRequestConfig { PrimaryDomain = "test.com", SubjectAlternativeNames = new string[] { "*.fred.com", "fred.com", "www.fred.com", "example.com", "www.example.com", "www.subdomain.example.com" }, Challenges = new ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = "http-01", DomainMatch = null, ChallengeCredentialKey = "config-default" }, new CertRequestChallengeConfig { ChallengeType = "dns-01", DomainMatch = "*.fred.com", ChallengeCredentialKey = "config-wildcard" }, new CertRequestChallengeConfig { ChallengeType = "dns-01", DomainMatch = "fred.com", ChallengeCredentialKey = "config2" }, new CertRequestChallengeConfig { ChallengeType = "dns-01", DomainMatch = "subdomain.example.com", ChallengeCredentialKey = "config3" }, new CertRequestChallengeConfig { ChallengeType = "http-01", DomainMatch = "example.com;www.exaomple.com;*.exaomple1.com", ChallengeCredentialKey = "config4" }, }), PerformAutomatedCertBinding = true, WebsiteRootPath = "c:\\inetpub\\wwwroot", DeploymentSiteOption = DeploymentOption.SingleSite }, ItemType = ManagedCertificateType.SSL_ACME }; // Assert var configMatch = managedCertificate.GetChallengeConfig(null); Assert.AreEqual("config-default", configMatch.ChallengeCredentialKey, "Blank domain should match blank domain match config"); configMatch = managedCertificate.GetChallengeConfig("*.fred.com"); Assert.AreEqual("config-wildcard", configMatch.ChallengeCredentialKey, "Should match on wildcard"); configMatch = managedCertificate.GetChallengeConfig("fred.com"); Assert.AreEqual("config2", configMatch.ChallengeCredentialKey, "Should match on domain"); configMatch = managedCertificate.GetChallengeConfig("subdomain.example.com"); Assert.AreEqual("config3", configMatch.ChallengeCredentialKey, "Should match on domain"); configMatch = managedCertificate.GetChallengeConfig("www.example.com"); Assert.AreEqual("config-default", configMatch.ChallengeCredentialKey, "Should match default"); configMatch = managedCertificate.GetChallengeConfig("www.exaomple.com"); Assert.AreEqual("config4", configMatch.ChallengeCredentialKey, "Should match on domain"); configMatch = managedCertificate.GetChallengeConfig("subdomain.exaomple1.com"); Assert.AreEqual("config4", configMatch.ChallengeCredentialKey, "Should match on domain wildcard"); configMatch = managedCertificate.GetChallengeConfig("www.subdomain.exaomple1.com"); Assert.AreEqual("config-default", configMatch.ChallengeCredentialKey, "Should not match on domain wildcard"); configMatch = managedCertificate.GetChallengeConfig("example.com"); Assert.AreEqual("config4", configMatch.ChallengeCredentialKey, "Should match on domain"); configMatch = managedCertificate.GetChallengeConfig("www.microsoft.com"); Assert.AreEqual("config-default", configMatch.ChallengeCredentialKey, "Should match default"); }