/// <summary> /// Test if the host fits to the binding /// 100: full match /// 90: partial match (Certificate less specific, e.g. *.example.com cert for sub.example.com binding) /// 50,59,48,...: partial match (IIS less specific, e.g. sub.example.com cert for *.example.com binding) /// 10: default match (catch-all binding) /// 0: no match /// </summary> /// <param name=""></param> /// <param name=""></param> /// <returns></returns> private int Fits(IIISBinding iis, Identifier certificate, SSLFlags flags) { // The default (emtpy) binding matches with all hostnames. // But it's not supported with Central SSL if (string.IsNullOrEmpty(iis.Host) && (!flags.HasFlag(SSLFlags.CentralSsl))) { return(10); } // Match sub.example.com (certificate) with *.example.com (IIS) if (iis.Host.StartsWith("*.") && !certificate.Value.StartsWith("*.")) { if (certificate.Value.ToLower().EndsWith(iis.Host.ToLower().Replace("*.", "."))) { // If there is a binding for *.a.b.c.com (5) and one for *.c.com (3) // then the hostname test.a.b.c.com (5) is a better (more specific) // for the former than for the latter, so we prefer to use that. var hostLevel = certificate.Value.Split('.').Count(); var bindingLevel = iis.Host.Split('.').Count(); return(50 - (hostLevel - bindingLevel)); } else { return(0); } } // Match *.example.com (certificate) with sub.example.com (IIS) if (!iis.Host.StartsWith("*.") && certificate.Value.StartsWith("*.")) { if (iis.Host.ToLower().EndsWith(certificate.Value.ToLower().Replace("*.", "."))) { // But it should not match with another.sub.example.com. var hostLevel = certificate.Value.Split('.').Count(); var bindingLevel = iis.Host.Split('.').Count(); if (hostLevel == bindingLevel) { return(90); } } else { return(0); } } // Full match return(string.Equals(iis.Host, certificate.Value, StringComparison.CurrentCultureIgnoreCase) ? 100 : 0); }
/// <summary> /// Regular constructor /// </summary> /// <param name="flags"></param> /// <param name="port"></param> /// <param name="ip"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="hostName"></param> /// <param name="siteId"></param> public BindingOptions( SSLFlags flags = SSLFlags.None, int port = IISClient.DefaultBindingPort, string ip = IISClient.DefaultBindingIp, byte[] thumbprint = null, string store = null, string hostName = null, long?siteId = null) { Flags = flags; Port = port; IP = ip; Thumbprint = thumbprint; Store = store; Host = hostName; SiteId = siteId; }
/// <summary> /// Create or update a single binding in a single site /// </summary> /// <param name="site"></param> /// <param name="host"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="newPort"></param> public void AddOrUpdateBindings(Site site, string host, SSLFlags flags, byte[] thumbprint, string store, int newPort = 443, bool allowCreate = true) { var existingBindings = site.Bindings.Where(x => string.Equals(x.Host, host, StringComparison.CurrentCultureIgnoreCase)).ToList(); var existingHttpsBindings = existingBindings.Where(x => x.Protocol == "https").ToList(); var existingHttpBindings = existingBindings.Where(x => x.Protocol == "http").ToList(); var update = existingHttpsBindings.Any(); if (update) { // Already on HTTPS, update those bindings to use the Let's Encrypt // certificate instead of the existing one. Note that this only happens // for the target website, if other websites have bindings using other // certificates, they will remain linked to the old ones. foreach (var existingBinding in existingHttpsBindings) { UpdateBinding(site, existingBinding, flags, thumbprint, store); } } else if (allowCreate) { _log.Information(true, "Adding new https binding {host}:{port}", host, newPort); string IP = "*"; if (existingHttpBindings.Any()) { IP = GetIP(existingHttpBindings.First().EndPoint.ToString(), host); } else { _log.Warning("No HTTP binding for {host} on {name}", host, site.Name); } Binding newBinding = site.Bindings.CreateElement("binding"); newBinding.Protocol = "https"; newBinding.BindingInformation = $"{IP}:{newPort}:{host}"; newBinding.CertificateStoreName = store; newBinding.CertificateHash = thumbprint; if (flags > 0) { newBinding.SetAttributeValue("sslFlags", flags); } site.Bindings.Add(newBinding); } else { _log.Information("Binding not created"); } }
/// <summary> /// Make sure the flags are set correctly for updating the binding, /// because special conditions apply to the default binding /// </summary> /// <param name="host"></param> /// <param name="flags"></param> /// <returns></returns> private SSLFlags CheckFlags(bool newBinding, string host, SSLFlags flags) { // SSL flags are not supported at all by Windows 2008 if (_client.Version.Major < 8) { return(SSLFlags.None); } // Add SNI on Windows Server 2012+ for new bindings if (newBinding && !string.IsNullOrEmpty(host) && _client.Version.Major >= 8) { flags |= SSLFlags.SNI; } // Modern flags are not supported by IIS versions lower than 10. // In fact they are not even supported by all versions of IIS 10, // but so far we don't know how to check for these features // availability (IIS reports its version as 10.0.0 even on // Server 2019). if (_client.Version.Major < 10) { flags &= ~SSLFlags.IIS10_Flags; } // Some flags cannot be used together with the CentralSsl flag, // because when using CentralSsl they are supposedly configured at // the server level instead of at the binding level (though the IIS // Manager doesn't seem to expose these options). if (flags.HasFlag(SSLFlags.CentralSsl)) { // Do not allow CentralSSL flag to be set on the default binding // Logic elsewhere in the program should prevent this // from happening. This is merely a sanity check if (string.IsNullOrEmpty(host)) { throw new InvalidOperationException("Central SSL is not supported without a hostname"); } flags &= ~SSLFlags.NotWithCentralSsl; } // All checks passed, return flags return(flags); }
/// <summary> /// Turn on SNI for #915 /// </summary> /// <param name="start"></param> /// <param name="match"></param> /// <param name="allBindings"></param> /// <returns></returns> private SSLFlags UpdateFlags(SSLFlags start, Binding match, Binding[] allBindings) { var updateFlags = start; if (Version.Major >= 8 && !match.HasSSLFlags(SSLFlags.SNI)) { if (allBindings .Except(new[] { match }) .Where(x => StructuralComparisons.StructuralEqualityComparer.Equals(match.CertificateHash, x.CertificateHash)) .Where(x => !x.HasSSLFlags(SSLFlags.SNI)) .Any()) { _log.Warning("Turning on SNI for existing binding to avoid conflict"); return(start | SSLFlags.SNI); } } return(start); }
public void UpdateFtpSite(Target target, SSLFlags flags, CertificateInfo newCertificate, CertificateInfo oldCertificate) { var ftpSites = FtpSites.ToList(); var oldThumbprint = oldCertificate?.Certificate?.Thumbprint; var newThumbprint = newCertificate?.Certificate?.Thumbprint; var updated = 0; foreach (var ftpSite in ftpSites) { var sslElement = ftpSite.GetChildElement("ftpServer"). GetChildElement("security"). GetChildElement("ssl"); var currentThumbprint = sslElement.GetAttributeValue("serverCertHash").ToString(); var update = false; if (ftpSite.Id == target.FtpSiteId) { if (string.Equals(currentThumbprint, newThumbprint, StringComparison.CurrentCultureIgnoreCase)) { _log.Information(true, "No updated need for ftp site {name}", ftpSite.Name); } else { update = true; } } else if (string.Equals(currentThumbprint, oldThumbprint, StringComparison.CurrentCultureIgnoreCase)) { update = true; } if (update) { sslElement.SetAttributeValue("serverCertHash", newThumbprint); _log.Information(true, "Updating existing ftp site {name}", ftpSite.Name); updated += 1; } } if (updated > 0) { _log.Information("Committing {count} {type} site changes to IIS", updated, "ftp"); Commit(); } }
/// <summary> /// Create a new binding /// </summary> /// <param name="site"></param> /// <param name="host"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="port"></param> /// <param name="IP"></param> private void AddBinding(Site site, string host, SSLFlags flags, byte[] thumbprint, string store, int port, string IP) { flags = CheckFlags(host, flags); _log.Information(true, "Adding new https binding {host}:{port}", host, port); Binding newBinding = site.Bindings.CreateElement("binding"); newBinding.Protocol = "https"; newBinding.BindingInformation = $"{IP}:{port}:{host}"; newBinding.CertificateStoreName = store; newBinding.CertificateHash = thumbprint; if (!string.IsNullOrEmpty(host) && Version.Major >= 8) { flags |= SSLFlags.SNI; } if (flags > 0) { newBinding.SetAttributeValue("sslFlags", flags); } site.Bindings.Add(newBinding); }
void IInstallationPlugin.Install(CertificateInfo newCertificate, CertificateInfo oldCertificate) { SSLFlags flags = 0; if (newCertificate.Store == null) { if (_iisClient.Version.Major < 8) { var errorMessage = "Centralized SSL is only supported on IIS8+"; _log.Error(errorMessage); throw new InvalidOperationException(errorMessage); } else { flags |= SSLFlags.CentralSSL; } } foreach (var split in _targetPlugin.Split(_renewal.Binding)) { _iisClient.AddOrUpdateBindings(split, flags, newCertificate, oldCertificate); } }
private void AddToIIS(string host, byte[] certificateHash, X509Store store) { Site site; if (_tempSiteId == null) { site = _iisClient.ServerManager.Sites.Add(host, "http", string.Format("*:80:{0}", host), "X:\\"); _tempSiteId = site.Id; } else { site = _iisClient.ServerManager.Sites.Where(x => x.Id == _tempSiteId).FirstOrDefault(); } SSLFlags flags = SSLFlags.SNI; if (Program.OptionsService.Options.CentralSsl) { flags |= SSLFlags.CentralSSL; } _iisClient.AddOrUpdateBindings(site, host, flags, certificateHash, store?.Name); _iisClient.Commit(); }
/// <summary> /// Turn on SNI for #915 /// </summary> /// <param name="start"></param> /// <param name="match"></param> /// <param name="allBindings"></param> /// <returns></returns> private bool UpdateExistingBindingFlags(SSLFlags start, TBinding match, TBinding[] allBindings, out SSLFlags modified) { modified = start; if (_client.Version.Major < 8) { _log.Warning("Not updating binding on IIS version before 8"); } else if (match.SSLFlags.HasFlag(SSLFlags.SNI)) { _log.Information("Binding is already SNI, no update necessary"); } else { if (allBindings .Except(new[] { match }) .Where(x => x.Port == match.Port) .Where(x => StructuralComparisons.StructuralEqualityComparer.Equals(match.CertificateHash, x.CertificateHash)) .Where(x => !x.SSLFlags.HasFlag(SSLFlags.SNI)) .Any()) { if (!string.IsNullOrEmpty(match.Host)) { _log.Warning("Turning on SNI for existing binding to avoid conflict"); modified = start | SSLFlags.SNI; } else { _log.Warning("Our best match was the default binding and it seems there are other non-SNI enabled " + "bindings listening to the same endpoint, which means we cannot update it without potentially " + "causing problems. Instead, a new binding will be created. You may manually update the bindings " + "if you want IIS to be configured in a different way."); return(false); } } } return(true); }
private void AddToIIS(CertificateInfo certificate) { var host = certificate.HostNames.First(); Site site; if (_tempSiteId == null) { site = _iisClient.ServerManager.Sites.Add(host, "http", string.Format("*:80:{0}", host), "X:\\"); _tempSiteId = site.Id; } else { site = _iisClient.ServerManager.Sites.Where(x => x.Id == _tempSiteId).FirstOrDefault(); } SSLFlags flags = SSLFlags.SNI; if (certificate.Store == null) { flags |= SSLFlags.CentralSSL; } _iisClient.AddOrUpdateBindings(site, host, flags, certificate.Certificate.GetCertHash(), certificate.Store?.Name, 443); _iisClient.Commit(); }
public BindingOptions WithFlags(SSLFlags flags) => new BindingOptions(flags, Port, IP, Thumbprint, Store, Host, SiteId);
public static bool HasSSLFlags(this Binding binding, SSLFlags flags) => (binding.SSLFlags() & flags) == flags;
public static bool HasSSLFlags(this Binding binding, SSLFlags flags) { return((binding.SSLFlags() & flags) == flags); }
public void AddMultipleWildcard2(string storeName, string bindingIp, int bindingPort, SSLFlags inputFlags, SSLFlags expectedFlags) { var originalBindings = new List <MockBinding> { new MockBinding() { IP = "*", Port = 80, Host = "a.example.com", Protocol = "http" } }; var site = new MockSite() { Id = httpOnlyId, Bindings = originalBindings.ToList() }; var iis = new MockIISClient(log) { MockSites = new[] { site } }; var bindingOptions = new BindingOptions(). WithSiteId(httpOnlyId). WithIP(bindingIp). WithPort(bindingPort). WithStore(storeName). WithFlags(inputFlags). WithThumbprint(newCert); iis.AddOrUpdateBindings(new[] { "*.example.com" }, bindingOptions, oldCert1); var expectedBindings = 2; Assert.AreEqual(expectedBindings, site.Bindings.Count); foreach (var newBinding in site.Bindings.Except(originalBindings)) { Assert.AreEqual("https", newBinding.Protocol); Assert.AreEqual(storeName, newBinding.CertificateStoreName); Assert.AreEqual(newCert, newBinding.CertificateHash); Assert.AreEqual(bindingPort, newBinding.Port); Assert.AreEqual(bindingIp, newBinding.IP); Assert.AreEqual(expectedFlags, newBinding.SSLFlags); } }
/// <summary> /// Update existing bindng /// </summary> /// <param name="site"></param> /// <param name="existingBinding"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> private void UpdateBinding(Site site, Binding existingBinding, SSLFlags flags, byte[] thumbprint, string store) { flags = CheckFlags(existingBinding.Host, flags); // IIS 7.x is very picky about accessing the sslFlags attribute var currentFlags = (SSLFlags)existingBinding.Attributes. Where(x => x.Name == "sslFlags"). Where(x => x.Value != null). Select(x => int.Parse(x.Value.ToString())). FirstOrDefault(); if ((currentFlags & ~SSLFlags.SNI) == (flags & ~SSLFlags.SNI) && // Don't care about SNI status ((store == null && existingBinding.CertificateStoreName == null) || StructuralComparisons.StructuralEqualityComparer.Equals(existingBinding.CertificateHash, thumbprint) && string.Equals(existingBinding.CertificateStoreName, store, StringComparison.InvariantCultureIgnoreCase))) { _log.Verbose("No binding update needed"); } else { _log.Information(true, "Updating existing https binding {host}:{port}", existingBinding.Host, existingBinding.EndPoint.Port); // Replace instead of change binding because of #371 var handled = new[] { "protocol", "bindingInformation", "sslFlags", "certificateStoreName", "certificateHash" }; Binding replacement = site.Bindings.CreateElement("binding"); replacement.Protocol = existingBinding.Protocol; replacement.BindingInformation = existingBinding.BindingInformation; replacement.CertificateStoreName = store; replacement.CertificateHash = thumbprint; foreach (ConfigurationAttribute attr in existingBinding.Attributes) { try { if (!handled.Contains(attr.Name) && attr.Value != null) { replacement.SetAttributeValue(attr.Name, attr.Value); } } catch (Exception ex) { _log.Warning("Unable to set attribute {name} on new binding: {ex}", attr.Name, ex.Message); } } // If current binding has SNI, the updated version // will also have that flag set, regardless // of whether or not it was requested by the caller. // Callers should not generally request SNI unless // required for the binding, e.g. for TLS-SNI validation. // Otherwise let the admin be in control. if (currentFlags.HasFlag(SSLFlags.SNI)) { flags |= SSLFlags.SNI; } if (flags > 0) { replacement.SetAttributeValue("sslFlags", flags); } site.Bindings.Remove(existingBinding); site.Bindings.Add(replacement); } }
/// <summary> /// Create or update a single binding in a single site /// </summary> /// <param name="site"></param> /// <param name="host"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> /// <param name="port"></param> public string AddOrUpdateBindings(Site site, string host, SSLFlags flags, byte[] thumbprint, string store, int?port, bool fuzzy) { // Get all bindings which could map to the host var matchingBindings = site.Bindings. Select(x => new { binding = x, fit = Fits(x.Host, host, flags) }). Where(x => x.fit > 0). OrderByDescending(x => x.fit). ToList(); var httpsMatches = matchingBindings.Where(x => x.binding.Protocol == "https"); var httpMatches = matchingBindings.Where(x => x.binding.Protocol == "http"); // Existing https binding for exactly the domain we are looking for, will be // updated to use the new ACME certificate var perfectHttpsMatches = httpsMatches.Where(x => x.fit == 100); if (perfectHttpsMatches.Any()) { foreach (var perfectMatch in perfectHttpsMatches) { UpdateBinding(site, perfectMatch.binding, flags, thumbprint, store); } return(host); } // If we find a http-binding for the domain, a corresponding https binding // is set up to match incoming secure traffic var perfectHttpMatches = httpMatches.Where(x => x.fit == 100); if (perfectHttpMatches.Any()) { AddBinding(site, host, flags, thumbprint, store, port, "*"); return(host); } if (fuzzy) { // There are no perfect matches for the domain, so at this point we start // to look at wildcard and/or default bindings binding. Since they are // order by 'best fit' we look at the first one. if (httpsMatches.Any()) { var bestMatch = httpsMatches.First(); UpdateBinding(site, bestMatch.binding, flags, thumbprint, store); return(bestMatch.binding.Host); } // Nothing on https, then start to look at http if (httpMatches.Any()) { var bestMatch = httpMatches.First(); AddBinding(site, bestMatch.binding.Host, flags, thumbprint, store, port, "*"); return(bestMatch.binding.Host); } } // At this point we haven't even found a partial match for our hostname // so as the ultimate step we create new https binding AddBinding(site, host, flags, thumbprint, store, port, "*"); return(host); }
/// <summary> /// Update/create bindings for all host names in the certificate /// </summary> /// <param name="target"></param> /// <param name="flags"></param> /// <param name="thumbprint"></param> /// <param name="store"></param> public void AddOrUpdateBindings(Target target, SSLFlags flags, CertificateInfo newCertificate, CertificateInfo oldCertificate) { try { var allBindings = WebSites. SelectMany(site => site.Bindings, (site, binding) => new { site, binding }). ToList(); var bindingsUpdated = 0; var found = new List <string>(); var oldThumbprint = oldCertificate?.Certificate?.GetCertHash(); if (oldThumbprint != null) { var siteBindings = allBindings. Where(sb => StructuralComparisons.StructuralEqualityComparer.Equals(sb.binding.CertificateHash, oldThumbprint)). ToList(); // Update all bindings created using the previous certificate foreach (var sb in siteBindings) { try { UpdateBinding(sb.site, sb.binding, flags, newCertificate.Certificate.GetCertHash(), newCertificate.Store?.Name); found.Add(sb.binding.Host); bindingsUpdated += 1; } catch (Exception ex) { _log.Error(ex, "Error updating binding {host}", sb.binding.BindingInformation); throw; } } } // Find all hostnames which are not covered by any of the already updated // bindings yet, because we will want to make sure that those are accessable // in the target site var targetSite = GetWebSite(target.InstallationSiteId ?? target.TargetSiteId ?? -1); IEnumerable <string> todo = target.GetHosts(true); while (todo.Count() > 0) { // Filter by previously matched bindings todo = todo.Where(host => !found.Any(binding => Fits(binding, host, flags) > 0)); if (todo.Count() > 0) { var current = todo.First(); try { var binding = AddOrUpdateBindings( targetSite, current, flags, newCertificate.Certificate.GetCertHash(), newCertificate.Store?.Name, target.SSLPort, true); // Allow a single newly created binding to match with // multiple hostnames on the todo list, e.g. the *.example.com binding // matches with both a.example.com and b.example.com found.Add(binding); bindingsUpdated += 1; } catch (Exception ex) { _log.Error(ex, "Error creating binding {host}: {ex}", current, ex.Message); // Prevent infinite retry loop, we just skip the domain when // an error happens creating a new binding for it. User can // always change/add the bindings manually after all. found.Add(current); } } } if (bindingsUpdated > 0) { _log.Information("Committing {count} {type} binding changes to IIS", bindingsUpdated, "https"); Commit(); _log.Information("IIS will serve the new certificates after the Application Pool IdleTimeout has been reached."); } else { _log.Warning("No bindings have been changed"); } } catch (Exception ex) { _log.Error(ex, "Error installing"); throw; } }
public void UpdateSimple(string storeName, string bindingIp, int bindingPort, SSLFlags inputFlags, SSLFlags expectedFlags) { var iis = new MockIISClient(log) { MockSites = new[] { new MockSite() { Id = regularId, Bindings = new List <MockBinding> { new MockBinding() { IP = "*", Port = 80, Host = regularHost, Protocol = "http" }, new MockBinding() { IP = AltIP, Port = AltPort, Host = regularHost, Protocol = "https", CertificateHash = oldCert1, CertificateStoreName = AltStore, SSLFlags = SSLFlags.None } } } } }; var bindingOptions = new BindingOptions(). WithSiteId(regularId). WithIP(bindingIp). WithPort(bindingPort). WithStore(storeName). WithFlags(inputFlags). WithThumbprint(newCert); var regularSite = iis.GetWebSite(regularId); iis.AddOrUpdateBindings(new[] { regularHost }, bindingOptions, oldCert1); Assert.AreEqual(2, regularSite.Bindings.Count); var updatedBinding = regularSite.Bindings[1]; Assert.AreEqual(regularHost, updatedBinding.Host); Assert.AreEqual("https", updatedBinding.Protocol); Assert.AreEqual(storeName, updatedBinding.CertificateStoreName); Assert.AreEqual(newCert, updatedBinding.CertificateHash); Assert.AreEqual(AltPort, updatedBinding.Port); Assert.AreEqual(AltIP, updatedBinding.IP); Assert.AreEqual(expectedFlags, updatedBinding.SSLFlags); }
public void UpdatePiramid(string certificateHost, string[] ignoreBindings, string expectedBinding, SSLFlags flags) { var iis = new MockIISClient(log) { MockSites = new[] { new MockSite() { Id = piramidId, Bindings = new List <MockBinding> { new MockBinding() { IP = DefaultIP, Port = 80, Host = "a.b.c.com", Protocol = "http" }, new MockBinding() { IP = DefaultIP, Port = 80, Host = "*.b.c.com", Protocol = "http" }, new MockBinding() { IP = DefaultIP, Port = 80, Host = "*.x.y.z.com", Protocol = "http" }, new MockBinding() { IP = DefaultIP, Port = 80, Host = "*.c.com", Protocol = "http" }, new MockBinding() { IP = DefaultIP, Port = 80, Host = "*.com", Protocol = "http" }, new MockBinding() { IP = DefaultIP, Port = 80, Host = "", Protocol = "http" } } } } }; var bindingOptions = new BindingOptions(). WithSiteId(piramidId). WithIP(DefaultIP). WithPort(DefaultPort). WithStore(DefaultStore). WithThumbprint(newCert). WithFlags(flags); var piramidSite = iis.GetWebSite(piramidId); var originalSet = piramidSite.Bindings.Where(x => !ignoreBindings.Contains(x.Host)).ToList(); piramidSite.Bindings = originalSet.ToList().OrderBy(x => Guid.NewGuid()).ToList(); iis.AddOrUpdateBindings(new[] { certificateHost }, bindingOptions, scopeCert); var newBindings = piramidSite.Bindings.Except(originalSet); Assert.AreEqual(1, newBindings.Count()); var newBinding = newBindings.First(); Assert.AreEqual(expectedBinding, newBinding.Host); }
public void AddNewSingle(string storeName, string bindingIp, int bindingPort, SSLFlags inputFlags, SSLFlags expectedFlags, int iisVersion) { var iis = new MockIISClient(log, iisVersion) { MockSites = new[] { new MockSite() { Id = httpOnlyId, Bindings = new List <MockBinding> { new MockBinding() { IP = "*", Port = 80, Host = httpOnlyHost, Protocol = "http" } } } } }; var testHost = httpOnlyHost; var bindingOptions = new BindingOptions(). WithSiteId(httpOnlyId). WithIP(bindingIp). WithPort(bindingPort). WithStore(storeName). WithFlags(inputFlags). WithThumbprint(newCert); var httpOnlySite = iis.GetWebSite(httpOnlyId); iis.AddOrUpdateBindings(new[] { testHost }, bindingOptions, oldCert1); Assert.AreEqual(2, httpOnlySite.Bindings.Count); var newBinding = httpOnlySite.Bindings[1]; Assert.AreEqual(testHost, newBinding.Host); Assert.AreEqual("https", newBinding.Protocol); Assert.AreEqual(storeName, newBinding.CertificateStoreName); Assert.AreEqual(newCert, newBinding.CertificateHash); Assert.AreEqual(bindingPort, newBinding.Port); Assert.AreEqual(bindingIp, newBinding.IP); Assert.AreEqual(expectedFlags, newBinding.SSLFlags); }