Пример #1
0
        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);
        }
Пример #3
0
        /// <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}"
                });
            }
        }
Пример #4
0
        /// <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);
        }
Пример #5
0
        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
                });
            }
        }
Пример #6
0
        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
                });
            }
        }
Пример #7
0
        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");
        }