public List <DomainOption> GetDomainOptionsFromSite(string siteId) { var defaultNoDomainHost = "(default binding)"; var domainOptions = new List <DomainOption>(); var matchingSites = new IISManager().GetSiteBindingList(Certify.Properties.Settings.Default.IgnoreStoppedSites, siteId); var siteBindingList = matchingSites.Where(s => s.SiteId == siteId); foreach (var siteDetails in siteBindingList) { //if domain not currently in the list of options, add it if (!domainOptions.Any(item => item.Domain == siteDetails.Host)) { DomainOption opt = new DomainOption { Domain = siteDetails.Host, IsPrimaryDomain = false, IsSelected = true, Title = "" }; if (String.IsNullOrEmpty(opt.Domain)) { //binding has no hostname/domain set - user will need to specify opt.Title = defaultNoDomainHost; opt.Domain = defaultNoDomainHost; opt.IsManualEntry = true; } else { opt.Title = siteDetails.Protocol + "://" + opt.Domain; } if (siteDetails.IP != null && siteDetails.IP != "0.0.0.0") { opt.Title += " : " + siteDetails.IP; } domainOptions.Add(opt); } } //TODO: if one or more binding is to a specific IP, how to manage in UI? if (domainOptions.Any(d => !String.IsNullOrEmpty(d.Domain))) { // mark first domain as primary, if we have no other settings if (!domainOptions.Any(d => d.IsPrimaryDomain == true)) { var electableDomains = domainOptions.Where(d => !String.IsNullOrEmpty(d.Domain) && d.Domain != defaultNoDomainHost); if (electableDomains.Any()) { // promote first domain in list to primary by default electableDomains.First().IsPrimaryDomain = true; } } } return(domainOptions); }
public CertifyManager() { Certify.Management.Util.SetSupportedTLSVersions(); var acmeSharp = new Certify.Management.APIProviders.ACMESharpProvider(); // ACME Sharp is both a vault (config storage) provider and ACME client provider _acmeClientProvider = acmeSharp; _vaultProvider = acmeSharp; _siteManager = new ItemManager(); _iisManager = new IISManager(); }
private bool IsManagedSiteRunning(string id, IISManager iis = null) { var managedSite = _siteManager.GetManagedSite(id); if (managedSite != null) { if (iis == null) { iis = _iisManager; } return(iis.IsSiteRunning(id)); } else { //site not identified, assume it is running return(true); } }
/// <summary> /// For current configured environment, show preview of recommended site management (for local IIS, scan sites and recommend actions) /// </summary> /// <returns></returns> public List <ManagedSite> Preview() { List <ManagedSite> sites = new List <ManagedSite>(); if (EnableLocalIISMode) { try { var iisSites = new IISManager().GetSiteBindingList(includeOnlyStartedSites: true).OrderBy(s => s.SiteId).ThenBy(s => s.Host); var siteIds = iisSites.GroupBy(x => x.SiteId); foreach (var s in siteIds) { ManagedSite managedSite = new ManagedSite { Id = s.Key }; managedSite.ItemType = ManagedItemType.SSL_LetsEncrypt_LocalIIS; managedSite.TargetHost = "localhost"; managedSite.Name = iisSites.First(i => i.SiteId == s.Key).SiteName; //TODO: replace sute binding with domain options //managedSite.SiteBindings = new List<ManagedSiteBinding>(); foreach (var binding in s) { var managedBinding = new ManagedSiteBinding { Hostname = binding.Host, IP = binding.IP, Port = binding.Port, UseSNI = true, CertName = "Certify_" + binding.Host }; // managedSite.SiteBindings.Add(managedBinding); } sites.Add(managedSite); } } catch (Exception) { //can't read sites System.Diagnostics.Debug.WriteLine("Can't get IIS site list."); } } return(sites); }
public List <ManagedSite> ImportManagedSitesFromVault(bool mergeSitesAsSan = false) { var sites = new List <ManagedSite>(); //get dns identifiers from vault var vaultManager = new VaultManager(Properties.Settings.Default.VaultPath, ACMESharp.Vault.Providers.LocalDiskVault.VAULT); var iisManager = new IISManager(); var identifiers = vaultManager.GetIdentifiers(); var iisSites = iisManager.GetSiteBindingList(ignoreStoppedSites: Certify.Properties.Settings.Default.IgnoreStoppedSites); foreach (var identifier in identifiers) { //identify IIS site related to this identifier (if any) var iisSite = iisSites.FirstOrDefault(d => d.Host == identifier.Dns); var site = new ManagedSite { Id = Guid.NewGuid().ToString(), GroupId = iisSite?.SiteId, Name = identifier.Dns + (iisSite != null ? " : " + iisSite.SiteName : ""), IncludeInAutoRenew = true, Comments = "Imported from vault", ItemType = ManagedItemType.SSL_LetsEncrypt_LocalIIS, TargetHost = "localhost", RequestConfig = new CertRequestConfig { BindingIPAddress = iisSite?.IP, BindingPort = iisSite?.Port.ToString(), ChallengeType = "http-01", EnableFailureNotifications = true, PerformAutoConfig = true, PerformAutomatedCertBinding = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = true, PrimaryDomain = identifier.Dns, SubjectAlternativeNames = new string[] { identifier.Dns }, WebsiteRootPath = iisSite?.PhysicalPath }, DomainOptions = new List <DomainOption>() { new DomainOption { Domain = identifier.Dns, IsPrimaryDomain = true, IsSelected = true } } }; sites.Add(site); } if (mergeSitesAsSan) { foreach (var s in sites) { //merge sites with same group (iis site etc) and different primary domain if (sites.Any(m => m.GroupId != null && m.GroupId == s.GroupId && m.RequestConfig.PrimaryDomain != s.RequestConfig.PrimaryDomain)) { //existing site to merge into //add san for dns var mergedSite = sites.FirstOrDefault(m => m.GroupId != null && m.GroupId == s.GroupId && m.RequestConfig.PrimaryDomain != s.RequestConfig.PrimaryDomain && m.RequestConfig.PrimaryDomain != null ); if (mergedSite != null) { mergedSite.DomainOptions.Add(new DomainOption { Domain = s.RequestConfig.PrimaryDomain, IsPrimaryDomain = false, IsSelected = true }); //use shortest version of domain name as site name if (mergedSite.RequestConfig.PrimaryDomain.Contains(s.RequestConfig.PrimaryDomain)) { mergedSite.Name = mergedSite.Name.Replace(mergedSite.RequestConfig.PrimaryDomain, s.RequestConfig.PrimaryDomain); } //flag spare site config to be discar s.RequestConfig.PrimaryDomain = null; } } } //discard sites which have been merged into other sites sites.RemoveAll(s => s.RequestConfig.PrimaryDomain == null); } return(sites); }
public async Task <CertificateRequestResult> PerformCertificateRequest(VaultManager vaultManager, ManagedSite managedSite, IProgress <RequestProgressState> progress = null) { // FIXME: refactor into different concerns, there's way to much being done here 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(); } //enable or disable EFS flag on private key certs based on preference vaultManager.UseEFSForSensitiveFiles = Properties.Settings.Default.EnableEFS; //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 }; } })); }
public async Task <CertificateRequestResult> PerformCertificateRequest(VaultManager vaultManager, ManagedSite managedSite) { if (vaultManager == null) { vaultManager = new VaultManager(Properties.Settings.Default.VaultPath, ACMESharp.Vault.Providers.LocalDiskVault.VAULT); } //primary domain and each subject alternative name must now be registered as an identifier with LE and validated var config = managedSite.RequestConfig; List <string> allDomains = new List <string>(); allDomains.Add(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>(); foreach (var domain in allDomains) { var identifierAlias = vaultManager.ComputeIdentifierAlias(domain); //check if this domain already has an associated identifier registerd with LetsEncrypt which hasn't expired yet var 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); } managedSite.AppendLog(new ManagedSiteLogItem { EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertificateRequestStarted, Message = "Attempting Certificate Request: " + managedSite.SiteType }); //begin authorization process (register identifier, request authorization if not already given) var authorization = vaultManager.BeginRegistrationAndValidation(config, identifierAlias, challengeType: config.ChallengeType, domain: domain); if (authorization != null && !identifierAlreadyValid) { if (authorization.Identifier.Authorization.IsPending()) { if (managedSite.SiteType == ManagedSiteType.LocalIIS) { //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.PerformExtensionlessAutoConfig && !authorization.ExtensionlessConfigCheckedOK) { managedSite.AppendLog(new ManagedSiteLogItem { EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertficateRequestFailed, Message = "Failed prerequisite configuration (" + managedSite.SiteType + ")" }); siteManager.StoreSettings(); return(new CertificateRequestResult { IsSuccess = false, ErrorMessage = "Automated checks for extensionless content failed. Authorizations 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." }); } else { //ask LE to validate our challenge response vaultManager.SubmitChallenge(identifierAlias, config.ChallengeType); bool identifierValidated = vaultManager.CompleteIdentifierValidationProcess(authorization.Identifier.Alias); if (!identifierValidated) { allIdentifiersValidated = false; } else { identifierAuthorizations.Add(authorization); } } } } } 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 }); } } } if (allIdentifiersValidated) { string primaryDnsIdentifier = identifierAuthorizations.First().Identifier.Alias; string[] alternativeDnsIdentifiers = identifierAuthorizations.Where(i => i.Identifier.Alias != primaryDnsIdentifier).Select(i => i.Identifier.Alias).ToArray(); var certRequestResult = vaultManager.PerformCertificateRequestProcess(primaryDnsIdentifier, alternativeDnsIdentifiers); if (certRequestResult.IsSuccess) { string pfxPath = certRequestResult.Result.ToString(); if (managedSite.SiteType == ManagedSiteType.LocalIIS && config.PerformAutomatedCertBinding) { var iisManager = new IISManager(); //Install certificate into certificate store and bind to IIS site if (iisManager.InstallCertForDomain(config.PrimaryDomain, pfxPath, cleanupCertStore: true, skipBindings: !config.PerformAutomatedCertBinding)) { //all done managedSite.AppendLog(new ManagedSiteLogItem { EventDate = DateTime.UtcNow, LogItemType = LogItemType.CertificateRequestSuccessful, Message = "Completed certificate request and automated bindings update (IIS)" }); siteManager.StoreSettings(); return(new CertificateRequestResult { IsSuccess = true, ErrorMessage = "Certificate installed and SSL bindings updated for " + config.PrimaryDomain }); } else { return(new CertificateRequestResult { IsSuccess = false, ErrorMessage = "An error occurred installing the certificate. Certificate file may not be valid: " + pfxPath }); } } else { return(new CertificateRequestResult { IsSuccess = true, ErrorMessage = "Certificate created ready for manual binding: " + pfxPath }); } } else { return(new CertificateRequestResult { IsSuccess = false, ErrorMessage = "The Let's Encrypt service did not issue a valid certificate in the time allowed. " + (certRequestResult.ErrorMessage != null ? certRequestResult.ErrorMessage : "") }); } } else { return(new CertificateRequestResult { IsSuccess = false, ErrorMessage = "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. " }); } }