Beispiel #1
0
        public async Task PerformCertificateCleanup()
        {
            try
            {
                var mode = CoreAppSettings.Current.CertificateCleanupMode;
                if (mode == null)
                {
                    mode = CertificateCleanupMode.AfterExpiry;
                }

                if (mode != CertificateCleanupMode.None)
                {
                    var excludedCertThumprints = new List <string>();

                    // excluded thumbprints are all certs currently tracked as managed certs
                    var managedCerts = await GetManagedCertificates();

                    foreach (var c in managedCerts)
                    {
                        if (!string.IsNullOrEmpty(c.CertificateThumbprintHash))
                        {
                            excludedCertThumprints.Add(c.CertificateThumbprintHash.ToLower());
                        }
                    }

                    if (mode == CertificateCleanupMode.FullCleanup)
                    {
                        // cleanup old pfx files in asset store(s), if any
                        var assetPath = Path.Combine(Util.GetAppDataFolder(), "certes", "assets");
                        if (Directory.Exists(assetPath))
                        {
                            var ext = new List <string> {
                                ".pfx"
                            };
                            DeleteOldCertificateFiles(assetPath, ext);
                        }

                        assetPath = Path.Combine(Util.GetAppDataFolder(), "assets");
                        if (Directory.Exists(assetPath))
                        {
                            var ext = new List <string> {
                                ".pfx", ".key", ".crt", ".pem"
                            };
                            DeleteOldCertificateFiles(assetPath, ext);
                        }
                    }

                    // this will only perform expiry cleanup, as no specific thumbprint provided
                    var certsRemoved = CertificateManager.PerformCertificateStoreCleanup(
                        (CertificateCleanupMode)mode,
                        DateTime.Now,
                        matchingName: null,
                        excludedThumbprints: excludedCertThumprints,
                        log: _serviceLog
                        );
                }
            }
            catch (Exception exp)
            {
                // log exception
                _serviceLog?.Error("Failed to perform certificate cleanup: " + exp.ToString());
            }
        }
Beispiel #2
0
        public bool CheckSNI(string host, string sni, bool?useProxyAPI = null)
        {
            // if validation proxy enabled, access to the domain being validated is checked via our
            // remote API rather than directly on the servers
            bool useProxy = useProxyAPI ?? CoreAppSettings.Current.EnableValidationProxyAPI;

            if (useProxy)
            {
                // TODO: check proxy here, needs server support. if successful "return true"; and "LogAction(...)"
                System.Diagnostics.Debug.WriteLine("ProxyAPI is not implemented for Checking SNI config, trying local");
                Log($"Proxy TLS SNI binding check error: {host}, {sni}");

                return(CheckSNI(host, sni, false)); // proxy failed, try local
            }
            var hosts = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.System), @"drivers\etc\hosts");

            try
            {
                var req = new HttpRequestMessage(HttpMethod.Get, $"https://{sni}");
                ServicePointManager.ServerCertificateValidationCallback = (obj, cert, chain, errors) =>
                {
                    // verify SNI-selected certificate is correctly configured
                    return(CertificateManager.VerifyCertificateSAN(cert, sni));
                };

                // modify the hosts file so we can resolve this request locally: create an entry for
                // the primary IP address and also for 127.0.0.1 (where primary IP will not resolve
                // internally i.e. the default resolution is an external IP)

                List <string> testHostEntries = new List <string> {
                    $"\n127.0.0.1\t{sni}",
                };

                var ip = Dns.GetHostEntry(host)?.AddressList?.FirstOrDefault();
                if (ip != null)
                {
                    testHostEntries.Add($"\n{ip}\t{sni}");
                }

                using (StreamWriter writer = File.AppendText(hosts))
                {
                    foreach (var hostEntry in testHostEntries)
                    {
                        writer.Write(hostEntry);
                    }
                }
                Thread.Sleep(250); // wait a bit for hosts file to take effect

                try
                {
                    using (var client = new HttpClient())
                    {
                        var resp = client.SendAsync(req).Result;
                        // if the GET request succeeded, the Cert validation succeeded
                        Log($"Local TLS SNI binding check OK: {host}, {sni}");;
                    }
                }
                finally
                {
                    // clean up temp entries in hosts file
                    try
                    {
                        var txt = File.ReadAllText(hosts);
                        foreach (var hostEntry in testHostEntries)
                        {
                            //should we just remove all .acme.invalid entries instead of looking for our current entries?
                            txt = txt.Substring(0, txt.Length - hostEntry.Length);
                        }

                        File.WriteAllText(hosts, txt);
                    }
                    catch
                    {
                        // if this fails the user will have to clean up manually
                        Log($"Error cleaning up hosts file: {hosts}");
                        throw;
                    }
                }
                return(true); // success!
            }
            catch (Exception ex)
            {
                // eat the error that HttpClient throws, either cert validation failed or the site is
                // inaccessible via https://host name
                Log($"Local TLS SNI binding check error: {host}, {sni}\n{ex.GetType()}: {ex.Message}\n{ex.StackTrace}");
                return(false);
            }
            finally
            {
                // reset the callback for other http requests
                ServicePointManager.ServerCertificateValidationCallback = null;
            }
        }
Beispiel #3
0
        /// <summary>
        /// Creates or updates the htttps bindings 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(ManagedSite managedSite, string pfxPath, bool cleanupCertStore)
        {
            var requestConfig = managedSite.RequestConfig;

            if (new System.IO.FileInfo(pfxPath).Length == 0)
            {
                throw new ArgumentException("InstallCertForRequest: Invalid PFX File");
            }

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

            if (storedCert != null)
            {
                //get list of domains we need to create/update https bindings for
                List <string> dnsHosts = new List <string> {
                    requestConfig.PrimaryDomain
                };
                if (requestConfig.SubjectAlternativeNames != null)
                {
                    dnsHosts.AddRange(requestConfig.SubjectAlternativeNames);
                }

                dnsHosts = dnsHosts.Distinct().ToList();

                // identify the IIS set we want to target
                var site = GetSiteById(managedSite.GroupId);

                if (site != null)
                {
                    //TODO: check site has bindings for given domains, otherwise set back to null
                }

                if (site == null)
                {
                    site = GetSiteByDomain(dnsHosts.FirstOrDefault(d => !String.IsNullOrWhiteSpace(d)));
                }

                // add/update required bindings for each dns hostname
                foreach (var hostname in dnsHosts)
                {
                    //match dns host to IIS site
                    if (String.IsNullOrWhiteSpace(hostname))
                    {
                        throw new ArgumentException("InstallCertForRequest: Invalid (empty) DNS hostname supplied");
                    }

                    if (site != null)
                    {
                        //create/update binding and associate new cert
                        //if any binding elements configured, use those, otherwise auto bind using defaults and SNI
                        InstallCertificateforBinding(site, storedCert, hostname,
                                                     sslPort: !String.IsNullOrWhiteSpace(requestConfig.BindingPort) ? int.Parse(requestConfig.BindingPort) : 443,
                                                     useSNI: (requestConfig.BindingUseSNI != null ? (bool)requestConfig.BindingUseSNI : true),
                                                     ipAddress: !String.IsNullOrWhiteSpace(requestConfig.BindingIPAddress) ? requestConfig.BindingIPAddress : null
                                                     );
                    }
                }

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

                return(true);
            }
            else
            {
                return(false);
            }
        }
Beispiel #4
0
        /// <summary>
        /// Creates or updates the htttps bindings 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 async Task <bool> InstallCertForRequest(ManagedSite managedSite, string pfxPath, bool cleanupCertStore)
        {
            var requestConfig = managedSite.RequestConfig;

            if (new System.IO.FileInfo(pfxPath).Length == 0)
            {
                throw new ArgumentException("InstallCertForRequest: Invalid PFX File");
            }

            //store cert against primary domain
            string certStoreName = CertificateManager.GetDefaultStore().Name;
            var    storedCert    = await CertificateManager.StoreCertificate(requestConfig.PrimaryDomain, pfxPath, isRetry : false, enableRetryBehaviour : _enableCertDoubleImportBehaviour);

            if (storedCert != null)
            {
                var certHash = storedCert.GetCertHash();
                var site     = FindManagedSite(managedSite);

                //get list of domains we need to create/update https bindings for
                List <string> dnsHosts = new List <string> {
                    ToUnicodeString(requestConfig.PrimaryDomain)
                };

                if (requestConfig.SubjectAlternativeNames != null)
                {
                    foreach (var san in requestConfig.SubjectAlternativeNames)
                    {
                        dnsHosts.Add(ToUnicodeString(san));
                    }
                    //dnsHosts.AddRange(requestConfig.SubjectAlternativeNames);
                }

                dnsHosts = dnsHosts.Distinct().ToList();

                // add/update required bindings for each dns hostname
                foreach (var hostname in dnsHosts)
                {
                    //match dns host to IIS site
                    if (String.IsNullOrWhiteSpace(hostname))
                    {
                        throw new ArgumentException("InstallCertForRequest: Invalid (empty) DNS hostname supplied");
                    }

                    if (site != null)
                    {
                        //TODO: if the binding fails we should report it, requires reporting a list of binding results

                        //create/update binding and associate new cert
                        //if any binding elements configured, use those, otherwise auto bind using defaults and SNI
                        InstallCertificateforBinding(certStoreName, certHash, site, hostname,
                                                     sslPort: !String.IsNullOrWhiteSpace(requestConfig.BindingPort) ? int.Parse(requestConfig.BindingPort) : 443,
                                                     useSNI: (requestConfig.BindingUseSNI != null ? (bool)requestConfig.BindingUseSNI : true),
                                                     ipAddress: !String.IsNullOrWhiteSpace(requestConfig.BindingIPAddress) ? requestConfig.BindingIPAddress : null,
                                                     alwaysRecreateBindings: requestConfig.AlwaysRecreateBindings
                                                     );
                    }
                }

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

                return(true);
            }
            else
            {
                return(false);
            }
        }
Beispiel #5
0
        public async Task <CertificateRequestResult> PerformCertificateRequest(VaultManager vaultManager, ManagedSite managedSite, IProgress <RequestProgressState> progress = null)
        {
            return(await Task.Run(async() =>
            {
                try
                {
                    ManagedSiteLog.AppendLog(managedSite.Id, new ManagedSiteLogItem {
                        EventDate = DateTime.UtcNow, LogItemType = LogItemType.GeneralInfo, Message = "Beginning Certificate Request Process: " + managedSite.Name
                    });

                    bool enableIdentifierReuse = false;

                    if (vaultManager == null)
                    {
                        vaultManager = GetVaultManager();
                    }
                    //primary domain and each subject alternative name must now be registered as an identifier with LE and validated

                    if (progress != null)
                    {
                        progress.Report(new RequestProgressState {
                            IsRunning = true, CurrentState = RequestState.Running, Message = "Registering Domain Identifiers"
                        });
                    }

                    await Task.Delay(200); //allow UI update

                    var config = managedSite.RequestConfig;

                    List <string> allDomains = new List <string> {
                        config.PrimaryDomain
                    };

                    if (config.SubjectAlternativeNames != null)
                    {
                        allDomains.AddRange(config.SubjectAlternativeNames);
                    }

                    bool allIdentifiersValidated = true;

                    if (config.ChallengeType == null)
                    {
                        config.ChallengeType = "http-01";
                    }

                    List <PendingAuthorization> identifierAuthorizations = new List <PendingAuthorization>();
                    var distinctDomains = allDomains.Distinct();

                    foreach (var domain in distinctDomains)
                    {
                        var identifierAlias = vaultManager.ComputeIdentifierAlias(domain);

                        //check if this domain already has an associated identifier registerd with LetsEncrypt which hasn't expired yet
                        //await Task.Delay(200); //allow UI update

                        ACMESharp.Vault.Model.IdentifierInfo existingIdentifier = null;

                        if (enableIdentifierReuse)
                        {
                            existingIdentifier = vaultManager.GetIdentifier(domain.Trim().ToLower());
                        }

                        bool identifierAlreadyValid = false;
                        if (existingIdentifier != null &&
                            existingIdentifier.Authorization != null &&
                            (existingIdentifier.Authorization.Status == "valid" || existingIdentifier.Authorization.Status == "pending") &&
                            existingIdentifier.Authorization.Expires > DateTime.Now.AddDays(1))
                        {
                            //we have an existing validated identifier, reuse that for this certificate request
                            identifierAlias = existingIdentifier.Alias;

                            if (existingIdentifier.Authorization.Status == "valid")
                            {
                                identifierAlreadyValid = true;
                            }

                            // managedSite.AppendLog(new ManagedSiteLogItem { EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertificateRequestStarted, Message = "Attempting Certificate Request: " + managedSite.SiteType });
                            System.Diagnostics.Debug.WriteLine("Reusing existing valid non-expired identifier for the domain " + domain);
                        }

                        ManagedSiteLog.AppendLog(managedSite.Id, new ManagedSiteLogItem {
                            EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertificateRequestStarted, Message = "Attempting Domain Validation: " + domain
                        });

                        //begin authorization process (register identifier, request authorization if not already given)
                        if (progress != null)
                        {
                            progress.Report(new RequestProgressState {
                                Message = "Registering and Validating " + domain
                            });
                        }

                        //TODO: make operations async and yeild IO of vault

                        /*var authorization = await Task.Run(() =>
                         * {
                         *  return vaultManager.BeginRegistrationAndValidation(config, identifierAlias, challengeType: config.ChallengeType, domain: domain);
                         * });*/

                        var authorization = vaultManager.BeginRegistrationAndValidation(config, identifierAlias, challengeType: config.ChallengeType, domain: domain);

                        if (authorization != null && authorization.Identifier != null && !identifierAlreadyValid)
                        {
                            if (authorization.Identifier.Authorization.IsPending())
                            {
                                if (managedSite.ItemType == ManagedItemType.SSL_LetsEncrypt_LocalIIS)
                                {
                                    if (progress != null)
                                    {
                                        progress.Report(new RequestProgressState {
                                            Message = "Performing Challenge Response via IIS: " + domain
                                        });
                                    }

                                    //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
                                    //prepare IIS with answer for the LE challenege
                                    authorization = vaultManager.PerformIISAutomatedChallengeResponse(config, authorization);

                                    //if we attempted extensionless config checks, report any errors
                                    if (config.PerformAutoConfig && !authorization.ExtensionlessConfigCheckedOK)
                                    {
                                        ManagedSiteLog.AppendLog(managedSite.Id, new ManagedSiteLogItem {
                                            EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertficateRequestFailed, Message = "Failed prerequisite configuration (" + managedSite.ItemType + ")"
                                        });
                                        siteManager.StoreSettings();

                                        var result = new CertificateRequestResult {
                                            ManagedItem = managedSite, IsSuccess = false, Message = "Automated configuration checks failed. Authorizations will not be able to complete.\nCheck you have http bindings for your site and ensure you can browse to http://" + domain + "/.well-known/acme-challenge/configcheck before proceeding."
                                        };
                                        if (progress != null)
                                        {
                                            progress.Report(new RequestProgressState {
                                                CurrentState = RequestState.Error, Message = result.Message, Result = result
                                            });
                                        }

                                        return result;
                                    }
                                    else
                                    {
                                        if (progress != null)
                                        {
                                            progress.Report(new RequestProgressState {
                                                CurrentState = RequestState.Running, Message = "Requesting Validation from Lets Encrypt: " + domain
                                            });
                                        }

                                        //ask LE to validate our challenge response
                                        vaultManager.SubmitChallenge(identifierAlias, config.ChallengeType);

                                        bool identifierValidated = vaultManager.CompleteIdentifierValidationProcess(authorization.Identifier.Alias);

                                        if (!identifierValidated)
                                        {
                                            if (progress != null)
                                            {
                                                progress.Report(new RequestProgressState {
                                                    CurrentState = RequestState.Error, Message = "Domain validation failed: " + domain
                                                });
                                            }

                                            allIdentifiersValidated = false;
                                        }
                                        else
                                        {
                                            if (progress != null)
                                            {
                                                progress.Report(new RequestProgressState {
                                                    CurrentState = RequestState.Running, Message = "Domain validation completed: " + domain
                                                });
                                            }

                                            identifierAuthorizations.Add(authorization);
                                        }
                                    }
                                }
                            }
                            else
                            {
                                if (authorization.Identifier.Authorization.Status == "valid")
                                {
                                    identifierAuthorizations.Add(new PendingAuthorization {
                                        Identifier = authorization.Identifier
                                    });
                                }
                            }
                        }
                        else
                        {
                            if (identifierAlreadyValid)
                            {
                                //we have previously validated this identifier and it has not yet expired, so we can just reuse it in our cert request
                                identifierAuthorizations.Add(new PendingAuthorization {
                                    Identifier = existingIdentifier
                                });
                            }
                        }
                    }

                    //check if all identifiers validates
                    if (identifierAuthorizations.Count == distinctDomains.Count())
                    {
                        allIdentifiersValidated = true;
                    }

                    if (allIdentifiersValidated)
                    {
                        string primaryDnsIdentifier = identifierAuthorizations.First().Identifier.Alias;
                        string[] alternativeDnsIdentifiers = identifierAuthorizations.Where(i => i.Identifier.Alias != primaryDnsIdentifier).Select(i => i.Identifier.Alias).ToArray();

                        if (progress != null)
                        {
                            progress.Report(new RequestProgressState {
                                CurrentState = RequestState.Running, Message = "Requesting Certificate via Lets Encrypt"
                            });
                        }
                        //await Task.Delay(200); //allow UI update

                        var certRequestResult = vaultManager.PerformCertificateRequestProcess(primaryDnsIdentifier, alternativeDnsIdentifiers);
                        if (certRequestResult.IsSuccess)
                        {
                            if (progress != null)
                            {
                                progress.Report(new RequestProgressState {
                                    CurrentState = RequestState.Success, Message = "Completed Certificate Request."
                                });
                            }

                            string pfxPath = certRequestResult.Result.ToString();

                            if (managedSite.ItemType == ManagedItemType.SSL_LetsEncrypt_LocalIIS && config.PerformAutomatedCertBinding)
                            {
                                if (progress != null)
                                {
                                    progress.Report(new RequestProgressState {
                                        CurrentState = RequestState.Running, Message = "Performing Automated Certificate Binding"
                                    });
                                }
                                //await Task.Delay(200); //allow UI update

                                var iisManager = new IISManager();

                                //Install certificate into certificate store and bind to IIS site
                                if (iisManager.InstallCertForRequest(managedSite.RequestConfig, pfxPath, cleanupCertStore: true))
                                {
                                    //all done
                                    ManagedSiteLog.AppendLog(managedSite.Id, new ManagedSiteLogItem {
                                        EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertificateRequestSuccessful, Message = "Completed certificate request and automated bindings update (IIS)"
                                    });

                                    //udpate managed site summary

                                    try
                                    {
                                        var certInfo = new CertificateManager().GetCertificate(pfxPath);
                                        managedSite.DateStart = certInfo.NotBefore;
                                        managedSite.DateExpiry = certInfo.NotAfter;
                                        managedSite.DateRenewed = DateTime.Now;

                                        managedSite.CertificatePath = pfxPath;
                                    }
                                    catch (Exception)
                                    {
                                        ManagedSiteLog.AppendLog(managedSite.Id, new ManagedSiteLogItem {
                                            EventDate = DateTime.UtcNow, LogItemType = LogItemType.GeneralWarning, Message = "Failed to parse certificate dates"
                                        });
                                    }
                                    siteManager.UpdatedManagedSite(managedSite);

                                    var result = new CertificateRequestResult {
                                        ManagedItem = managedSite, IsSuccess = true, Message = "Certificate installed and SSL bindings updated for " + config.PrimaryDomain
                                    };
                                    if (progress != null)
                                    {
                                        progress.Report(new RequestProgressState {
                                            IsRunning = false, CurrentState = RequestState.Success, Message = result.Message
                                        });
                                    }

                                    return result;
                                }
                                else
                                {
                                    return new CertificateRequestResult {
                                        ManagedItem = managedSite, IsSuccess = false, Message = "An error occurred installing the certificate. Certificate file may not be valid: " + pfxPath
                                    };
                                }
                            }
                            else
                            {
                                //user has opted for manual binding of certificate
                                try
                                {
                                    var certInfo = new CertificateManager().GetCertificate(pfxPath);
                                    managedSite.DateStart = certInfo.NotBefore;
                                    managedSite.DateExpiry = certInfo.NotAfter;
                                    managedSite.DateRenewed = DateTime.Now;

                                    managedSite.CertificatePath = pfxPath;
                                }
                                catch (Exception)
                                {
                                    ManagedSiteLog.AppendLog(managedSite.Id, new ManagedSiteLogItem {
                                        EventDate = DateTime.UtcNow, LogItemType = LogItemType.GeneralWarning, Message = "Failed to parse certificate dates"
                                    });
                                }
                                siteManager.UpdatedManagedSite(managedSite);

                                return new CertificateRequestResult {
                                    ManagedItem = managedSite, IsSuccess = true, Message = "Certificate created ready for manual binding: " + pfxPath
                                };
                            }
                        }
                        else
                        {
                            return new CertificateRequestResult {
                                ManagedItem = managedSite, IsSuccess = false, Message = "The Let's Encrypt service did not issue a valid certificate in the time allowed. " + (certRequestResult.ErrorMessage ?? "")
                            };
                        }
                    }
                    else
                    {
                        return new CertificateRequestResult {
                            ManagedItem = managedSite, IsSuccess = false, Message = "Validation of the required challenges did not complete successfully. Please ensure all domains to be referenced in the Certificate can be used to access this site without redirection. "
                        };
                    }
                }
                catch (Exception exp)
                {
                    System.Diagnostics.Debug.WriteLine(exp.ToString());
                    return new CertificateRequestResult {
                        ManagedItem = managedSite, IsSuccess = false, Message = managedSite.Name + ": Request failed - " + exp.Message
                    };
                }
            }));
        }
Beispiel #6
0
        public async Task <CertificateRequestResult> PerformCertificateRequest(ManagedSite managedSite, IProgress <RequestProgressState> progress = null)
        {
            // FIXME: refactor into different concerns, there's way too much being done here
            if (managedSite.RequestConfig.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP && managedSite.RequestConfig.PerformExtensionlessConfigChecks)
            {
                ReportProgress(progress, new RequestProgressState {
                    IsRunning = true, CurrentState = RequestState.Running, Message = "Performing Config Tests"
                });

                var testResult = await TestChallenge(managedSite);

                if (!testResult.IsOK)
                {
                    return(new CertificateRequestResult {
                        ManagedItem = managedSite, IsSuccess = false, Message = String.Join("; ", testResult.FailedItemSummary), Result = testResult.Result
                    });
                }
            }

            return(await Task.Run(async() =>
            {
                // start with a failure result, set to success when succeeding
                var result = new CertificateRequestResult {
                    ManagedItem = managedSite, IsSuccess = false, Message = ""
                };

                var config = managedSite.RequestConfig;
                try
                {
                    // run pre-request script, if set
                    if (!string.IsNullOrEmpty(config.PreRequestPowerShellScript))
                    {
                        try
                        {
                            string scriptOutput = await PowerShellManager.RunScript(result, config.PreRequestPowerShellScript);
                            LogMessage(managedSite.Id, $"Pre-Request Script output: \n{scriptOutput}");
                        }
                        catch (Exception ex)
                        {
                            LogMessage(managedSite.Id, $"Pre-Request Script error:\n{ex.Message}");
                        }
                    }

                    // if the script has requested the certificate request to be aborted, skip the request
                    if (result.Abort)
                    {
                        LogMessage(managedSite.Id, $"Certificate Request Aborted: {managedSite.Name}");
                        result.Message = "Certificate Request was aborted by PS script";
                        goto CertRequestAborted;
                    }

                    LogMessage(managedSite.Id, $"Beginning Certificate Request Process: {managedSite.Name}");

                    //enable or disable EFS flag on private key certs based on preference
                    if (Properties.Settings.Default.EnableEFS)
                    {
                        _vaultProvider.EnableSensitiveFileEncryption();
                    }

                    //primary domain and each subject alternative name must now be registered as an identifier with LE and validated
                    ReportProgress(progress, new RequestProgressState {
                        IsRunning = true, CurrentState = RequestState.Running, Message = "Registering Domain Identifiers"
                    });

                    await Task.Delay(200); //allow UI update, we should we using async calls instead

                    List <string> allDomains = new List <string> {
                        config.PrimaryDomain
                    };

                    if (config.SubjectAlternativeNames != null)
                    {
                        allDomains.AddRange(config.SubjectAlternativeNames);
                    }

                    // begin by assuming all identifiers are valid
                    bool allIdentifiersValidated = true;

                    if (config.ChallengeType == null)
                    {
                        config.ChallengeType = ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP;
                    }

                    List <PendingAuthorization> identifierAuthorizations = new List <PendingAuthorization>();
                    var distinctDomains = allDomains.Distinct();

                    string failureSummaryMessage = null;

                    // perform validation process for each domain
                    foreach (var domain in distinctDomains)
                    {
                        //begin authorization process (register identifier, request authorization if not already given)
                        var domainIdentifierId = _vaultProvider.ComputeDomainIdentifierId(domain);

                        LogMessage(managedSite.Id, $"Attempting Domain Validation: {domain}", LogItemType.CertificateRequestStarted);
                        ReportProgress(progress, $"Registering and Validating {domain} ");

                        //TODO: make operations async and yield IO of vault

                        /*var authorization = await Task.Run(() =>
                         * {
                         *  return vaultManager.BeginRegistrationAndValidation(config, identifierAlias, challengeType: config.ChallengeType, domain: domain);
                         * });*/

                        // begin authorization by registering the domain identifier. This may return
                        // an already validated authorization or we may still have to complete the
                        // authorization challenge. When rate limits are encountered, this step may fail.
                        var authorization = _vaultProvider.BeginRegistrationAndValidation(config, domainIdentifierId, challengeType: config.ChallengeType, domain: domain);

                        if (authorization != null && authorization.Identifier != null)
                        {
                            // check if authorization is pending, it may already be valid if an
                            // existing authorization was reused
                            if (authorization.Identifier.IsAuthorizationPending)
                            {
                                if (managedSite.ItemType == ManagedItemType.SSL_LetsEncrypt_LocalIIS)
                                {
                                    ReportProgress(progress, $"Performing Challenge Response via IIS: {domain} ");

                                    // ask LE to check our answer to their authorization challenge
                                    // (http-01 or tls-sni-01), LE will then attempt to fetch our
                                    // answer, if all accessible and correct (authorized) LE will
                                    // then allow us to request a certificate
                                    authorization = _vaultProvider.PerformIISAutomatedChallengeResponse(_iisManager, managedSite, authorization);

                                    // pass authorization log items onto main log

                                    /*authorization.LogItems?.ForEach((msg) =>
                                     * {
                                     *  if (msg != null) LogMessage(managedSite.Id, msg, LogItemType.GeneralInfo);
                                     * });*/

                                    if ((config.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP && config.PerformExtensionlessConfigChecks && !authorization.ExtensionlessConfigCheckedOK) ||
                                        (config.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI && config.PerformTlsSniBindingConfigChecks && !authorization.TlsSniConfigCheckedOK))
                                    {
                                        //if we failed the config checks, report any errors
                                        LogMessage(managedSite.Id, $"Failed prerequisite configuration checks ({ managedSite.ItemType })", LogItemType.CertficateRequestFailed);

                                        _siteManager.StoreSettings();

                                        if (config.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_HTTP)
                                        {
                                            result.Message = "Automated configuration checks failed. Authorizations will not be able to complete.\nCheck you have http bindings for your site and ensure you can browse to http://" + domain + "/.well-known/acme-challenge/configcheck before proceeding.";
                                        }

                                        if (config.ChallengeType == ACMESharpCompat.ACMESharpUtils.CHALLENGE_TYPE_SNI)
                                        {
                                            result.Message = "Automated configuration checks failed. Authorizations will not be able to complete.\nCheck you have https SNI bindings for your site\n(ex: '0123456789ABCDEF0123456789ABCDEF.0123456789ABCDEF0123456789ABCDEF.acme.invalid') before proceeding.";
                                        }

                                        ReportProgress(progress, new RequestProgressState {
                                            CurrentState = RequestState.Error, Message = result.Message, Result = result
                                        });

                                        break;
                                    }
                                    else
                                    {
                                        ReportProgress(progress, new RequestProgressState {
                                            CurrentState = RequestState.Running, Message = $"Requesting Validation from Let's Encrypt: {domain}"
                                        });
                                        try
                                        {
                                            //ask LE to validate our challenge response
                                            _vaultProvider.SubmitChallenge(domainIdentifierId, config.ChallengeType);

                                            bool identifierValidated = _vaultProvider.CompleteIdentifierValidationProcess(authorization.Identifier.Alias);

                                            if (!identifierValidated)
                                            {
                                                var identifierInfo = _vaultProvider.GetDomainIdentifier(domain);
                                                var errorMsg = identifierInfo?.ValidationError;
                                                var errorType = identifierInfo?.ValidationErrorType;

                                                failureSummaryMessage = $"Domain validation failed: {domain} \r\n{errorMsg}";
                                                ReportProgress(progress, new RequestProgressState {
                                                    CurrentState = RequestState.Error, Message = failureSummaryMessage
                                                }, managedSite.Id);

                                                allIdentifiersValidated = false;
                                            }
                                            else
                                            {
                                                ReportProgress(progress, new RequestProgressState {
                                                    CurrentState = RequestState.Running, Message = "Domain validation completed: " + domain
                                                }, managedSite.Id);

                                                identifierAuthorizations.Add(authorization);
                                            }
                                        }
                                        finally
                                        {
                                            // clean up challenge answers
                                            // (.well-known/acme-challenge/* files for http-01 or iis
                                            // bindings for tls-sni-01)
                                            authorization.Cleanup();
                                        }
                                    }
                                }
                            }
                            else
                            {
                                // we already have a completed authorization, check it's valid
                                if (authorization.Identifier.Status == "valid")
                                {
                                    LogMessage(managedSite.Id, $"Domain already has current authorization, skipping verification: { domain }");

                                    identifierAuthorizations.Add(new PendingAuthorization {
                                        Identifier = authorization.Identifier
                                    });
                                }
                                else
                                {
                                    LogMessage(managedSite.Id, $"Domain authorization failed : { domain } ");

                                    allIdentifiersValidated = false;
                                }
                            }
                        }
                        else
                        {
                            // could not begin authorization
                            LogMessage(managedSite.Id, $"Could not begin authorization for domain with Let's Encrypt: { domain } ");

                            if (authorization != null && authorization.LogItems != null)
                            {
                                LogMessage(managedSite.Id, authorization.LogItems);
                            }
                            allIdentifiersValidated = false;
                        }

                        // abandon authorization attempts if one of our domains has failed verification
                        if (!allIdentifiersValidated)
                        {
                            break;
                        }
                    }

                    //check if all identifiers have a valid authorization
                    if (identifierAuthorizations.Count != distinctDomains.Count())
                    {
                        allIdentifiersValidated = false;
                    }

                    if (allIdentifiersValidated)
                    {
                        string primaryDnsIdentifier = identifierAuthorizations.First().Identifier.Alias;
                        string[] alternativeDnsIdentifiers = identifierAuthorizations.Select(i => i.Identifier.Alias).ToArray();

                        ReportProgress(progress, new RequestProgressState {
                            CurrentState = RequestState.Running, Message = "Requesting Certificate via Lets Encrypt"
                        }, managedSite.Id);

                        // Perform CSR request
                        // FIXME: make call async
                        var certRequestResult = _vaultProvider.PerformCertificateRequestProcess(primaryDnsIdentifier, alternativeDnsIdentifiers);

                        if (certRequestResult.IsSuccess)
                        {
                            ReportProgress(progress, new RequestProgressState {
                                CurrentState = RequestState.Success, Message = "Completed Certificate Request."
                            }, managedSite.Id);

                            string pfxPath = certRequestResult.Result.ToString();

                            // update managed site summary
                            try
                            {
                                var certInfo = CertificateManager.LoadCertificate(pfxPath);
                                managedSite.DateStart = certInfo.NotBefore;
                                managedSite.DateExpiry = certInfo.NotAfter;
                                managedSite.DateRenewed = DateTime.Now;

                                managedSite.CertificatePath = pfxPath;
                                managedSite.CertificateRevoked = false;

                                //ensure certificate contains all the requested domains
                                var subjectNames = certInfo.GetNameInfo(System.Security.Cryptography.X509Certificates.X509NameType.UpnName, false);

                                LogMessage(managedSite.Id, "New certificate contains following domains: " + subjectNames, LogItemType.GeneralInfo);
                            }
                            catch (Exception)
                            {
                                LogMessage(managedSite.Id, "Failed to parse certificate dates", LogItemType.GeneralError);
                            }

                            if (managedSite.ItemType == ManagedItemType.SSL_LetsEncrypt_LocalIIS && config.PerformAutomatedCertBinding)
                            {
                                ReportProgress(progress, new RequestProgressState {
                                    CurrentState = RequestState.Running, Message = "Performing Automated Certificate Binding"
                                });

                                // Install certificate into certificate store and bind to IIS site
                                if (_iisManager.InstallCertForRequest(managedSite, pfxPath, cleanupCertStore: true))
                                {
                                    //all done
                                    LogMessage(managedSite.Id, "Completed certificate request and automated bindings update (IIS)", LogItemType.CertificateRequestSuccessful);

                                    _siteManager.UpdatedManagedSite(managedSite);

                                    result.IsSuccess = true;
                                    result.Message = $"Certificate installed and SSL bindings updated for {config.PrimaryDomain }";
                                    ReportProgress(progress, new RequestProgressState {
                                        IsRunning = false, CurrentState = RequestState.Success, Message = result.Message
                                    });
                                }
                                else
                                {
                                    result.Message = $"An error occurred installing the certificate. Certificate file may not be valid: {pfxPath}";
                                    LogMessage(managedSite.Id, result.Message, LogItemType.GeneralError);
                                }
                            }
                            else
                            {
                                //user has opted for manual binding of certificate

                                _siteManager.UpdatedManagedSite(managedSite);

                                result.IsSuccess = true;
                                result.Message = $"Certificate created ready for manual binding: {pfxPath}";
                                LogMessage(managedSite.Id, result.Message, LogItemType.CertificateRequestSuccessful);
                            }
                        }
                        else
                        {
                            result.Message = $"The Let's Encrypt service did not issue a valid certificate in the time allowed. {(certRequestResult.ErrorMessage ?? "")}";
                            LogMessage(managedSite.Id, result.Message, LogItemType.CertficateRequestFailed);
                        }
                    }
                    else
                    {
                        result.Message = "Validation of the required challenges did not complete successfully. " + (failureSummaryMessage != null ? failureSummaryMessage : "");
                        LogMessage(managedSite.Id, result.Message, LogItemType.CertficateRequestFailed);
                    }

                    // Goto label for aborted certificate request
                    CertRequestAborted : { }
                }
                catch (Exception exp)
                {
                    result.IsSuccess = false;
                    result.Message = managedSite.Name + ": Request failed - " + exp.Message + " " + exp.ToString();
                    LogMessage(managedSite.Id, result.Message, LogItemType.CertficateRequestFailed);
                    LogMessage(managedSite.Id, String.Join("\r\n", _vaultProvider.GetActionSummary()));
                    System.Diagnostics.Debug.WriteLine(exp.ToString());
                }
                finally
                {
                    // if the request was not aborted, perform post-request actions
                    if (!result.Abort)
                    {
                        // run post-request script, if set
                        if (!string.IsNullOrEmpty(config.PostRequestPowerShellScript))
                        {
                            try
                            {
                                string scriptOutput = await PowerShellManager.RunScript(result, config.PostRequestPowerShellScript);
                                LogMessage(managedSite.Id, $"Post-Request Script output:\n{scriptOutput}");
                            }
                            catch (Exception ex)
                            {
                                LogMessage(managedSite.Id, $"Post-Request Script error: {ex.Message}");
                            }
                        }
                        // run webhook triggers, if set
                        if ((config.WebhookTrigger == Webhook.ON_SUCCESS && result.IsSuccess) ||
                            (config.WebhookTrigger == Webhook.ON_ERROR && !result.IsSuccess) ||
                            (config.WebhookTrigger == Webhook.ON_SUCCESS_OR_ERROR))
                        {
                            try
                            {
                                var(success, code) = await Webhook.SendRequest(config, result.IsSuccess);
                                LogMessage(managedSite.Id, $"Webhook invoked: Url: {config.WebhookUrl}, Success: {success}, StatusCode: {code}");
                            }
                            catch (Exception ex)
                            {
                                LogMessage(managedSite.Id, $"Webhook error: {ex.Message}");
                            }
                        }
                    }
                }
                return result;
            }));
        }
Beispiel #7
0
        public bool VerifyUpdateFile(string tempFile, string expectedHash, bool throwOnDeviation = true)
        {
            var performCertValidation = true;

            var signatureVerified = false;

            if (performCertValidation)
            {
                // check digital signature
                var wintrustSignatureVerified = Security.WinTrust.WinTrust.VerifyEmbeddedSignature(tempFile);

                //get verified signed file cert
                var cert = CertificateManager.GetFileCertificate(tempFile);

                //ensure cert subject
                if (!(cert != null && wintrustSignatureVerified && cert.SubjectName.Name.StartsWith("CN=Webprofusion Pty Ltd, O=Webprofusion Pty Ltd")))
                {
                    if (throwOnDeviation)
                    {
                        throw new Exception("Downloaded file failed digital signature check.");
                    }
                    else
                    {
                        return(false);
                    }
                }
                else
                {
                    signatureVerified = true;
                }
            }

            //verify file SHA256
            string computedSHA256 = null;

            using (Stream stream = new FileStream(tempFile, FileMode.Open, FileAccess.Read, FileShare.None, 8192, true))
            {
                computedSHA256 = GetFileSHA256(stream);
            }

            var hashVerified = false;

            if (expectedHash.ToLower() == computedSHA256)
            {
                hashVerified = true;
            }
            else
            {
                if (throwOnDeviation)
                {
                    throw new Exception("Downloaded file failed SHA256 hash check");
                }
                else
                {
                    return(false);
                }
            }

            if (hashVerified && (!performCertValidation || (performCertValidation && signatureVerified)))
            {
                return(true);
            }
            else
            {
                return(false);
            }
        }