Beispiel #1
0
        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);
        }
Beispiel #2
0
        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);
        }
Beispiel #3
0
        /// <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;
                });
            }
        }
Beispiel #4
0
        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);
        }
Beispiel #5
0
        /// <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;
            }));
        }
Beispiel #6
0
        /// <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()));
        }