Example #1
0
        /// <summary>
        /// Sends an HTTP Request with the requested parameters
        /// </summary>
        /// <param name="url"></param>
        /// <param name="method"></param>
        /// <param name="contentType"></param>
        /// <param name="body"></param>
        /// <returns> A named Tuple with Success boolean and int StatusCode of the HTTP Request </returns>
        public async static Task <WebhookResult> SendRequest(CertRequestConfig config, bool forSuccess)
        {
            using (var client = new HttpClient())
            {
                client.DefaultRequestHeaders.Add("User-Agent", Management.Util.GetUserAgent() + " CertifyManager");

                HttpRequestMessage message;
                string             Url = ParseValues(config.WebhookUrl, config, forSuccess, true);
                switch (config.WebhookMethod)
                {
                case METHOD_GET:
                    message = new HttpRequestMessage(HttpMethod.Get, Url);
                    break;

                case METHOD_POST:
                    message = new HttpRequestMessage(HttpMethod.Post, Url)
                    {
                        Content = new StringContent(
                            ParseValues(!string.IsNullOrEmpty(config.WebhookContentBody) ? config.WebhookContentBody : DEFAULT_BODY,
                                        config, forSuccess, false),
                            Encoding.UTF8,
                            string.IsNullOrEmpty(config.WebhookContentType) ? "application/json" : config.WebhookContentType)
                    };
                    break;

                default:
                    throw new ArgumentException("Method must be GET or POST", "method");
                }
                var resp = await client.SendAsync(message);

                return(new WebhookResult(resp.IsSuccessStatusCode, (int)resp.StatusCode));
            }
        }
Example #2
0
        /// <summary>
        /// Provides templating variable replacement for Config values
        /// </summary>
        /// <param name="template"></param>
        /// <param name="config"></param>
        /// <param name="forSuccess"></param>
        /// <returns></returns>
        private static string ParseValues(string template, CertRequestConfig config, bool forSuccess, bool url_encode)
        {
            // add all config properties to template vars
            var vars = new Dictionary <string, string>();

            foreach (var prop in config.GetType().GetProperties())
            {
                string value = prop.GetValue(config)?.ToString() ?? "";
                if (url_encode)
                {
                    value = WebUtility.UrlEncode(value);
                }
                else
                {
                    value = value.Replace(@"\", @"\\");
                }
                vars[prop.Name.ToLower()] = value;
            }

            // add special processing for these values
            vars["success"] = forSuccess ? "true" : "false";
            vars["subjectalternativenames"] = string.Join(",", config.SubjectAlternativeNames ?? new string[] { config.PrimaryDomain });

            // process the template and replace values
            return(Regex.Replace(template, @"\$(\w+)(?=[\W$])", m =>
            {
                // replace var if it can be found, otherwise don't
                string key = m.Groups[1].Value.ToLower();
                return vars.ContainsKey(key) ? vars[key] : "$" + key;
            },
                                 RegexOptions.IgnoreCase));
        }
        public async Task <PendingOrder> BeginCertificateOrder(ILog log, CertRequestConfig config, string orderUri = null)
        {
            var domains = new List <string> {
                config.PrimaryDomain
            };

            domains.AddRange(config.SubjectAlternativeNames);

            var order = await _client.CreateOrderAsync(domains.Distinct());

            var authorizations = new List <PendingAuthorization>();

            foreach (var authz in order.Payload.Authorizations)
            {
                var pendingAuthz = new PendingAuthorization
                {
                    Identifier = new IdentifierItem {
                        Id = authz
                    }
                    //TODO
                };
                authorizations.Add(pendingAuthz);
            }
            return(new PendingOrder {
                OrderUri = order.OrderUrl, Authorizations = authorizations
            });
        }
Example #4
0
        /// <summary>
        /// Creates or updates the htttps bindinds associated with the dns names in the current request config, using the requested port/ips or autobinding
        /// </summary>
        /// <param name="requestConfig"></param>
        /// <param name="pfxPath"></param>
        /// <param name="cleanupCertStore"></param>
        /// <returns></returns>
        internal bool InstallCertForRequest(CertRequestConfig requestConfig, string pfxPath, bool cleanupCertStore)
        {
            if (new System.IO.FileInfo(pfxPath).Length == 0)
            {
                throw new ArgumentException("InstallCertForRequest: Invalid PFX File");
            }

            //store cert against primary domain
            var storedCert = StoreCertificate(requestConfig.PrimaryDomain, pfxPath);

            if (storedCert != null)
            {
                List <string> dnsHosts = new List <string> {
                    requestConfig.PrimaryDomain
                };
                if (requestConfig.SubjectAlternativeNames != null)
                {
                    dnsHosts.AddRange(requestConfig.SubjectAlternativeNames);
                }
                dnsHosts = dnsHosts.Distinct().ToList();

                foreach (var hostname in dnsHosts)
                {
                    //match dns host to IIS site
                    var site = GetSiteByDomain(hostname);
                    if (site != null)
                    {
                        //create/update binding and associate new cert
                        if (!requestConfig.PerformAutomatedCertBinding)
                        {
                            //create auto binding and use SNI
                            InstallCertificateforBinding(site, storedCert, hostname);
                        }
                        else
                        {
                            //if any binding elements configured, use those, otherwise auto bind using defaults and SNI
                            InstallCertificateforBinding(site, storedCert, hostname,
                                                         sslPort: !String.IsNullOrEmpty(requestConfig.BindingPort) ? int.Parse(requestConfig.BindingPort) : 443,
                                                         useSNI: (requestConfig.BindingUseSNI != null ? (bool)requestConfig.BindingUseSNI : true),
                                                         ipAddress: requestConfig.BindingIPAddress
                                                         );
                        }
                    }
                }

                if (cleanupCertStore)
                {
                    //remove old certs for this primary domain
                    CleanupCertificateDuplicates(storedCert, requestConfig.PrimaryDomain);
                }

                return(true);
            }
            else
            {
                return(false);
            }
        }
Example #5
0
        /// <summary>
        /// For the given set of options get a new CertRequestConfig to store
        /// </summary>
        /// <returns></returns>
        private ManagedSite GetUpdatedManagedSiteSettings()
        {
            CertRequestConfig config = new CertRequestConfig();

            RefreshDomainOptionSettingsFromUI();

            var primaryDomain = this.domains.FirstOrDefault(d => d.IsPrimaryDomain == true && d.IsSelected == true);

            if (primaryDomain == null)
            {
                primaryDomain = this.domains.FirstOrDefault(d => d.IsSelected == true);
            }

            config.PrimaryDomain = _idnMapping.GetAscii(primaryDomain.Domain); // ACME service requires international domain names in ascii mode
            if (this.domains.Count(d => d.IsSelected) > 1)
            {
                //apply remaining selected domains as subject alternative names
                config.SubjectAlternativeNames =
                    this.domains.Where(dm => dm.Domain != primaryDomain.Domain && dm.IsSelected == true)
                    .Select(i => i.Domain)
                    .ToArray();
            }

            config.PerformChallengeFileCopy         = true;
            config.PerformExtensionlessConfigChecks = !chkSkipConfigCheck.Checked;
            config.PerformAutoConfig = true;

            config.EnableFailureNotifications = chkEnableNotifications.Checked;

            //determine if this site has an existing entry in Managed Sites, if so use that, otherwise start a new one
            ManagedSite managedSite = _selectedManagedSite;

            if (managedSite == null)
            {
                managedSite = new ManagedSite();

                var siteInfo = (SiteBindingItem)lstSites.SelectedItem;

                managedSite.Id                 = Guid.NewGuid().ToString() + ":" + siteInfo.SiteId;
                managedSite.GroupId            = siteInfo.SiteId;
                managedSite.IncludeInAutoRenew = chkIncludeInAutoRenew.Checked;
                config.WebsiteRootPath         = Environment.ExpandEnvironmentVariables(siteInfo.PhysicalPath);
            }
            else
            {
                managedSite.IncludeInAutoRenew = chkIncludeInAutoRenew.Checked;
            }

            managedSite.ItemType = ManagedItemType.SSL_LetsEncrypt_LocalIIS;
            managedSite.Name     = txtManagedSiteName.Text;

            //store domain options settings and request config for this site so we can replay for automated renewal
            managedSite.DomainOptions = this.domains;
            managedSite.RequestConfig = config;

            return(managedSite);
        }
        public async Task <ProcessStepResult> CompleteCertificateRequest(ILog log, CertRequestConfig config, string orderId, string pwd)
        {
            var order = await _client.GetOrderDetailsAsync("");

            _ = await _client.GetOrderCertificateAsync(order);

            return(new ProcessStepResult {
                IsSuccess = true
            });
        }
Example #7
0
        /// <summary>
        /// Provides templating variable replacement for Config values
        /// </summary>
        /// <param name="template"></param>
        /// <param name="config"></param>
        /// <param name="forSuccess"></param>
        /// <returns></returns>
        private static string ParseValues(string template, CertRequestConfig config, bool forSuccess, bool url_encode)
        {
            // add all config properties to template vars
            var vars = new Dictionary <string, string>();

            foreach (var prop in config.GetType().GetProperties())
            {
                var objValue = prop.GetValue(config);

                var value = "";
                if (objValue != null && objValue is Array)
                {
                    foreach (var i in ((Array)objValue))
                    {
                        value += i.ToString() + " ";
                    }
                }
                else
                {
                    value = objValue?.ToString() ?? "";
                }


                if (url_encode)
                {
                    value = WebUtility.UrlEncode(value);
                }
                else
                {
                    value = value.Replace(@"\", @"\\");
                }

                vars[prop.Name.ToLower()] = value;
            }

            // ChallengeType can be multiple values, use the first one present

            vars["challengetype"] = config.Challenges.FirstOrDefault()?.ChallengeType ?? vars["challengetype"];

            // add special processing for these values
            vars["success"] = forSuccess ? "true" : "false";
            vars["subjectalternativenames"] = string.Join(",", config.SubjectAlternativeNames ?? new string[] { config.PrimaryDomain });

            // process the template and replace values
            return(Regex.Replace(template, @"\$(\w+)(?=[\W$])", m =>
            {
                // replace var if it can be found, otherwise don't
                var key = m.Groups[1].Value.ToLower();
                return vars.ContainsKey(key) ? vars[key] : "$" + key;
            },
                                 RegexOptions.IgnoreCase));
        }
Example #8
0
        /// <summary>
        /// For the given set of options get a new CertRequestConfig to store
        /// </summary>
        /// <returns></returns>
        private ManagedSite GetUpdatedManagedSiteSettings()
        {
            var item = SelectedItem;
            CertRequestConfig config = new CertRequestConfig();

            // RefreshDomainOptionSettingsFromUI();
            var primaryDomain = item.DomainOptions.FirstOrDefault(d => d.IsPrimaryDomain == true);

            //if no primary domain need to go back and select one
            if (primaryDomain == null)
            {
                throw new ArgumentException("Primary subject domain must be set.");
            }

            var _idnMapping = new System.Globalization.IdnMapping();

            config.PrimaryDomain = _idnMapping.GetAscii(primaryDomain.Domain); // ACME service requires international domain names in ascii mode

            //apply remaining selected domains as subject alternative names
            config.SubjectAlternativeNames =
                item.DomainOptions.Where(dm => dm.Domain != primaryDomain.Domain && dm.IsSelected == true)
                .Select(i => i.Domain)
                .ToArray();

            //config.PerformChallengeFileCopy = true;
            //config.PerformExtensionlessConfigChecks = !chkSkipConfigCheck.Checked;
            config.PerformAutoConfig = true;

            // config.EnableFailureNotifications = chkEnableNotifications.Checked;

            //determine if this site has an existing entry in Managed Sites, if so use that, otherwise start a new one

            if (SelectedItem.Id == null)
            {
                var siteInfo = SelectedWebSite;
                //if siteInfo null we need to go back and select a site

                item.Id      = Guid.NewGuid().ToString() + ":" + siteInfo.SiteId;
                item.GroupId = siteInfo.SiteId;

                config.WebsiteRootPath = Environment.ExpandEnvironmentVariables(siteInfo.PhysicalPath);
            }

            item.ItemType = ManagedItemType.SSL_LetsEncrypt_LocalIIS;

            //store domain options settings and request config for this site so we can replay for automated renewal
            // managedSite.DomainOptions = this.domains;
            //managedSite.RequestConfig = config;

            return(item);
        }
Example #9
0
        private bool PerformCertRequestAndIISBinding(string certDomain, string[] alternativeNames)
        {
            // ACME service requires international domain names in ascii mode
            certDomain = _idnMapping.GetAscii(certDomain);

            //create cert and binding it

            //Typical command sequence for a new certificate

            //Initialize-ACMEVault -BaseURI https://acme-staging.api.letsencrypt.org/

            // Get-Module -ListAvailable ACMESharp
            // New-ACMEIdentifier -Dns test7.examplesite.co.uk -Alias test7_examplesite_co_uk636213616564101276 -Label Identifier:test7.examplesite.co.uk
            // Complete-ACMEChallenge -Ref test7_examplesite_co_uk636213616564101276 -ChallengeType http-01 -Handler manual  -Regenerate
            // Submit-ACMEChallenge -Ref test7_examplesite_co_uk636213616564101276 -Challenge http-01
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // New-ACMECertificate -Identifier test7_examplesite_co_uk636213616564101276 -Alias cert_test7_examplesite_co_uk636213616564101276 -Generate
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // Get-ACMECertificate -Ref = ac22dbfe - b75f - 4cac-9247-b40c1d9bf9eb -ExportPkcs12 C:\ProgramData\ACMESharp\sysVault\99-ASSET\ac22dbfe-b75f-4cac-9247-b40c1d9bf9eb-all.pfx -Overwrite

            //get info on existing IIS site we want to create/update SSL binding for
            IISManager iisManager = new IISManager();
            var        iisSite    = iisManager.GetSiteBindingByDomain(certDomain);
            var        certConfig = new CertRequestConfig()
            {
                PrimaryDomain            = certDomain,
                PerformChallengeFileCopy = true,
                WebsiteRootPath          = Environment.ExpandEnvironmentVariables(iisSite.PhysicalPath)
            };

            var certifyManager = new VaultManager(Properties.Settings.Default.VaultPath, LocalDiskVault.VAULT);

            certifyManager.UsePowershell = false;

            //init vault if not already created
            certifyManager.InitVault(staging: true);

            //domain alias is used as an ID in both the vault and the LE server, it's specific to one authorization attempt and cannot be reused for renewal
            var domainIdentifierAlias = certifyManager.ComputeIdentifierAlias(certDomain);

            //NOTE: to support a SAN certificate (multiple alternative domains on one site) the domain validation steps need to be repeat for each name:

            //register identifier with LE, get http challenge spec back
            //create challenge response answer file under site .well-known, auto configure web.config for extenstionless content, mark challenge prep completed
            var authState = certifyManager.BeginRegistrationAndValidation(certConfig, domainIdentifierAlias);

            //ask LE to check our answer to their authorization challenge (http), LE will then attempt to fetch our answer, if all accessible and correct (authorized) LE will then allow us to request a certificate
            if (authState.Identifier.Authorization.IsPending())
            {
                //prepare IIS with answer for the LE challenege
                certifyManager.PerformIISAutomatedChallengeResponse(certConfig, authState);

                //ask LE to validate our challenge response
                certifyManager.SubmitChallenge(domainIdentifierAlias, "http-01");
            }

            //now check if LE has validated our challenge answer
            bool validated = certifyManager.CompleteIdentifierValidationProcess(domainIdentifierAlias);

            if (validated)
            {
                var certRequestResult = certifyManager.PerformCertificateRequestProcess(domainIdentifierAlias, alternativeIdentifierRefs: null);
                if (certRequestResult.IsSuccess)
                {
                    string pfxPath = certRequestResult.Result.ToString();
                    //Install certificate into certificate store and bind to IIS site
                    //TODO, match by site id?
                    if (iisManager.InstallCertForDomain(certDomain, pfxPath, cleanupCertStore: true, skipBindings: false))
                    {
                        //all done
                        System.Diagnostics.Debug.WriteLine("Certificate installed and SSL bindings updated for " + certDomain);
                        return(true);
                    }
                    else
                    {
                        System.Diagnostics.Debug.WriteLine("Failed to install PFX file for Certificate.");
                        return(false);
                    }
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine("LE did not issue a valid certificate in the time allowed.");
                    return(false);
                }
            }
            else
            {
                System.Diagnostics.Debug.WriteLine("Validation of the required challenges did not complete successfully.");
                return(false);
            }
        }
Example #10
0
 public Task <ProcessStepResult> CompleteCertificateRequest(ILog log, CertRequestConfig config, string orderId)
 {
     throw new System.NotImplementedException();
 }
Example #11
0
 public Task <PendingOrder> BeginCertificateOrder(ILog log, CertRequestConfig config, string orderUri = null)
 {
     throw new System.NotImplementedException();
 }
Example #12
0
 public PendingAuthorization PerformIISAutomatedChallengeResponse(CertRequestConfig requestConfig, PendingAuthorization pendingAuth)
 {
     return(_vaultManager.PerformIISAutomatedChallengeResponse(requestConfig, pendingAuth));
 }
Example #13
0
        public async Task <PendingAuthorization> BeginRegistrationAndValidation(CertRequestConfig config, string domainIdentifierId, string challengeType, string domain)
        {
            //if no alternative domain specified, use the primary domain as the subject
            if (domain == null)
            {
                domain = config.PrimaryDomain;
            }

            try
            {
                List <String> domainOrders = new List <string>();
                domainOrders.Add(domain);

                var order = await _acme.NewOrder(domainOrders);

                // track order in memory
                if (_currentOrders.Keys.Contains(domain))
                {
                    _currentOrders.Remove(domain);
                }
                _currentOrders.Add(domain, order);

                var orderAuthorizations = await order.Authorizations();

                IAuthorizationContext authz = (await order.Authorizations()).First();

                var allChallenges = await authz.Challenges();

                List <AuthorizationChallengeItem> challenges = new List <AuthorizationChallengeItem>();

                // add http challenge (if any)
                var httpChallenge = await authz.Http();

                if (httpChallenge != null)
                {
                    var httpChallengeStatus = await httpChallenge.Resource();

                    if (httpChallengeStatus.Status == ChallengeStatus.Invalid)
                    {
                        // we need to start a new http challenge

                        //retry order
                        //return await BeginRegistrationAndValidation(config, domainIdentifierId, challengeType, domain);
                    }

                    challenges.Add(new AuthorizationChallengeItem
                    {
                        ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP,
                        Key           = httpChallenge.Token,
                        Value         = httpChallenge.KeyAuthz,
                        ChallengeData = httpChallenge,
                        ResourceUri   = $"http://{domain}/.well-known/acme-challenge/{httpChallenge.Token}",
                        ResourcePath  = $".well-known\\acme-challenge\\{httpChallenge.Token}",
                        IsValidated   = (httpChallengeStatus.Status == ChallengeStatus.Valid)
                    });
                }

                // add dns challenge (if any)
                var dnsChallenge = await authz.Dns();

                if (dnsChallenge != null)
                {
                    var dnsChallengeStatus = await dnsChallenge.Resource();

                    if (dnsChallengeStatus.Status == ChallengeStatus.Invalid)
                    {
                        // we need to start a new http challenge
                        //await authz.Deactivate();

                        //retry order
                        //return await BeginRegistrationAndValidation(config, domainIdentifierId, challengeType, domain);
                    }

                    var dnsValue = ComputeDnsValue(dnsChallenge, _acme.AccountKey);
                    var dnsKey   = $"_acme-challenge.{domain}".Replace("*.", "");

                    challenges.Add(new AuthorizationChallengeItem
                    {
                        ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS,
                        Key           = dnsKey,
                        Value         = dnsValue,
                        ChallengeData = dnsChallenge,
                        IsValidated   = (dnsChallengeStatus.Status == ChallengeStatus.Valid)
                    });
                }

                // add tls-sni-01 challenge (if any)

                /* var tls01Challenge = await authz.Challenges(;
                 * if (dnsChallenge != null)
                 * {
                 *   var dnsChallengeStatus = await dnsChallenge.Resource();
                 *
                 *   challenges.Add(new AuthorizationChallengeItem
                 *   {
                 *       ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS,
                 *       Key = dnsChallenge.Token,
                 *       Value = dnsChallenge.KeyAuthz,
                 *       ChallengeData = dnsChallenge,
                 *       IsValidated = (dnsChallengeStatus.Status == ChallengeStatus.Valid)
                 *   });
                 * }*/

                // report back on the challenges we now may need to attempt
                return(new PendingAuthorization
                {
                    Challenges = challenges,
                    Identifier = new IdentifierItem
                    {
                        Dns = domain,
                        IsAuthorizationPending = !challenges.Any(c => c.IsValidated) //auth is pending if we have no challenges already validated
                    },
                    AuthorizationContext = authz,
                    IsValidated = challenges.Any(c => c.IsValidated)
                });
            }
            catch (Exception exp)
            {
                // failed to register the domain identifier with LE (invalid, rate limit or CAA fail?)
                LogAction("NewOrder [" + domain + "]", exp.Message);

                return(new PendingAuthorization
                {
                    AuthorizationError = exp.Message
                });
            }
        }
Example #14
0
        private bool PerformCertRequestAndIISBinding(string certDomain)
        {
            // ACME service requires international domain names in ascii mode
            certDomain = _idnMapping.GetAscii(certDomain);

            //create cert and binding it

            //Typical command sequence for a new certificate

            //Initialize-ACMEVault -BaseURI https://acme-staging.api.letsencrypt.org/

            // Get-Module -ListAvailable ACMESharp
            // New-ACMEIdentifier -Dns test7.examplesite.co.uk -Alias test7_examplesite_co_uk636213616564101276 -Label Identifier:test7.examplesite.co.uk
            // Complete-ACMEChallenge -Ref test7_examplesite_co_uk636213616564101276 -ChallengeType http-01 -Handler manual  -Regenerate
            // Submit-ACMEChallenge -Ref test7_examplesite_co_uk636213616564101276 -Challenge http-01
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // New-ACMECertificate -Identifier test7_examplesite_co_uk636213616564101276 -Alias cert_test7_examplesite_co_uk636213616564101276 -Generate
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // Update-ACMEIdentifier -Ref test7_examplesite_co_uk636213616564101276
            // Get-ACMECertificate -Ref = ac22dbfe - b75f - 4cac-9247-b40c1d9bf9eb -ExportPkcs12 C:\ProgramData\ACMESharp\sysVault\99-ASSET\ac22dbfe-b75f-4cac-9247-b40c1d9bf9eb-all.pfx -Overwrite

            var vaultManager = new VaultManager(Properties.Settings.Default.VaultPath, LocalDiskVault.VAULT);

            //init vault if not already created
            vaultManager.InitVault(staging: true);

            var certifyManager = vaultManager.PowershellManager;

            //domain alias is used as an ID in both the vault and the LE server, it's specific to one authorization attempt and cannot be reused for renewal

            var domainIdentifierAlias = vaultManager.ComputeIdentifierAlias(certDomain);
            //get info on existing IIS site we want to create/update SSL binding for
            IISManager iisManager = new IISManager();
            var        iisSite    = iisManager.GetSiteBindingByDomain(certDomain);
            var        certConfig = new CertRequestConfig()
            {
                Domain = certDomain,
                PerformChallengeFileCopy = true,
                WebsiteRootPath          = Environment.ExpandEnvironmentVariables(iisSite.PhysicalPath)
            };

            //NOTE: to support a SAN certificate (multiple alternative domains on one site) the domain validation steps need to be repeat for each name:

            //register identifier with LE, get http challenge spec back
            //create challenge response answer file under site .well-known, auto configure web.config for extenstionless content, mark challenge prep completed
            var authState = vaultManager.BeginRegistrationAndValidation(certConfig, domainIdentifierAlias);

            //ask LE to check our answer to their authorization challenge (http), LE will then attempt to fetch our answer, if all accessible and correct (authorized) LE will then allow us to request a certificate
            certifyManager.SubmitChallenge(domainIdentifierAlias, "http-01");

            //
            certifyManager.UpdateIdentifier(domainIdentifierAlias);
            var identiferStatus = vaultManager.GetIdentifier(domainIdentifierAlias, true);
            var attempts        = 0;
            var maxAttempts     = 3;

            while (identiferStatus.Authorization.Status == "pending" && attempts < maxAttempts)
            {
                System.Threading.Thread.Sleep(2000); //wait a couple of seconds before checking again
                certifyManager.UpdateIdentifier(domainIdentifierAlias);
                identiferStatus = vaultManager.GetIdentifier(domainIdentifierAlias, true);
                attempts++;
            }

            if (identiferStatus.Authorization.Status != "valid")
            {
                //still pending or failed
                System.Diagnostics.Debug.WriteLine("LE Authorization problem: " + identiferStatus.Authorization.Status);
                return(false);
            }
            else
            {
                //all good, we can request a certificate
                //if authorizing a SAN we would need to repeat the above until all domains are valid, then we can request cert
                var certAlias = "cert_" + domainIdentifierAlias;

                //register cert placeholder in vault
                certifyManager.NewCertificate(domainIdentifierAlias, certAlias, subjectAlternativeNameIdentifiers: null);

                //ask LE to issue a certificate for our domain(s)
                certifyManager.SubmitCertificate(certAlias);

                //LE may now have issued a certificate, this process may not be immediate
                var certDetails = vaultManager.GetCertificate(certAlias, reloadVaultConfig: true);
                attempts = 0;
                //cert not issued yet, wait and try again
                while ((certDetails == null || String.IsNullOrEmpty(certDetails.IssuerSerialNumber)) && attempts < maxAttempts)
                {
                    System.Threading.Thread.Sleep(2000); //wait a couple of seconds before checking again
                    certifyManager.UpdateCertificate(certAlias);
                    certDetails = vaultManager.GetCertificate(certAlias, reloadVaultConfig: true);
                    attempts++;
                }

                if (certDetails != null && !String.IsNullOrEmpty(certDetails.IssuerSerialNumber))
                {
                    //we have an issued certificate, we can go ahead and install it as required
                    System.Diagnostics.Debug.WriteLine("Received certificate issued by LE." + JsonConvert.SerializeObject(certDetails));

                    //if using cert in IIS, we need to export the certificate PFX file, install it as a certificate and setup the site binding to map to this cert
                    string certFolderPath = vaultManager.GetCertificateFilePath(certDetails.Id, LocalDiskVault.ASSET);
                    string pfxFile        = certAlias + "-all.pfx";
                    string pfxPath        = System.IO.Path.Combine(certFolderPath, pfxFile);

                    //create folder to export PFX to, if required
                    if (!System.IO.Directory.Exists(certFolderPath))
                    {
                        System.IO.Directory.CreateDirectory(certFolderPath);
                    }

                    //if file already exists we want to delet the old one
                    if (System.IO.File.Exists(pfxPath))
                    {
                        //delete existing PFX (if any)
                        System.IO.File.Delete(pfxPath);
                    }

                    //export the PFX file
                    vaultManager.ExportCertificate(certAlias, pfxOnly: true);

                    if (!System.IO.File.Exists(pfxPath))
                    {
                        System.Diagnostics.Debug.WriteLine("Failed to export PFX. " + pfxPath);
                        return(false);
                    }

                    //Install certificate into certificate store and bind to IIS site
                    //TODO, match by site id?
                    if (iisManager.InstallCertForDomain(certDomain, pfxPath, cleanupCertStore: true, skipBindings: false))
                    {
                        //all done
                        System.Diagnostics.Debug.WriteLine("Certificate installed and SSL bindings updated for " + certDomain);
                        return(true);
                    }
                    else
                    {
                        System.Diagnostics.Debug.WriteLine("Failed to install PFX file for Certificate.");
                        return(false);
                    }
                }
                else
                {
                    System.Diagnostics.Debug.WriteLine("LE did not issue a valid certificate in the time allowed." + JsonConvert.SerializeObject(certDetails));
                    return(false);
                }
            }
        }
Example #15
0
        /// <summary>
        /// Once validation has completed for our requested domains we can complete the certificate
        /// request by submitting a Certificate Signing Request (CSR) to the CA
        /// </summary>
        /// <param name="log">  </param>
        /// <param name="primaryDnsIdentifier">  </param>
        /// <param name="alternativeDnsIdentifiers">  </param>
        /// <param name="config">  </param>
        /// <returns>  </returns>
        public async Task <ProcessStepResult> CompleteCertificateRequest(ILog log, CertRequestConfig config, string orderId)
        {
            var orderContext = _currentOrders[orderId];

            // generate temp keypair for signing CSR
            var keyAlg = KeyAlgorithm.RS256;

            if (!string.IsNullOrEmpty(config.CSRKeyAlg))
            {
                if (config.CSRKeyAlg == "RS256")
                {
                    keyAlg = KeyAlgorithm.RS256;
                }
                if (config.CSRKeyAlg == "ECDSA256")
                {
                    keyAlg = KeyAlgorithm.ES256;
                }
                if (config.CSRKeyAlg == "ECDSA384")
                {
                    keyAlg = KeyAlgorithm.ES384;
                }
            }

            var csrKey = KeyFactory.NewKey(keyAlg);

            var certFriendlyName = $"{config.PrimaryDomain} [Certify] ";

            // generate cert
            CertificateChain certificateChain = null;

            try
            {
                certificateChain = await orderContext.Generate(new CsrInfo
                {
                    CommonName = _idnMapping.GetAscii(config.PrimaryDomain)
                }, csrKey);

                X509Certificate2 cert = new X509Certificate2(certificateChain.Certificate.ToDer());
                certFriendlyName += $"{cert.GetEffectiveDateString()} to {cert.GetExpirationDateString()}";
            }
            catch (AcmeRequestException exp)
            {
                var msg = $"Failed to finalize certificate order:  {exp.Error?.Detail}";
                log.Error(msg);

                return(new ProcessStepResult {
                    ErrorMessage = msg, IsSuccess = false, Result = exp.Error
                });
            }

            var certFolderPath = _settingsFolder + "\\assets\\pfx";

            if (!System.IO.Directory.Exists(certFolderPath))
            {
                System.IO.Directory.CreateDirectory(certFolderPath);
            }

            string certFile = Guid.NewGuid().ToString() + ".pfx";
            string pfxPath  = certFolderPath + "\\" + certFile;

            var pfx      = certificateChain.ToPfx(csrKey);
            var pfxBytes = pfx.Build(certFriendlyName, "");

            System.IO.File.WriteAllBytes(pfxPath, pfxBytes);

            return(new ProcessStepResult {
                IsSuccess = true, Result = pfxPath
            });
        }
Example #16
0
 public PendingAuthorization BeginRegistrationAndValidation(CertRequestConfig config, string domainIdentifierId, string challengeType, string domain)
 {
     throw new NotImplementedException();
 }
Example #17
0
        public async Task <ProcessStepResult> PerformCertificateRequestProcess(string primaryDnsIdentifier, string[] alternativeDnsIdentifiers, CertRequestConfig config)
        {
            // create our new certificate
            var orderContext = _currentOrders[config.PrimaryDomain];

            //update order status
            var order = await orderContext.Resource();

            // order.Generate()
            var csrKey = KeyFactory.NewKey(KeyAlgorithm.RS256);
            var csr    = new CsrInfo
            {
                CommonName = config.PrimaryDomain
            };

            //alternative to certes IOrderContextExtension.Finalize
            var builder = new CertificationRequestBuilder(csrKey);

            foreach (var authzCtx in await orderContext.Authorizations())
            {
                var authz = await authzCtx.Resource();

                if (!builder.SubjectAlternativeNames.Contains(authz.Identifier.Value))
                {
                    if (config.PrimaryDomain != $"*.{authz.Identifier.Value}")
                    {
                        //only add domain to SAN if it is not derived from a wildcard domain eg test.com from *.test.com
                        builder.SubjectAlternativeNames.Add(authz.Identifier.Value);
                    }
                }
            }

            // if main request is for a wildcard domain, add that to SAN list
            if (config.PrimaryDomain.StartsWith("*."))
            {
                //add wildcard domain to san
                builder.SubjectAlternativeNames.Add(config.PrimaryDomain);
            }
            builder.AddName("CN", config.PrimaryDomain);

            /* foreach (var f in csr.AllFieldsDictionary)
             * {
             *   builder.AddName(f.Key, f.Value);
             * }*/

            if (string.IsNullOrWhiteSpace(csr.CommonName))
            {
                builder.AddName("CN", builder.SubjectAlternativeNames[0]);
            }

            var certResult = await orderContext.Finalize(builder.Generate());

            var pem = await orderContext.Download();

            var cert = new CertificateInfo(pem, csrKey);

            var certFriendlyName = config.PrimaryDomain + "[Certify]";
            var certFolderPath   = _settingsFolder + "\\assets\\pfx";

            if (!System.IO.Directory.Exists(certFolderPath))
            {
                System.IO.Directory.CreateDirectory(certFolderPath);
            }

            string certFile = Guid.NewGuid().ToString() + ".pfx";
            string pfxPath  = certFolderPath + "\\" + certFile;

            System.IO.File.WriteAllBytes(pfxPath, cert.ToPfx(certFriendlyName, ""));

            return(new ProcessStepResult {
                IsSuccess = true, Result = pfxPath
            });
        }
Example #18
0
 public Task <ProcessStepResult> PerformCertificateRequestProcess(string primaryDnsIdentifier, string[] alternativeDnsIdentifiers, CertRequestConfig config)
 {
     return(Task.FromResult(_vaultManager.PerformCertificateRequestProcess(primaryDnsIdentifier, alternativeDnsIdentifiers)));
 }
Example #19
0
 public async Task <PendingAuthorization> BeginRegistrationAndValidation(CertRequestConfig config, string domainIdentifierId, string challengeType, string domain)
 {
     return(await Task.FromResult(_vaultManager.BeginRegistrationAndValidation(config, domainIdentifierId, challengeType, domain)));
 }
        private async Task <List <ActionStep> > DeployToAllTargetBindings(IBindingDeploymentTarget deploymentTarget,
                                                                          ManagedCertificate managedCertificate,
                                                                          CertRequestConfig requestConfig,
                                                                          string certStoreName,
                                                                          byte[] certHash,
                                                                          List <string> dnsHosts,
                                                                          bool isPreviewOnly = false
                                                                          )
        {
            var actions     = new List <ActionStep>();
            var targetSites = new List <IBindingDeploymentTargetItem>();

            // ensure defaults applied for deployment mode
            requestConfig.ApplyDeploymentOptionDefaults();

            // if single site, add that
            if (requestConfig.DeploymentSiteOption == DeploymentOption.SingleSite)
            {
                if (!string.IsNullOrEmpty(managedCertificate.ServerSiteId))
                {
                    var site = await deploymentTarget.GetTargetItem(managedCertificate.ServerSiteId);

                    if (site != null)
                    {
                        targetSites.Add(site);
                    }
                }
            }

            // or add all sites (if required)
            if (requestConfig.DeploymentSiteOption == DeploymentOption.AllSites || requestConfig.DeploymentSiteOption == DeploymentOption.Auto)
            {
                targetSites.AddRange(await deploymentTarget.GetAllTargetItems());
            }

            // for each sites we want to target, identify bindings to add/update as required
            foreach (var site in targetSites)
            {
                try
                {
                    var existingBindings = await deploymentTarget.GetBindings(site.Id);

                    var existingHttps = existingBindings.Where(e => e.Protocol == "https").ToList();

                    //remove https bindings which already have an https equivalent (specific hostname or blank)
                    existingBindings.RemoveAll(b => existingHttps.Any(e => e.Host == b.Host) && b.Protocol == "http");

                    existingBindings = existingBindings.OrderBy(b => b.Protocol).ThenBy(b => b.Host).ToList();

                    // for each binding create or update an https binding
                    foreach (var b in existingBindings)
                    {
                        var updateBinding = false;

                        //if binding is http and there is no https binding, create one
                        var hostname = b.Host;

                        // install the cert for this binding if the hostname matches, or we have a
                        // matching wildcard, or if there is no hostname specified in the binding

                        if (requestConfig.DeploymentBindingReplacePrevious || requestConfig.DeploymentSiteOption == DeploymentOption.Auto)
                        {
                            // if replacing previous, check if current binding cert hash matches
                            // previous cert hash
                            if (b.CertificateHash != null && (managedCertificate.CertificatePreviousThumbprintHash != null || managedCertificate.CertificateThumbprintHash != null))
                            {
                                if (string.Equals(b.CertificateHash, managedCertificate.CertificatePreviousThumbprintHash))
                                {
                                    updateBinding = true;
                                }
                                else if (string.Equals(b.CertificateHash, managedCertificate.CertificateThumbprintHash))
                                {
                                    updateBinding = true;
                                }
                            }
                        }

                        if (updateBinding == false)
                        {
                            // TODO: add wildcard match
                            if (string.IsNullOrEmpty(hostname) && requestConfig.DeploymentBindingBlankHostname)
                            {
                                updateBinding = true;
                            }
                            else
                            {
                                if (requestConfig.DeploymentBindingMatchHostname)
                                {
                                    updateBinding = ManagedCertificate.IsDomainOrWildcardMatch(dnsHosts, hostname);
                                }
                            }
                        }

                        if (requestConfig.DeploymentBindingOption == DeploymentBindingOption.UpdateOnly)
                        {
                            // update existing bindings only, so only update if this is already an
                            // https binding
                            if (b.Protocol != "https")
                            {
                                updateBinding = false;
                            }
                        }

                        if (b.Protocol != "http" && b.Protocol != "https")
                        {
                            // skip bindings for other service types
                            updateBinding = false;
                        }

                        if (updateBinding)
                        {
                            //SSL port defaults to 443 or the config default, unless we already have an https binding, in which case re-use same port
                            var sslPort         = 443;
                            var targetIPAddress = "*";

                            if (!string.IsNullOrWhiteSpace(requestConfig.BindingPort))
                            {
                                sslPort = int.Parse(requestConfig.BindingPort);
                            }

                            if (b.Protocol == "https")
                            {
                                if (b.Port > 0)
                                {
                                    sslPort = b.Port;
                                }
                                if (!unassignedIPs.Contains(b.IP))
                                {
                                    targetIPAddress = b.IP;
                                }
                            }
                            else
                            {
                                if (!unassignedIPs.Contains(requestConfig.BindingIPAddress))
                                {
                                    targetIPAddress = requestConfig.BindingIPAddress;
                                }
                            }

                            //create/update binding and associate new cert

                            //if any binding elements configured, use those, otherwise auto bind using defaults and SNI

                            var stepActions = await UpdateBinding(
                                deploymentTarget,
                                site,
                                existingBindings,
                                certStoreName,
                                certHash,
                                hostname,
                                sslPort : sslPort,
                                useSNI : (requestConfig.BindingUseSNI != null ? (bool)requestConfig.BindingUseSNI : true),
                                ipAddress : targetIPAddress,
                                alwaysRecreateBindings : requestConfig.AlwaysRecreateBindings,
                                isPreviewOnly : isPreviewOnly
                                );

                            actions.AddRange(stepActions);
                        }
                    }
                    //
                }
                catch (Exception exp)
                {
                    actions.Add(new ActionStep {
                        Title = site.Name, Category = "Deploy.AddOrUpdateBindings", HasError = true, Description = exp.ToString()
                    });
                }
            }

            return(actions);
        }
Example #21
0
 public PendingAuthorization BeginRegistrationAndValidation(CertRequestConfig config, string domainIdentifierId, string challengeType, string domain)
 {
     return(_vaultManager.BeginRegistrationAndValidation(config, domainIdentifierId, challengeType, domain));
 }
Example #22
0
        /// <summary>
        /// Begin order for new certificate for one or more domains, fetching the required challenges
        /// to complete
        /// </summary>
        /// <param name="log">  </param>
        /// <param name="config">  </param>
        /// <param name="orderUri"> Uri of existing order to resume </param>
        /// <returns>  </returns>
        public async Task <PendingOrder> BeginCertificateOrder(ILog log, CertRequestConfig config, string orderUri = null)
        {
            if (DateTime.Now.Subtract(lastInitDateTime).TotalMinutes > 30)
            {
                // our acme context nonce may have expired (which returns "JWS has an invalid
                // anti-replay nonce") so start a new one
                InitProvider();
            }

            var pendingOrder = new PendingOrder {
                IsPendingAuthorizations = true
            };

            // prepare a list of all pending authorization we need to complete, or those we have
            // already satisfied
            var authzList = new List <PendingAuthorization>();

            //if no alternative domain specified, use the primary domain as the subject
            var domainOrders = new List <string>();

            // order all of the distinct domains in the config (primary + SAN).
            domainOrders.Add(_idnMapping.GetAscii(config.PrimaryDomain));

            if (config.SubjectAlternativeNames != null)
            {
                foreach (var s in config.SubjectAlternativeNames)
                {
                    if (!domainOrders.Contains(s))
                    {
                        domainOrders.Add(_idnMapping.GetAscii(s));
                    }
                }
            }

            try
            {
                IOrderContext order;

                if (orderUri != null)
                {
                    order = _acme.Order(new Uri(orderUri));
                }
                else
                {
                    order = await _acme.NewOrder(domainOrders);
                }

                if (order == null)
                {
                    throw new Exception("Could not create certificate order.");
                }

                orderUri = order.Location.ToString();

                pendingOrder.OrderUri = orderUri;

                log.Information($"Created ACME Order: {orderUri}");

                // track order in memory, keyed on order Uri
                if (_currentOrders.Keys.Contains(orderUri))
                {
                    _currentOrders.Remove(orderUri);
                }

                _currentOrders.Add(orderUri, order);

                // handle order status 'Ready' if all ahtorizations are already valid
                var orderDetails = await order.Resource();

                if (orderDetails.Status == OrderStatus.Ready)
                {
                    pendingOrder.IsPendingAuthorizations = false;
                }

                // get all required pending (or already valid) authorizations for this order

                log.Verbose($"Fetching Authorizations.");

                var orderAuthorizations = await order.Authorizations();

                // get the challenges for each authorization
                foreach (IAuthorizationContext authz in orderAuthorizations)
                {
                    log.Verbose($"Fetching Authz Challenges.");

                    var allChallenges = await authz.Challenges();

                    var res = await authz.Resource();

                    string authzDomain = res.Identifier.Value;
                    if (res.Wildcard == true)
                    {
                        authzDomain = "*." + authzDomain;
                    }

                    var challenges = new List <AuthorizationChallengeItem>();

                    // add http challenge (if any)
                    var httpChallenge = await authz.Http();

                    if (httpChallenge != null)
                    {
                        var httpChallengeStatus = await httpChallenge.Resource();

                        log.Information($"Got http-01 challenge {httpChallengeStatus.Url}");

                        if (httpChallengeStatus.Status == ChallengeStatus.Invalid)
                        {
                            log.Error($"HTTP challenge has an invalid status");
                        }

                        challenges.Add(new AuthorizationChallengeItem
                        {
                            ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP,
                            Key           = httpChallenge.Token,
                            Value         = httpChallenge.KeyAuthz,
                            ChallengeData = httpChallenge,
                            ResourceUri   = $"http://{authzDomain.Replace("*.", "")}/.well-known/acme-challenge/{httpChallenge.Token}",
                            ResourcePath  = $".well-known\\acme-challenge\\{httpChallenge.Token}",
                            IsValidated   = (httpChallengeStatus.Status == ChallengeStatus.Valid)
                        });
                    }

                    // add dns challenge (if any)
                    var dnsChallenge = await authz.Dns();

                    if (dnsChallenge != null)
                    {
                        var dnsChallengeStatus = await dnsChallenge.Resource();

                        log.Information($"Got dns-01 challenge {dnsChallengeStatus.Url}");

                        if (dnsChallengeStatus.Status == ChallengeStatus.Invalid)
                        {
                            log.Error($"DNS challenge has an invalid status");
                        }

                        var dnsValue = _acme.AccountKey.DnsTxt(dnsChallenge.Token); //ComputeDnsValue(dnsChallenge, _acme.AccountKey);
                        var dnsKey   = $"_acme-challenge.{authzDomain}".Replace("*.", "");

                        challenges.Add(new AuthorizationChallengeItem
                        {
                            ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS,
                            Key           = dnsKey,
                            Value         = dnsValue,
                            ChallengeData = dnsChallenge,
                            IsValidated   = (dnsChallengeStatus.Status == ChallengeStatus.Valid)
                        });
                    }

                    // add tls-sni-01 challenge (if any)

                    /* var tls01Challenge = await authz.Challenges(;
                     * if (dnsChallenge != null)
                     * {
                     *   var dnsChallengeStatus = await dnsChallenge.Resource();
                     *
                     *   challenges.Add(new AuthorizationChallengeItem
                     *   {
                     *       ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS,
                     *       Key = dnsChallenge.Token,
                     *       Value = dnsChallenge.KeyAuthz,
                     *       ChallengeData = dnsChallenge,
                     *       IsValidated = (dnsChallengeStatus.Status == ChallengeStatus.Valid)
                     *   });
                     * }*/

                    // report back on the challenges we now may need to attempt
                    authzList.Add(
                        new PendingAuthorization
                    {
                        Challenges = challenges,
                        Identifier = new IdentifierItem
                        {
                            Dns = authzDomain,
                            IsAuthorizationPending = !challenges.Any(c => c.IsValidated)  //auth is pending if we have no challenges already validated
                        },
                        AuthorizationContext = authz,
                        IsValidated          = challenges.Any(c => c.IsValidated),
                        OrderUri             = orderUri
                    });
                }

                pendingOrder.Authorizations = authzList;

                return(pendingOrder);
            }
            catch (AcmeRequestException exp)
            {
                // failed to register one or more domain identifier with LE (invalid, rate limit or
                // CAA fail?)

                var msg = $"Failed to begin certificate order: {exp.Error?.Detail}";

                log.Error(msg);

                pendingOrder.Authorizations =
                    new List <PendingAuthorization> {
                    new PendingAuthorization
                    {
                        AuthorizationError = msg,
                        IsFailure          = true
                    }
                };

                return(pendingOrder);
            }
        }
Example #23
0
 public PendingAuthorization PerformIISAutomatedChallengeResponse(CertRequestConfig requestConfig, PendingAuthorization pendingAuth)
 {
     throw new NotImplementedException();
 }
        private void btnRequestCertificate_Click(object sender, EventArgs e)
        {
            if (lstSites.SelectedItem == null)
            {
                MessageBox.Show("No IIS Site Selected");
                return;
            }

            if (VaultManager == null)
            {
                MessageBox.Show("Vault Manager is null. Please report this problem.");
            }

            //prevent further clicks on request button
            btnRequestCertificate.Enabled = false;
            ShowProgressBar();
            this.Cursor = Cursors.WaitCursor;

            bool certsApproved = false;
            bool certsStored   = false;

            CertRequestConfig config = new CertRequestConfig();
            var selectItem           = (SiteListItem)lstSites.SelectedItem;

            config.Domain = selectItem.Host;
            config.PerformChallengeFileCopy = true;
            config.WebsiteRootPath          = Environment.ExpandEnvironmentVariables(selectItem.PhysicalPath);

            var vaultConfig = VaultManager.GetVaultConfig();

            //check if domain already has an associated identifier
            var identifierAlias = VaultManager.ComputeIdentifierAlias(config.Domain);

            //try alias or DNS name before creating a new identifier
            var identifier = VaultManager.GetIdentifier(identifierAlias);

            if (identifier == null)
            {
                identifier = VaultManager.GetIdentifier(config.Domain);
            }

            if (identifier != null)
            {
                //domain already exists in vault
                //check if has pending authorization challenges
                if (identifier.Authorization != null && identifier.Authorization.Challenges != null)
                {
                    var challenge = identifier.Authorization.Challenges.FirstOrDefault(c => c.Type == "http-01");
                    if (challenge != null)
                    {
                        if (challenge.Status != "invalid")
                        {
                            //update challenge status
                            MessageBox.Show("An existing challenge was already in progress, status will now be updated. " + challenge.Token);
                            VaultManager.UpdateIdentifierStatus(identifierAlias);

                            identifier = VaultManager.GetIdentifier(identifierAlias
                                                                    , true);

                            challenge = identifier.Authorization.Challenges.FirstOrDefault(c => c.Type == "http-01");
                            if (challenge.Status == "valid")
                            {
                                certsApproved = true;
                            }
                        }
                        else
                        {
                            MessageBox.Show("The existing challenge for this identifier failed. We will need to create a new one.");
                            identifierAlias += "_" + Guid.NewGuid().ToString().Substring(0, 6);
                        }
                    }
                }
            }

            if (!certsApproved)
            {
                var authorization = VaultManager.DomainInitAndRegistration(config, identifierAlias);

                if (authorization != null)
                {
                    if (!authorization.ExtensionlessConfigCheckedOK)
                    {
                        MessageBox.Show("Automated checks for extensionless content failed. Authorisations will not be able to complete. Change the web.config in <your site>\\.well-known\\acme-challenge and ensure you can browse to http://<your site>/.well-known/acme-challenge/configcheck before proceeding.");
                        return;
                    }
                    //at this point we can either get the user to manually copy the file to web site folder structure
                    //if file has already been copied we can go ahead and ask the server to verify it

                    //ask server to check our challenge answer is present and correct
                    VaultManager.SubmitChallenge(authorization.Identifier.Alias);

                    //give LE time to check our challenge answer stored on our server
                    Thread.Sleep(2000);

                    VaultManager.UpdateIdentifierStatus(authorization.Identifier.Alias);
                    VaultManager.ReloadVaultConfig();

                    //check status of the challenge
                    var updatedIdentifier = VaultManager.GetIdentifier(authorization.Identifier.Alias);

                    var challenge = updatedIdentifier.Authorization.Challenges.FirstOrDefault(c => c.Type == "http-01");

                    //if all OK, we will be ready to fetch our certificate
                    if (challenge?.Status == "valid")
                    {
                        certsApproved = true;
                    }
                    else
                    {
                        if (challenge != null)
                        {
                            MessageBox.Show("Challenge not yet completed. Check that http://" + config.Domain + "/" + challenge.ToString() + " path/file is present and accessible in your web browser.");
                        }
                        else
                        {
                            if (challenge.Status == "invalid")
                            {
                                MessageBox.Show("Challenge failed to complete. Check that http://" + config.Domain + "/" + challenge.ToString() + " path/file is present and accessible in your web browser. You may require extensionless file type mappings");
                            }
                        }
                    }
                }
                else
                {
                    MessageBox.Show("Could not begin authorization. Check Logs. Ensure the domain being authorized is whitelisted with LetsEncrypt service.");
                }
            }

            //create certs for current authorization
            string certRef = null;
            //if (certsApproved)
            {
                certRef = VaultManager.CreateCertificate(identifierAlias);
                VaultManager.UpdateIdentifierStatus(identifierAlias);
                identifier = VaultManager.GetIdentifier(identifierAlias, true);

                VaultManager.ReloadVaultConfig();
                if (VaultManager.CertExists(identifierAlias))
                {
                    certsStored = true;
                }
            }

            //auto setup/install
            var certInfo = VaultManager.GetCertificate(certRef);

            if (certInfo != null && certInfo.CrtDerFile == null)
            {
                //failed to get cert first time, try again
                certRef = VaultManager.CreateCertificate(identifierAlias);
                VaultManager.UpdateIdentifierStatus(identifierAlias);

                certInfo = VaultManager.GetCertificate(certRef);
            }

            //txtOutput.Text = "To complete this request copy the file " + CurrentAuthorization.TempFilePath + " to the following location under your website root (note: no file extension): " + CurrentAuthorization.Challenge.ChallengeAnswer.Key;
            //ReloadVault();

            this.Cursor = Cursors.Default;

            if (!certsStored)
            {
                if (certsApproved)
                {
                    MessageBox.Show("Certificates approved but not yet stored in vault. Try again later.");
                    CloseParentForm();
                    return;
                }
                else
                {
                    MessageBox.Show("Certificates not approved yet. Authorization challenge may have failed. Try again later.");
                    CloseParentForm();
                    return;
                }
            }
            else
            {
                if (certInfo != null)
                {
                    string certFolderPath = VaultManager.GetCertificateFilePath(certInfo.Id, LocalDiskVault.ASSET);
                    string pfxFile        = certInfo.Id.ToString() + "-all.pfx";
                    string pfxPath        = Path.Combine(certFolderPath, pfxFile);

                    if (!System.IO.Directory.Exists(certFolderPath))
                    {
                        System.IO.Directory.CreateDirectory(certFolderPath);
                    }
                    if (!File.Exists(pfxPath))
                    {
                        //hmm, no pfx, try to create pfx again TODO: shouldn't need this
                        VaultManager.ExportCertificate("=" + certInfo.Id.ToString(), pfxOnly: true);
                    }

                    if (File.Exists(pfxPath))
                    {
                        //VaultManager.UpdateIdentifierStatus(certInfo.IdentifierRef);
                        //identifier = VaultManager.GetIdentifier(certInfo.IdentifierRef, true);

                        IISManager iisManager = new IISManager();
                        if (identifier == null || identifier.Dns == null)
                        {
                            MessageBox.Show("Error: identifier/dns is null. Cannot match domain for binding");
                        }
                        else
                        {
                            if (iisManager.InstallCertForDomain(identifier.Dns, pfxPath, cleanupCertStore: true, skipBindings: !chkAutoBindings.Checked))
                            {
                                //all done
                                MessageBox.Show("Certificate installed and SSL bindings updated for " + identifier.Dns, Properties.Resources.AppName);
                                CloseParentForm();
                                return;
                            }

                            if (chkAutoBindings.Checked)
                            {
                                //auto store and create site bindings
                                MessageBox.Show("Your certificate has been imported and SSL bindings updated for " + config.Domain, Properties.Resources.AppName);
                                CloseParentForm();
                                return;
                            }
                            else
                            {
                                //auto store cert
                                MessageBox.Show("Your certificate has been imported and is ready for you to configure IIS bindings.", Properties.Resources.AppName);
                                CloseParentForm();
                                return;
                            }
                        }
                    }
                    else
                    {
                        MessageBox.Show("Failed to generate PFX file for Certificate.", Properties.Resources.AppName);
                        CloseParentForm();
                        return;
                    }
                }
                else
                {
                    //cert was null
                    MessageBox.Show("Certification was not successful. Certificate not valid or not yet authorized.", Properties.Resources.AppName);
                    CloseParentForm();
                    return;
                }
            }
        }
Example #25
0
        /// <summary>
        /// Once validation has completed for our requested domains we can complete the certificate
        /// request by submitting a Certificate Signing Request (CSR) to the CA
        /// </summary>
        /// <param name="log">  </param>
        /// <param name="primaryDnsIdentifier">  </param>
        /// <param name="alternativeDnsIdentifiers">  </param>
        /// <param name="config">  </param>
        /// <returns>  </returns>
        public async Task <ProcessStepResult> CompleteCertificateRequest(ILog log, CertRequestConfig config, string orderId)
        {
            var orderContext = _currentOrders[orderId];

            // check order status, if it's not 'ready' then try a few more times before giving up
            var order = await orderContext.Resource();

            var attempts = 5;

            while (attempts > 0 && order?.Status != OrderStatus.Ready)
            {
                await Task.Delay(2000);

                order = await orderContext.Resource();

                attempts--;
            }

            if (order?.Status != OrderStatus.Ready)
            {
                return(new ProcessStepResult {
                    IsSuccess = false, ErrorMessage = "Certificate Request did not complete. Order did not reach Ready status in the time allowed.", Result = order
                });
            }

            // generate temp keypair for signing CSR
            var keyAlg = KeyAlgorithm.RS256;

            if (!string.IsNullOrEmpty(config.CSRKeyAlg))
            {
                if (config.CSRKeyAlg == "RS256")
                {
                    keyAlg = KeyAlgorithm.RS256;
                }
                if (config.CSRKeyAlg == "ECDSA256")
                {
                    keyAlg = KeyAlgorithm.ES256;
                }
                if (config.CSRKeyAlg == "ECDSA384")
                {
                    keyAlg = KeyAlgorithm.ES384;
                }
                if (config.CSRKeyAlg == "ECDSA521")
                {
                    keyAlg = KeyAlgorithm.ES512;
                }
            }

            var csrKey = KeyFactory.NewKey(keyAlg);

            var certFriendlyName = $"{config.PrimaryDomain} [Certify] ";

            // generate cert
            CertificateChain certificateChain = null;

            try
            {
                certificateChain = await orderContext.Generate(new CsrInfo
                {
                    CommonName = _idnMapping.GetAscii(config.PrimaryDomain)
                }, csrKey);

                var cert = new X509Certificate2(certificateChain.Certificate.ToDer());
                certFriendlyName += $"{cert.GetEffectiveDateString()} to {cert.GetExpirationDateString()}";
            }
            catch (AcmeRequestException exp)
            {
                var msg = $"Failed to finalize certificate order:  {exp.Error?.Detail}";
                log.Error(msg);

                return(new ProcessStepResult {
                    ErrorMessage = msg, IsSuccess = false, Result = exp.Error
                });
            }

            var certFolderPath = _settingsFolder + "\\assets\\pfx";

            if (!System.IO.Directory.Exists(certFolderPath))
            {
                System.IO.Directory.CreateDirectory(certFolderPath);
            }

            string certFile = Guid.NewGuid().ToString() + ".pfx";
            string pfxPath  = certFolderPath + "\\" + certFile;

            var pfx      = certificateChain.ToPfx(csrKey);
            var pfxBytes = pfx.Build(certFriendlyName, "");

            System.IO.File.WriteAllBytes(pfxPath, pfxBytes);

            return(new ProcessStepResult {
                IsSuccess = true, Result = pfxPath
            });
        }
        /// <summary>
        /// Once validation has completed for our requested domains we can complete the certificate
        /// request by submitting a Certificate Signing Request (CSR) to the CA
        /// </summary>
        /// <param name="log">  </param>
        /// <param name="primaryDnsIdentifier">  </param>
        /// <param name="alternativeDnsIdentifiers">  </param>
        /// <param name="config">  </param>
        /// <returns>  </returns>
        public async Task <ProcessStepResult> CompleteCertificateRequest(ILog log, CertRequestConfig config, string orderId)
        {
            var orderContext = _currentOrders[orderId];

            // check order status, if it's not 'ready' then try a few more times before giving up
            var order = await orderContext.Resource();

            var attempts = 5;

            while (attempts > 0 && order?.Status != OrderStatus.Ready)
            {
                await Task.Delay(2000);

                order = await orderContext.Resource();

                attempts--;
            }

            if (order?.Status != OrderStatus.Ready)
            {
                return(new ProcessStepResult {
                    IsSuccess = false, ErrorMessage = "Certificate Request did not complete. Order did not reach Ready status in the time allowed.", Result = order
                });
            }

            // generate temp keypair for signing CSR
            var keyAlg = KeyAlgorithm.RS256;

            if (!string.IsNullOrEmpty(config.CSRKeyAlg))
            {
                if (config.CSRKeyAlg == "RS256")
                {
                    keyAlg = KeyAlgorithm.RS256;
                }

                if (config.CSRKeyAlg == "ECDSA256")
                {
                    keyAlg = KeyAlgorithm.ES256;
                }

                if (config.CSRKeyAlg == "ECDSA384")
                {
                    keyAlg = KeyAlgorithm.ES384;
                }

                if (config.CSRKeyAlg == "ECDSA521")
                {
                    keyAlg = KeyAlgorithm.ES512;
                }
            }

            var csrKey = KeyFactory.NewKey(keyAlg);

            var certFriendlyName = $"{config.PrimaryDomain} [Certify] ";

            // generate cert
            CertificateChain certificateChain = null;
            DateTime?        certExpiration   = null;

            try
            {
                certificateChain = await orderContext.Generate(new CsrInfo
                {
                    CommonName = _idnMapping.GetAscii(config.PrimaryDomain)
                }, csrKey);

                var cert = new X509Certificate2(certificateChain.Certificate.ToDer());
                certExpiration    = cert.NotAfter;
                certFriendlyName += $"{cert.GetEffectiveDateString()} to {cert.GetExpirationDateString()}";
            }
            catch (AcmeRequestException exp)
            {
                var msg = $"Failed to finalize certificate order:  {exp.Error?.Detail}";
                log.Error(msg);

                return(new ProcessStepResult {
                    ErrorMessage = msg, IsSuccess = false, Result = exp.Error
                });
            }

            // file will be named as {expiration yyyyMMdd}_{guid} e.g. 20290301_4fd1b2ea-7b6e-4dca-b5d9-e0e7254e568b
            var certId = certExpiration.Value.ToString("yyyyMMdd") + "_" + Guid.NewGuid().ToString().Substring(0, 8);

            var domainAsPath = config.PrimaryDomain.Replace("*", "_");

            var pfxPath = ExportFullCertPFX(certFriendlyName, csrKey, certificateChain, certId, domainAsPath);

            // ExportFullCertPEM(csrKey, certificateChain, certId, domainAsPath);

            return(new ProcessStepResult {
                IsSuccess = true, Result = pfxPath
            });
        }
Example #27
0
        /// <summary>
        /// Begin order for new certificate for one or more domains, fetching the required challenges
        /// to complete
        /// </summary>
        /// <param name="log">  </param>
        /// <param name="config">  </param>
        /// <param name="orderUri"> Uri of existing order to resume </param>
        /// <returns>  </returns>
        public async Task <PendingOrder> BeginCertificateOrder(ILog log, CertRequestConfig config, string orderUri = null)
        {
            if (DateTime.Now.Subtract(_lastInitDateTime).TotalMinutes > 30)
            {
                // our acme context nonce may have expired (which returns "JWS has an invalid
                // anti-replay nonce") so start a new one
                await InitProvider(_log);
            }

            var pendingOrder = new PendingOrder {
                IsPendingAuthorizations = true
            };

            // prepare a list of all pending authorization we need to complete, or those we have
            // already satisfied
            var authzList = new List <PendingAuthorization>();

            //if no alternative domain specified, use the primary domain as the subject
            var domainOrders = new List <string>
            {
                // order all of the distinct domains in the config (primary + SAN).
                _idnMapping.GetAscii(config.PrimaryDomain)
            };

            if (config.SubjectAlternativeNames != null)
            {
                foreach (var s in config.SubjectAlternativeNames)
                {
                    if (!domainOrders.Contains(s))
                    {
                        domainOrders.Add(_idnMapping.GetAscii(s));
                    }
                }
            }

            try
            {
                IOrderContext order             = null;
                var           remainingAttempts = 3;
                var           orderCreated      = false;
                object        lastException     = null;
                var           orderErrorMsg     = "";

                try
                {
                    while (!orderCreated && remainingAttempts > 0)
                    {
                        try
                        {
                            remainingAttempts--;

                            log.Information($"BeginCertificateOrder: creating/retrieving order. Retries remaining:{remainingAttempts} ");

                            if (orderUri != null)
                            {
                                order = _acme.Order(new Uri(orderUri));
                            }
                            else
                            {
                                order = await _acme.NewOrder(domainOrders);
                            }

                            if (order != null)
                            {
                                orderCreated = true;
                            }
                        }
                        catch (Exception exp)
                        {
                            log.Error(exp.ToString());

                            orderErrorMsg = exp.Message;
                            if (exp is TaskCanceledException)
                            {
                                log.Warning($"BeginCertificateOrder: timeout while communicating with the ACME API");
                            }
                            if (exp is AcmeRequestException)
                            {
                                var err = (exp as AcmeRequestException).Error;

                                // e.g. urn:ietf:params:acme:error:userActionRequired

                                orderErrorMsg = err?.Detail ?? orderErrorMsg;

                                if ((int)err.Status == 429)
                                {
                                    // hit an ACME API rate limit

                                    log.Warning($"BeginCertificateOrder: encountered a rate limit while communicating with the ACME API");

                                    return(new PendingOrder(orderErrorMsg));
                                }

                                if (err.Type?.EndsWith("accountDoesNotExist") == true)
                                {
                                    // wrong account details, probably used staging for prod or vice versa
                                    log.Warning($"BeginCertificateOrder: attempted to use invalid account details with the ACME API");

                                    return(new PendingOrder(orderErrorMsg));
                                }
                            }
                            else if (exp.InnerException != null && exp.InnerException is AcmeRequestException)
                            {
                                orderErrorMsg = (exp.InnerException as AcmeRequestException).Error?.Detail ?? orderErrorMsg;
                            }

                            remainingAttempts--;

                            log.Error($"BeginCertificateOrder: error creating order. Retries remaining:{remainingAttempts} :: {orderErrorMsg} ");

                            lastException = exp;

                            if (remainingAttempts == 0)
                            {
                                // all attempts to create order failed
                                throw;
                            }
                            else
                            {
                                await Task.Delay(1000);
                            }
                        }
                    }
                }
                catch (NullReferenceException exp)
                {
                    var msg = $"Failed to begin certificate order (account problem or API is not currently available): {exp.Message}";

                    log.Error(msg);

                    return(new PendingOrder(msg));
                }

                if (order == null)
                {
                    var msg = "Failed to begin certificate order.";

                    if (lastException is AcmeRequestException)
                    {
                        var err = (lastException as AcmeRequestException).Error;

                        msg = err?.Detail ?? msg;
                        if (lastException != null && (lastException as Exception).InnerException is AcmeRequestException)
                        {
                            msg = ((lastException as Exception).InnerException as AcmeRequestException).Error?.Detail ?? msg;
                        }
                    }
                    else
                    {
                        if (lastException is Exception)
                        {
                            msg += "::" + (lastException as Exception).ToString();
                        }
                    }

                    return(new PendingOrder("Error creating Order with Certificate Authority: " + msg));
                }

                orderUri = order.Location.ToString();

                pendingOrder.OrderUri = orderUri;

                log.Information($"Created ACME Order: {orderUri}");

                // track order in memory, keyed on order Uri
                if (_currentOrders.Keys.Contains(orderUri))
                {
                    _currentOrders.Remove(orderUri);
                }

                _currentOrders.Add(orderUri, order);

                // handle order status 'Ready' if all authorizations are already valid
                var orderDetails = await order.Resource();

                if (orderDetails.Status == OrderStatus.Ready)
                {
                    pendingOrder.IsPendingAuthorizations = false;
                }

                // get all required pending (or already valid) authorizations for this order

                log.Information($"Fetching Authorizations.");

                var orderAuthorizations = await order.Authorizations();

                // get the challenges for each authorization
                foreach (var authz in orderAuthorizations)
                {
                    log.Debug($"Fetching Authz Challenges.");

                    var allChallenges = await authz.Challenges();

                    var res = await authz.Resource();

                    var authzDomain = res.Identifier.Value;
                    if (res.Wildcard == true)
                    {
                        authzDomain = "*." + authzDomain;
                    }

                    var challenges = new List <AuthorizationChallengeItem>();

                    // add http challenge (if any)
                    var httpChallenge = await authz.Http();

                    if (httpChallenge != null)
                    {
                        var httpChallengeStatus = await httpChallenge.Resource();

                        log.Information($"Got http-01 challenge {httpChallengeStatus.Url}");

                        if (httpChallengeStatus.Status == ChallengeStatus.Invalid)
                        {
                            log.Error($"HTTP challenge has an invalid status");
                        }

                        challenges.Add(new AuthorizationChallengeItem
                        {
                            ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP,
                            Key           = httpChallenge.Token,
                            Value         = httpChallenge.KeyAuthz,
                            ChallengeData = httpChallenge,
                            ResourceUri   = $"http://{authzDomain.Replace("*.", "")}/.well-known/acme-challenge/{httpChallenge.Token}",
                            ResourcePath  = $".well-known\\acme-challenge\\{httpChallenge.Token}",
                            IsValidated   = (httpChallengeStatus.Status == ChallengeStatus.Valid)
                        });
                    }

                    // add dns challenge (if any)
                    var dnsChallenge = await authz.Dns();

                    if (dnsChallenge != null)
                    {
                        var dnsChallengeStatus = await dnsChallenge.Resource();

                        log.Information($"Got dns-01 challenge {dnsChallengeStatus.Url}");

                        if (dnsChallengeStatus.Status == ChallengeStatus.Invalid)
                        {
                            log.Error($"DNS challenge has an invalid status");
                        }

                        var dnsValue = _acme.AccountKey.DnsTxt(dnsChallenge.Token); //ComputeDnsValue(dnsChallenge, _acme.AccountKey);
                        var dnsKey   = $"_acme-challenge.{authzDomain}".Replace("*.", "");

                        challenges.Add(new AuthorizationChallengeItem
                        {
                            ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS,
                            Key           = dnsKey,
                            Value         = dnsValue,
                            ChallengeData = dnsChallenge,
                            IsValidated   = (dnsChallengeStatus.Status == ChallengeStatus.Valid)
                        });
                    }

                    // report back on the challenges we now may need to attempt
                    authzList.Add(
                        new PendingAuthorization
                    {
                        Challenges = challenges,
                        Identifier = new IdentifierItem
                        {
                            Dns = authzDomain,
                            IsAuthorizationPending = !challenges.Any(c => c.IsValidated)  //auth is pending if we have no challenges already validated
                        },
                        AuthorizationContext = authz,
                        IsValidated          = challenges.Any(c => c.IsValidated),
                        OrderUri             = orderUri
                    });
                }

                pendingOrder.Authorizations = authzList;

                return(pendingOrder);
            }
            catch (AcmeRequestException exp)
            {
                // failed to register one or more domain identifier with LE (invalid, rate limit or
                // CAA fail?)

                var msg = $"Could not begin certificate order: {exp.Error?.Detail}";

                log.Error(msg);

                return(new PendingOrder(msg));
            }
        }