public async Task TestDNSTests() { var net = new NetworkUtils(enableProxyValidationAPI: true); var logImp = new LoggerConfiguration() .WriteTo.Debug() .CreateLogger(); var log = new Loggy(logImp); // check invalid domain var result = await net.CheckDNS(log, "fdlsakdfoweinoijsjdfpsdkfspdf.com"); Assert.IsFalse(result.All(r => r.IsSuccess), "Non-existant DNS does not throw an error"); result = await net.CheckDNS(log, "cloudapp.net"); Assert.IsFalse(result.All(r => r.IsSuccess), "Valid domain that does not resolve to an IP Address does not throw an error"); // certifytheweb.com = no CAA records result = await net.CheckDNS(log, "webprofusion.com"); Assert.IsTrue(result.All(r => r.IsSuccess), "CAA records are not required"); // google.com = no letsencrypt.org CAA records (returns "pki.goog" only) result = await net.CheckDNS(log, "google.com"); Assert.IsFalse(result.All(r => r.IsSuccess), "If CAA records are present, letsencrypt.org is returned."); // dnsimple.com = correctly configured letsencrypt.org CAA record result = await net.CheckDNS(log, "dnsimple.com"); Assert.IsTrue(result.All(r => r.IsSuccess), "Correctly configured LE CAA entries work"); // example.com = correctly configured DNSSEC record result = await net.CheckDNS(log, "example.com"); Assert.IsTrue(result.All(r => r.IsSuccess), "correctly configured DNSSEC record should pass dns check"); // dnssec-failed.org = incorrectly configured DNSSEC record result = await net.CheckDNS(log, "dnssec-failed.org"); Assert.IsFalse(result.All(r => r.IsSuccess), "incorrectly configured DNSSEC record should fail dns check"); }
public void TestDNSTests() { var net = new NetworkUtils(); // check invalid domain Assert.IsFalse(net.CheckDNS("fdlsakdfoweinoijsjdfpsdkfspdf.com").Ok, "Non-existant DNS does not throw an error"); Assert.IsFalse(net.CheckDNS("cloudapp.net").Ok, "Valid domain that does not resolve to an IP Address does not throw an error"); // certifytheweb.com = no CAA records Assert.IsTrue(net.CheckDNS("certifytheweb.com").Ok, "CAA records are not required"); // google.com = no letsencrypt.org CAA records (returns "pki.goog" only) Assert.IsFalse(net.CheckDNS("google.com").Ok, "If CAA records are present, letsencrypt.org is returned."); // dnsimple.com = correctly configured letsencrypt.org CAA record Assert.IsTrue(net.CheckDNS("dnsimple.com").Ok, "Correctly configured LE CAA entries work"); // example.com = correctly configured DNSSEC record Assert.IsTrue(net.CheckDNS("example.com").Ok, "correctly configured DNSSEC record should pass dns check"); // dnssec-failed.org = incorrectly configured DNSSEC record Assert.IsFalse(net.CheckDNS("dnssec-failed.org").Ok, "incorrectly configured DNSSEC record should fail dns check"); }
/// <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); }
/// <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 <APIResult> TestChallengeResponse(IISManager iisManager, ManagedSite managedSite, bool isPreviewMode) { return(await Task.Run(() => { ActionLogs.Clear(); // reset action logs var requestConfig = managedSite.RequestConfig; var result = new APIResult(); 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 && CoreAppSettings.Current.EnableDNSValidationChecks) { // 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 == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP) { foreach (var domain in domains.Distinct()) { string challengeFileUrl = $"http://{domain}/.well-known/acme-challenge/configcheck"; var simulatedAuthorization = new PendingAuthorization { Challenge = new AuthorizeChallengeItem { ChallengeData = new ACMESharp.ACME.HttpChallenge(ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP, new ACMESharp.ACME.HttpChallengeAnswer { KeyAuthorization = GenerateSimulatedKeyAuth() }) { FilePath = ".well-known/acme-challenge/configcheck", FileContent = "Extensionless File Config Test - OK", FileUrl = challengeFileUrl } } }; 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}"); } } } else if (requestConfig.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI) { if (iisManager.GetIisVersion().Major < 8) { result.IsOK = false; result.FailedItemSummary.Add($"The {ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI} challenge is only available for IIS versions 8+."); return result; } result.IsOK = domains.Distinct().All(domain => { var simulatedAuthorization = new PendingAuthorization { Challenge = new AuthorizeChallengeItem() { ChallengeData = new ACMESharp.ACME.TlsSniChallenge(ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI, new ACMESharp.ACME.TlsSniChallengeAnswer { KeyAuthorization = GenerateSimulatedKeyAuth() }) { IterationCount = 1 } } }; generatedAuthorizations.Add(simulatedAuthorization); return PrepareChallengeResponse_TlsSni01( iisManager, 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> /// 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; })); }