public PendingAuthorization PerformIISAutomatedChallengeResponse(CertRequestConfig requestConfig, PendingAuthorization pendingAuth) { bool extensionlessConfigOK = false; bool checkViaProxy = true; //if copying the file for the user, attempt that now if (pendingAuth.Challenge != null && requestConfig.PerformChallengeFileCopy) { var httpChallenge = (ACMESharp.ACME.HttpChallenge)pendingAuth.Challenge.Challenge; this.LogAction("Preparing challenge response for LetsEncrypt server to check at: " + httpChallenge.FileUrl); this.LogAction("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued."); //copy temp file to path challenge expects in web folder var destFile = Path.Combine(requestConfig.WebsiteRootPath, httpChallenge.FilePath); var destPath = Path.GetDirectoryName(destFile); if (!Directory.Exists(destPath)) { Directory.CreateDirectory(destPath); } //copy challenge response to web folder /.well-known/acme-challenge System.IO.File.WriteAllText(destFile, httpChallenge.FileContent); var wellknownContentPath = httpChallenge.FilePath.Substring(0, httpChallenge.FilePath.LastIndexOf("/")); var testFilePath = Path.Combine(requestConfig.WebsiteRootPath, wellknownContentPath + "//configcheck"); System.IO.File.WriteAllText(testFilePath, "Extensionless File Config Test - OK"); //create a web.config for extensionless files, then test it (make a request for the extensionless configcheck file over http) string webConfigContent = Properties.Resources.IISWebConfig; if (!File.Exists(destPath + "\\web.config")) { //no existing config, attempt auto config and perform test System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (requestConfig.PerformExtensionlessConfigChecks) { if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { extensionlessConfigOK = true; } } } else { //web config already exists, don't overwrite it, just test it if (requestConfig.PerformExtensionlessConfigChecks) { if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { extensionlessConfigOK = true; } if (!extensionlessConfigOK && requestConfig.PerformAutoConfig) { //didn't work, try our default config System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { extensionlessConfigOK = true; } } } } if (!extensionlessConfigOK && requestConfig.PerformAutoConfig) { //if first attempt(s) at config failed, try an alternative config webConfigContent = Properties.Resources.IISWebConfigAlt; System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { //ready to complete challenge extensionlessConfigOK = true; } } } //configuration applied, ready to ask LE to validate our answer pendingAuth.ExtensionlessConfigCheckedOK = extensionlessConfigOK; return(pendingAuth); }
public PendingAuthorization PerformIISAutomatedChallengeResponse(IISManager iisManager, ManagedSite managedSite, PendingAuthorization pendingAuth) { var requestConfig = managedSite.RequestConfig; bool extensionlessConfigOK = false; //if validation proxy enabled, access to the domain being validated is checked via our remote API rather than directly on the servers bool checkViaProxy = Certify.Properties.Settings.Default.EnableValidationProxyAPI; //if copying the file for the user, attempt that now if (pendingAuth.Challenge != null && requestConfig.PerformChallengeFileCopy) { var httpChallenge = (ACMESharp.ACME.HttpChallenge)pendingAuth.Challenge.ChallengeData; this.LogAction("Preparing challenge response for LetsEncrypt server to check at: " + httpChallenge.FileUrl); this.LogAction("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued."); // get website root path string websiteRootPath = requestConfig.WebsiteRootPath; Environment.SetEnvironmentVariable("websiteroot", iisManager.GetSitePhysicalPath(managedSite)); // sets env variable for this process only websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath); // expand all env variables //copy temp file to path challenge expects in web folder var destFile = Path.Combine(websiteRootPath, httpChallenge.FilePath); var destPath = Path.GetDirectoryName(destFile); if (!Directory.Exists(destPath)) { Directory.CreateDirectory(destPath); } //copy challenge response to web folder /.well-known/acme-challenge System.IO.File.WriteAllText(destFile, httpChallenge.FileContent); var wellknownContentPath = httpChallenge.FilePath.Substring(0, httpChallenge.FilePath.LastIndexOf("/")); var testFilePath = Path.Combine(websiteRootPath, wellknownContentPath + "//configcheck"); // write the config check file if it doesn't already exist if (!File.Exists(testFilePath)) { System.IO.File.WriteAllText(testFilePath, "Extensionless File Config Test - OK"); } //create a web.config for extensionless files, then test it (make a request for the extensionless configcheck file over http) string webConfigContent = Core.Properties.Resources.IISWebConfig; if (!File.Exists(destPath + "\\web.config")) { //no existing config, attempt auto config and perform test System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (requestConfig.PerformExtensionlessConfigChecks) { if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { extensionlessConfigOK = true; } } } else { //web config already exists, don't overwrite it, just test it if (requestConfig.PerformExtensionlessConfigChecks) { if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { extensionlessConfigOK = true; } if (!extensionlessConfigOK && requestConfig.PerformAutoConfig) { //didn't work, try our default config System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { extensionlessConfigOK = true; } } } } if (requestConfig.PerformExtensionlessConfigChecks) { if (!extensionlessConfigOK && requestConfig.PerformAutoConfig) { //if first attempt(s) at config failed, try an alternative config webConfigContent = Properties.Resources.IISWebConfigAlt; System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (CheckURL("http://" + requestConfig.PrimaryDomain + "/" + wellknownContentPath + "/configcheck", checkViaProxy)) { //ready to complete challenge extensionlessConfigOK = true; } } } } //configuration applied, ready to ask LE to validate our answer pendingAuth.ExtensionlessConfigCheckedOK = extensionlessConfigOK; return(pendingAuth); }
/// <summary> /// Prepares IIS to respond to a http-01 challenge /// </summary> /// <returns> /// A Boolean returning Func. Invoke the Func to test the challenge response locally. /// </returns> private Func <bool> PrepareChallengeResponse_Http01(IISManager iisManager, string domain, ManagedSite managedSite, PendingAuthorization pendingAuth) { var requestConfig = managedSite.RequestConfig; var httpChallenge = (ACMESharp.ACME.HttpChallenge)pendingAuth.Challenge.ChallengeData; this.LogAction("Preparing challenge response for LetsEncrypt server to check at: " + httpChallenge.FileUrl); this.LogAction("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued."); // get website root path, expand environment variables if required string websiteRootPath = requestConfig.WebsiteRootPath; // if website root path not specified, determine it now if (String.IsNullOrEmpty(websiteRootPath)) { websiteRootPath = iisManager.GetSitePhysicalPath(managedSite); } if (!String.IsNullOrEmpty(websiteRootPath) && websiteRootPath.Contains("%")) { // if websiteRootPath contains %websiteroot% variable, replace that with the current // physical path for the site if (websiteRootPath.Contains("%websiteroot%")) { // sets env variable for this process only Environment.SetEnvironmentVariable("websiteroot", iisManager.GetSitePhysicalPath(managedSite)); } // expand any environment variables present in site path websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath); } if (String.IsNullOrEmpty(websiteRootPath) || !Directory.Exists(websiteRootPath)) { // our website no longer appears to exist on disk, continuing would potentially // create unwanted folders, so it's time for us to give up this.LogAction($"The website root path for {managedSite.Name} could not be determined. Request cannot continue."); return(() => false); } // copy temp file to path challenge expects in web folder var destFile = Path.Combine(websiteRootPath, httpChallenge.FilePath); var destPath = Path.GetDirectoryName(destFile); if (!Directory.Exists(destPath)) { Directory.CreateDirectory(destPath); } // 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)) { File.WriteAllText(destFile, httpChallenge.FileContent); } // configure cleanup - should this be configurable? Because in some case many sites // renewing may all point to the same web root, we keep the configcheck file pendingAuth.Cleanup = () => { if (!destFile.EndsWith("configcheck")) { File.Delete(destFile); } }; // create a web.config for extensionless files, then test it (make a request for the // extensionless configcheck file over http) string webConfigContent = Core.Properties.Resources.IISWebConfig; if (!File.Exists(destPath + "\\web.config")) { // no existing config, attempt auto config and perform test this.LogAction($"Config does not exist, writing default config to: {destPath}\\web.config"); System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); return(() => NetUtil.CheckURL($"http://{domain}/{httpChallenge.FilePath}")); } else { // web config already exists, don't overwrite it, just test it return(() => { if (NetUtil.CheckURL($"http://{domain}/{httpChallenge.FilePath}")) { return true; } if (requestConfig.PerformAutoConfig) { this.LogAction($"Pre-config check failed: Auto-config will overwrite existing config: {destPath}\\web.config"); // didn't work, try our default config System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); if (NetUtil.CheckURL($"http://{domain}/{httpChallenge.FilePath}")) { return true; } } return false; }); } }
public PendingAuthorization PerformIISAutomatedChallengeResponse(IISManager iisManager, ManagedSite managedSite, PendingAuthorization pendingAuth) { var requestConfig = managedSite.RequestConfig; var domain = pendingAuth.Identifier.Dns; if (pendingAuth.Challenge != null) { if (pendingAuth.Challenge.ChallengeData is ACMESharp.ACME.HttpChallenge && requestConfig.PerformChallengeFileCopy /* is this needed? */) { var check = PrepareChallengeResponse_Http01(iisManager, domain, managedSite, pendingAuth); if (requestConfig.PerformExtensionlessConfigChecks) { pendingAuth.ExtensionlessConfigCheckedOK = check(); } } if (pendingAuth.Challenge.ChallengeData is ACMESharp.ACME.TlsSniChallenge) { var check = PrepareChallengeResponse_TlsSni01(iisManager, domain, managedSite, pendingAuth); if (requestConfig.PerformTlsSniBindingConfigChecks) { // set config check OK if all checks return true pendingAuth.TlsSniConfigCheckedOK = check(); } } } return(pendingAuth); }
/// <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> /// Prepares IIS to respond to a tls-sni-01 challenge /// </summary> /// <returns> /// A Boolean-returning Func. Invoke the Func to test the challenge response locally. /// </returns> private Func <bool> PrepareChallengeResponse_TlsSni01(IISManager iisManager, string domain, ManagedSite managedSite, PendingAuthorization pendingAuth) { var requestConfig = managedSite.RequestConfig; var tlsSniChallenge = (ACMESharp.ACME.TlsSniChallenge)pendingAuth.Challenge.ChallengeData; var tlsSniAnswer = (ACMESharp.ACME.TlsSniChallengeAnswer)tlsSniChallenge.Answer; var sha256 = System.Security.Cryptography.SHA256.Create(); var z = new byte[tlsSniChallenge.IterationCount][]; // compute n sha256 hashes, where n=challengedata.iterationcount z[0] = sha256.ComputeHash(Encoding.UTF8.GetBytes(tlsSniAnswer.KeyAuthorization)); for (int i = 1; i < z.Length; i++) { z[i] = sha256.ComputeHash(z[i - 1]); } // generate certs and install iis bindings var cleanupQueue = new List <Action>(); var checkQueue = new List <Func <bool> >(); foreach (string hex in z.Select(b => BitConverter.ToString(b).Replace("-", "").ToLower())) { string sni = $"{hex.Substring(0, 32)}.{hex.Substring(32)}.acme.invalid"; this.LogAction($"Preparing binding at: https://{domain}, sni: {sni}"); var x509 = CertificateManager.GenerateTlsSni01Certificate(sni); CertificateManager.StoreCertificate(x509); iisManager.InstallCertificateforBinding(managedSite, x509, sni); // add check to the queue checkQueue.Add(() => NetUtil.CheckSNI(domain, sni)); // add cleanup actions to queue cleanupQueue.Add(() => iisManager.RemoveHttpsBinding(managedSite, sni)); cleanupQueue.Add(() => CertificateManager.RemoveCertificate(x509)); } // configure cleanup to execute the cleanup queue pendingAuth.Cleanup = () => cleanupQueue.ForEach(a => a()); // perform our own config checks pendingAuth.TlsSniConfigCheckedOK = true; return(() => checkQueue.All(check => check())); }