public async Task TestChallengeRequestHttp01() { var site = await iisManager.GetIISSiteById(_siteId); Assert.AreEqual(site.Name, testSiteName); var dummyManagedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = testSiteName, GroupId = site.Id.ToString(), UseStagingMode = true, RequestConfig = new CertRequestConfig { PrimaryDomain = testSiteDomain, Challenges = new ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = "http-01" } }), PerformAutoConfig = true, PerformAutomatedCertBinding = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = true, WebsiteRootPath = testSitePath }, ItemType = ManagedCertificateType.SSL_ACME }; var result = await certifyManager.PerformCertificateRequest(null, dummyManagedCertificate); //ensure cert request was successful Assert.IsTrue(result.IsSuccess, "Certificate Request Not Completed"); //check details of cert, subject alternative name should include domain and expiry must be great than 89 days in the future var managedCertificates = await certifyManager.GetManagedCertificates(); var managedCertificate = managedCertificates.FirstOrDefault(m => m.Id == dummyManagedCertificate.Id); //emsure we have a new managed site Assert.IsNotNull(managedCertificate); //have cert file details Assert.IsNotNull(managedCertificate.CertificatePath); var fileExists = System.IO.File.Exists(managedCertificate.CertificatePath); Assert.IsTrue(fileExists); //check cert is correct var certInfo = CertificateManager.LoadCertificate(managedCertificate.CertificatePath); Assert.IsNotNull(certInfo); var isRecentlyCreated = Math.Abs((DateTime.UtcNow - certInfo.NotBefore).TotalDays) < 2; Assert.IsTrue(isRecentlyCreated); var expiresInFuture = (certInfo.NotAfter - DateTime.UtcNow).TotalDays >= 89; Assert.IsTrue(expiresInFuture); // remove managed site await certifyManager.DeleteManagedCertificate(managedCertificate.Id); }
private async Task <List <ActionStep> > DeployToAllTargetBindings(IBindingDeploymentTarget deploymentTarget, ManagedCertificate managedCertificate, CertRequestConfig requestConfig, string certStoreName, byte[] certHash, List <string> dnsHosts, bool isPreviewOnly = false ) { var actions = new List <ActionStep>(); var targetSites = new List <IBindingDeploymentTargetItem>(); // ensure defaults applied for deployment mode requestConfig.ApplyDeploymentOptionDefaults(); // if single site, add that if (requestConfig.DeploymentSiteOption == DeploymentOption.SingleSite) { if (!string.IsNullOrEmpty(managedCertificate.ServerSiteId)) { var site = await deploymentTarget.GetTargetItem(managedCertificate.ServerSiteId); if (site != null) { targetSites.Add(site); } } } // or add all sites (if required) if (requestConfig.DeploymentSiteOption == DeploymentOption.AllSites || requestConfig.DeploymentSiteOption == DeploymentOption.Auto) { targetSites.AddRange(await deploymentTarget.GetAllTargetItems()); } // for each sites we want to target, identify bindings to add/update as required foreach (var site in targetSites) { try { var existingBindings = await deploymentTarget.GetBindings(site.Id); var existingHttps = existingBindings.Where(e => e.Protocol == "https").ToList(); //remove https bindings which already have an https equivalent (specific hostname or blank) existingBindings.RemoveAll(b => existingHttps.Any(e => e.Host == b.Host) && b.Protocol == "http"); existingBindings = existingBindings.OrderBy(b => b.Protocol).ThenBy(b => b.Host).ToList(); // for each binding create or update an https binding foreach (var b in existingBindings) { var updateBinding = false; //if binding is http and there is no https binding, create one var hostname = b.Host; // install the cert for this binding if the hostname matches, or we have a // matching wildcard, or if there is no hostname specified in the binding if (requestConfig.DeploymentBindingReplacePrevious || requestConfig.DeploymentSiteOption == DeploymentOption.Auto) { // if replacing previous, check if current binding cert hash matches // previous cert hash if (b.CertificateHash != null && (managedCertificate.CertificatePreviousThumbprintHash != null || managedCertificate.CertificateThumbprintHash != null)) { if (string.Equals(b.CertificateHash, managedCertificate.CertificatePreviousThumbprintHash)) { updateBinding = true; } else if (string.Equals(b.CertificateHash, managedCertificate.CertificateThumbprintHash)) { updateBinding = true; } } } if (updateBinding == false) { // TODO: add wildcard match if (string.IsNullOrEmpty(hostname) && requestConfig.DeploymentBindingBlankHostname) { updateBinding = true; } else { if (requestConfig.DeploymentBindingMatchHostname) { updateBinding = ManagedCertificate.IsDomainOrWildcardMatch(dnsHosts, hostname); } } } if (requestConfig.DeploymentBindingOption == DeploymentBindingOption.UpdateOnly) { // update existing bindings only, so only update if this is already an // https binding if (b.Protocol != "https") { updateBinding = false; } } if (b.Protocol != "http" && b.Protocol != "https") { // skip bindings for other service types updateBinding = false; } if (updateBinding) { //SSL port defaults to 443 or the config default, unless we already have an https binding, in which case re-use same port var sslPort = 443; var targetIPAddress = "*"; if (!string.IsNullOrWhiteSpace(requestConfig.BindingPort)) { sslPort = int.Parse(requestConfig.BindingPort); } if (b.Protocol == "https") { if (b.Port > 0) { sslPort = b.Port; } if (!unassignedIPs.Contains(b.IP)) { targetIPAddress = b.IP; } } else { if (!unassignedIPs.Contains(requestConfig.BindingIPAddress)) { targetIPAddress = requestConfig.BindingIPAddress; } } //create/update binding and associate new cert //if any binding elements configured, use those, otherwise auto bind using defaults and SNI var stepActions = await UpdateBinding( deploymentTarget, site, existingBindings, certStoreName, certHash, hostname, sslPort : sslPort, useSNI : (requestConfig.BindingUseSNI != null ? (bool)requestConfig.BindingUseSNI : true), ipAddress : targetIPAddress, alwaysRecreateBindings : requestConfig.AlwaysRecreateBindings, isPreviewOnly : isPreviewOnly ); actions.AddRange(stepActions); } } // } catch (Exception exp) { actions.Add(new ActionStep { Title = site.Name, Category = "Deploy.AddOrUpdateBindings", HasError = true, Description = exp.ToString() }); } } return(actions); }
/// <summary> /// Perform set of test challenges and configuration checks to determine if site appears /// valid for certificate requests /// </summary> /// <param name="managedCertificate"> managed site to check </param> /// <param name="isPreviewMode"> /// If true, perform full set of checks (DNS etc), if false performs minimal/basic checks /// </param> /// <returns> </returns> public async Task <List <StatusMessage> > TestChallenge(ILog log, ManagedCertificate managedCertificate, bool isPreviewMode, IProgress <RequestProgressState> progress = null) { var results = new List <StatusMessage>(); if (managedCertificate.RequestConfig.PerformAutoConfig && managedCertificate.GetChallengeConfig(null).ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { var serverCheck = await _serverProvider.RunConfigurationDiagnostics(managedCertificate.ServerSiteId); results.AddRange(serverCheck.ConvertAll(x => new StatusMessage { IsOK = !x.HasError, HasWarning = x.HasWarning, Message = x.Description })); } if (managedCertificate.GetChallengeConfig(null).ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { if (CoreAppSettings.Current.EnableHttpChallengeServer) { _httpChallengeServerAvailable = await StartHttpChallengeServer(); if (_httpChallengeServerAvailable) { results.Add(new StatusMessage { IsOK = true, Message = "Http Challenge Server process available." }); } else { results.Add(new StatusMessage { IsOK = true, HasWarning = true, Message = "Built-in Http Challenge Server process unavailable or could not start. Challenge responses will fall back to IIS." }); } } } results.AddRange( await _challengeDiagnostics.TestChallengeResponse( log, _serverProvider, managedCertificate, isPreviewMode, CoreAppSettings.Current.EnableDNSValidationChecks, progress ) ); if (progress != null) { if (results.Any(r => r.IsOK == false)) { ReportProgress(progress, new RequestProgressState(RequestState.Error, "One or more tests failed", managedCertificate, isPreviewMode)); } else { ReportProgress(progress, new RequestProgressState(RequestState.Success, "All Tests Completed OK", managedCertificate, isPreviewMode)); } } if (CoreAppSettings.Current.EnableHttpChallengeServer) { await StopHttpChallengeServer(); } return(results); }
/// <summary> /// Simulates responding to a challenge, performs a sample configuration and attempts to /// verify it. /// </summary> /// <param name="serverManager"> </param> /// <param name="managedCertificate"> </param> /// <returns> APIResult </returns> /// <remarks> /// The purpose of this method is to test the options (permissions, configuration) before /// submitting a request to the ACME server, to avoid creating failed requests and hitting /// usage limits. /// </remarks> public async Task <List <StatusMessage> > TestChallengeResponse( ILog log, ICertifiedServer serverManager, ManagedCertificate managedCertificate, bool isPreviewMode, bool enableDnsChecks, IProgress <RequestProgressState> progress = null ) { var results = new List <StatusMessage>(); var requestConfig = managedCertificate.RequestConfig; var result = new StatusMessage { IsOK = true }; var domains = new List <string> { requestConfig.PrimaryDomain }; if (requestConfig.SubjectAlternativeNames != null) { domains.AddRange(requestConfig.SubjectAlternativeNames); } domains = domains.Distinct().ToList(); // if wildcard domain included, check first level labels not also specified, i.e. // *.example.com & www.example.com cannot be mixed, but example.com, *.example.com & // test.wwww.example.com can var invalidLabels = new List <string>(); if (domains.Any(d => d.StartsWith("*."))) { foreach (var wildcard in domains.Where(d => d.StartsWith("*."))) { var rootDomain = wildcard.Replace("*.", ""); // add list of domains where label count exceeds root domain label count invalidLabels.AddRange(domains.Where(domain => domain != wildcard && domain.EndsWith(rootDomain) && domain.Count(s => s == '.') == wildcard.Count(s => s == '.'))); if (invalidLabels.Any()) { results.Add(new StatusMessage { IsOK = false, Message = $"Wildcard domain certificate requests (e.g. {wildcard}) cannot be mixed with requests including immediate subdomains (e.g. {invalidLabels[0]})." }); return(results); } } } var generatedAuthorizations = new List <PendingAuthorization>(); try { // if DNS checks enabled, attempt them here if (isPreviewMode && enableDnsChecks) { var includeIPResolution = false; if (managedCertificate.RequestConfig.Challenges.Any(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP)) { includeIPResolution = true; } log.Information("Performing preview DNS tests. {managedItem}", managedCertificate); var tasks = new List <Task <List <ActionResult> > >(); foreach (var d in domains) { tasks.Add(_netUtil.CheckDNS(log, d.Replace("*.", ""), includeIPResolution)); } var allResults = await Task.WhenAll(tasks); // add DNS check results. DNS check fails are considered a warning instead of an error. foreach (var checkResults in allResults) { foreach (var c in checkResults) { results.Add(new StatusMessage { IsOK = true, HasWarning = !c.IsSuccess, Message = c.Message }); } } } foreach (var domain in domains) { var challengeConfig = managedCertificate.GetChallengeConfig(domain); var challengeType = challengeConfig.ChallengeType; if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { log.Warning("tls-sni-01 challenge type is no longer supported by the Let's Encrypt service. Falling back to http-01"); challengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP; } if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { // if dns validation not selected but one or more domains is a wildcard, reject if (domain.StartsWith("*.")) { results.Add(new StatusMessage { IsOK = false, Message = $"http-01 authorization cannot be used for wildcard domains: {domain}. Use DNS (dns-01) validation instead." }); return(results); } var challengeFileUrl = $"http://{domain}/.well-known/acme-challenge/configcheck"; var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP, ResourcePath = ".well-known\\acme-challenge\\configcheck", ResourceUri = challengeFileUrl, Value = "Extensionless File Config Test - OK" } } }; generatedAuthorizations.Add(simulatedAuthorization); var httpChallengeResult = await PerformChallengeResponse_Http01( log, serverManager, domain, managedCertificate, simulatedAuthorization ); if (!httpChallengeResult.IsSuccess) { result.IsOK = false; result.FailedItemSummary.Add($"Config checks failed to verify http://{domain} is both publicly accessible and can serve extensionless files e.g. {challengeFileUrl}"); result.Message = httpChallengeResult.Message; results.Add(result); // don't check any more after first failure break; } else { results.Add(new StatusMessage { IsOK = true, Message = httpChallengeResult.Message, Result = httpChallengeResult }); } } else if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { result.IsOK = false; result.FailedItemSummary.Add($"The {SupportedChallengeTypes.CHALLENGE_TYPE_SNI} challenge type is no longer available."); results.Add(result); return(results); /* * * var serverVersion = await serverManager.GetServerVersion(); * * if (serverVersion.Major < 8) * { * result.IsOK = false; * result.FailedItemSummary.Add($"The {SupportedChallengeTypes.CHALLENGE_TYPE_SNI} challenge is only available for IIS versions 8+."); * results.Add(result); * * return results; * } * * var simulatedAuthorization = new PendingAuthorization * { * Challenges = new List<AuthorizationChallengeItem> { * new AuthorizationChallengeItem * { * ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_SNI, * HashIterationCount= 1, * Value = GenerateSimulatedKeyAuth() * } * } * }; * * generatedAuthorizations.Add(simulatedAuthorization); * * result.IsOK = * PrepareChallengeResponse_TlsSni01( * log, serverManager, domain, managedCertificate, simulatedAuthorization * )(); * * results.Add(result); */ } else if (challengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { var recordName = $"_acme-challenge-test.{domain}".Replace("*.", ""); if (challengeConfig.ChallengeProvider == Certify.Providers.DNS.AcmeDns.DnsProviderAcmeDns.Definition.Id) { // use real cname to avoid having to setup different records recordName = $"_acme-challenge.{domain}".Replace("*.", ""); } var simulatedAuthorization = new PendingAuthorization { Challenges = new List <AuthorizationChallengeItem> { new AuthorizationChallengeItem { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, Key = recordName, Value = GenerateSimulatedDnsAuthValue() } } }; generatedAuthorizations.Add(simulatedAuthorization); var dnsResult = await PerformChallengeResponse_Dns01( log, domain.Replace("*.", ""), managedCertificate, simulatedAuthorization ); result.Message = dnsResult.Result.Message; result.IsOK = dnsResult.Result.IsSuccess; results.Add(result); } else { throw new NotSupportedException($"ChallengeType not supported: {challengeConfig.ChallengeType}"); } } } finally { //FIXME: needs to be filtered by managed site: result.Message = String.Join("\r\n", GetActionLogSummary()); generatedAuthorizations.ForEach(ga => ga.Cleanup()); } return(results); }
private async Task <DnsChallengeHelperResult> PerformChallengeResponse_Dns01(ILog log, string domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth) { var dnsChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS); if (dnsChallenge == null) { var msg = $"No dns-01 challenge to complete for {managedCertificate.Name}. Request cannot continue."; log.Warning(msg); return(new DnsChallengeHelperResult { Result = new ActionResult { IsSuccess = false, Message = msg }, IsAwaitingUser = false, PropagationSeconds = 0 }); } // create DNS records (manually or via automation) var dnsHelper = new DnsChallengeHelper(); var dnsResult = await dnsHelper.CompleteDNSChallenge(log, managedCertificate, domain, dnsChallenge.Key, dnsChallenge.Value); if (!dnsResult.Result.IsSuccess) { log.Error($"DNS update failed: {dnsResult.Result.Message}"); } else { log.Information($"DNS: {dnsResult.Result.Message}"); } var cleanupQueue = new List <Action> { }; // configure cleanup actions for use after challenge completes pendingAuth.Cleanup = async() => { var result = await dnsHelper.DeleteDNSChallenge(log, managedCertificate, domain, dnsChallenge.Key, dnsChallenge.Value); //log.Information(result.Result?.Message); }; return(dnsResult); }
/// <summary> /// Creates or updates the https 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> public async Task <List <ActionStep> > StoreAndDeploy(IBindingDeploymentTarget deploymentTarget, ManagedCertificate managedCertificate, string pfxPath, string pfxPwd, bool isPreviewOnly) { var actions = new List <ActionStep>(); var requestConfig = managedCertificate.RequestConfig; if (!isPreviewOnly) { if (new System.IO.FileInfo(pfxPath).Length == 0) { throw new ArgumentException("InstallCertForRequest: Invalid PFX File"); } } //store cert in default store against primary domain var certStoreName = CertificateManager.GetStore().Name; X509Certificate2 storedCert = null; byte[] certHash = null; // unless user has opted not to store cert, store it now if (requestConfig.DeploymentSiteOption != DeploymentOption.NoDeployment) { if (!isPreviewOnly) { try { storedCert = await CertificateManager.StoreCertificate(requestConfig.PrimaryDomain, pfxPath, isRetry : false, enableRetryBehaviour : _enableCertDoubleImportBehaviour, pwd : pfxPwd); if (storedCert != null) { certHash = storedCert.GetCertHash(); // TODO: move setting friendly name to cert request manager managedCertificate.CertificateFriendlyName = storedCert.FriendlyName; actions.Add(new ActionStep { HasError = false, Title = "Certificate Stored", Category = "Certificate Storage", Description = "Certificate stored OK" }); } } catch (Exception exp) { actions.Add(new ActionStep { HasError = true, Title = "Certificate Storage Failed", Category = "Certificate Storage", Description = "Error storing certificate." + exp.Message }); return(actions); } } else { //fake cert for preview only storedCert = new X509Certificate2(); certHash = new byte[] { 0x00, 0x01, 0x02 }; } } if (storedCert != null) { //get list of domains we need to create/update https bindings for var dnsHosts = new List <string> { requestConfig.PrimaryDomain }; if (requestConfig.SubjectAlternativeNames != null) { foreach (var san in requestConfig.SubjectAlternativeNames) { dnsHosts.Add(san); } } dnsHosts = dnsHosts .Distinct() .Where(d => !string.IsNullOrEmpty(d)) .ToList(); // depending on our deployment mode we decide which sites/bindings to update: var deployments = await DeployToAllTargetBindings(deploymentTarget, managedCertificate, requestConfig, certStoreName, certHash, dnsHosts, isPreviewOnly); actions.AddRange(deployments); } else { if (requestConfig.DeploymentSiteOption != DeploymentOption.NoDeployment) { actions.Add(new ActionStep { HasError = true, Title = "Certificate Storage", Description = "Certificate could not be stored." }); } } // deployment tasks completed return(actions); }
public async Task <PendingAuthorization> PerformAutomatedChallengeResponse(ILog log, ICertifiedServer iisManager, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth) { var requestConfig = managedCertificate.RequestConfig; var domain = pendingAuth.Identifier.Dns; var challengeConfig = managedCertificate.GetChallengeConfig(domain); if (pendingAuth.Challenges != null) { // from list of possible challenges, select the one we prefer to attempt var requiredChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == challengeConfig.ChallengeType); if (requiredChallenge != null) { pendingAuth.AttemptedChallenge = requiredChallenge; if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { // perform http-01 challenge response var check = await PerformChallengeResponse_Http01(log, iisManager, domain, managedCertificate, pendingAuth); if (requestConfig.PerformExtensionlessConfigChecks) { pendingAuth.AttemptedChallenge.ConfigCheckedOK = check.IsSuccess; } } if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI) { // perform tls-sni-01 challenge response var check = PrepareChallengeResponse_TlsSni01(log, iisManager, domain, managedCertificate, pendingAuth); if (requestConfig.PerformTlsSniBindingConfigChecks) { // set config check OK if all checks return true pendingAuth.AttemptedChallenge.ConfigCheckedOK = check(); } } if (requiredChallenge.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { // perform dns-01 challenge response var check = await PerformChallengeResponse_Dns01(log, domain, managedCertificate, pendingAuth); pendingAuth.AttemptedChallenge.ConfigCheckedOK = check.Result.IsSuccess; pendingAuth.AttemptedChallenge.ChallengeResultMsg = check.Result.Message; pendingAuth.AttemptedChallenge.IsAwaitingUser = check.IsAwaitingUser; pendingAuth.AttemptedChallenge.PropagationSeconds = check.PropagationSeconds; } } } return(pendingAuth); }
/// <summary> /// Request creation of a new managed certificate. This only adds to the list of managed certificates and does not perform the certificate order or deployment etc. /// </summary> /// <param name="item"></param> /// <returns></returns> public async Task <ActionResult <ManagedCertificate> > CreateManagedCertificate(ManagedCertificateCreateOptions item) { if (item.Domains == null || !item.Domains.Any()) { return(new ActionResult <ManagedCertificate>("Managed Certificate must contain one or more domains", false)); } item.Domains = item.Domains.Select(d => d.ToLowerInvariant().Trim()) .Distinct() .ToList(); var primaryDomain = item.Domains.First(); var domainOptions = new ObservableCollection <DomainOption>( item.Domains .Select(d => new DomainOption { Domain = d, IsSelected = true }) ); domainOptions.First().IsPrimaryDomain = true; var managedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = item.Title, IncludeInAutoRenew = true, ItemType = ManagedCertificateType.SSL_ACME, UseStagingMode = true, RequestConfig = new CertRequestConfig { PrimaryDomain = item.Domains.First(), SubjectAlternativeNames = item.Domains.ToArray(), Challenges = new ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = "http-01" } }), PerformAutoConfig = true, PerformAutomatedCertBinding = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = false }, DomainOptions = domainOptions }; try { var result = await _client.UpdateManagedCertificate(managedCertificate); if (result != null) { return(new ActionResult <ManagedCertificate> { IsSuccess = true, Message = "OK", Result = result }); } else { return(new ActionResult <ManagedCertificate> { IsSuccess = false, Message = "Failed to create managed certificate." }); } } catch (Exception exp) { return(new ActionResult <ManagedCertificate> { IsSuccess = false, Message = "Failed to create managed certificate: " + exp.ToString() }); } }
public async Task<List<ActionResult>> Execute(DeploymentTaskExecutionParams execParams) { var definition = GetDefinition(execParams.Definition); var results = await Validate(execParams); if (results.Any()) { return results; } var managedCert = ManagedCertificate.GetManagedCertificate(execParams.Subject); if (string.IsNullOrEmpty(managedCert.CertificatePath)) { results.Add(new ActionResult("No certificate to deploy.", false)); return results; } string vaultUri = execParams.Settings.Parameters.FirstOrDefault(c => c.Key == "vault_uri")?.Value; string vaultPath = execParams.Settings.Parameters.FirstOrDefault(c => c.Key == "vault_secret_path")?.Value; var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("X-Vault-Token", execParams.Credentials["api_token"]); httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); var vaultUrl = $"{vaultUri}{vaultPath}"; byte[] pfxData = File.ReadAllBytes(managedCert.CertificatePath); var pfxPwd = ""; var secret = new { data = new { key = GetEncodedCertComponent("pemkey", pfxData, pfxPwd), cert = GetEncodedCertComponent("pemcrt", pfxData, pfxPwd), intermediates = GetEncodedCertComponent("pemchain", pfxData, pfxPwd), pfx = GetEncodedCertComponent("pfxfull", pfxData, pfxPwd) } }; /* { "data": { }, "options": { }, "version": 0 }"; */ var content = new StringContent(Newtonsoft.Json.JsonConvert.SerializeObject(secret), System.Text.UnicodeEncoding.UTF8, "application/json"); execParams.Log.Information($"Deploying to Vault: {vaultUrl}"); var response = await httpClient.PostAsync(vaultUrl, content); if (response.IsSuccessStatusCode) { return results; } else { var error = await response.Content.ReadAsStringAsync(); return new List<ActionResult> { new ActionResult("Vault storage failed: " + error, false) }; } }
public async Task <List <ActionStep> > GetPreviewActions(ManagedCertificate item) { return(await CertifyClient.PreviewActions(item)); }
/// <summary> /// Generate a list of actions which will be performed on the next renewal of this managed certificate, populating /// the description of each action with a Markdown format description /// </summary> /// <param name="item"></param> /// <param name="serverProvider"></param> /// <param name="certifyManager"></param> /// <returns></returns> public async Task <List <ActionStep> > GeneratePreview( ManagedCertificate item, ICertifiedServer serverProvider, ICertifyManager certifyManager, ICredentialsManager credentialsManager ) { var newLine = "\r\n"; var steps = new List <ActionStep>(); var stepIndex = 1; var hasDomains = true; var allTaskProviders = await certifyManager.GetDeploymentProviders(); var certificateAuthorities = await certifyManager.GetCertificateAuthorities(); // ensure defaults are applied for the deployment mode, overwriting any previous selections item.RequestConfig.ApplyDeploymentOptionDefaults(); if (string.IsNullOrEmpty(item.RequestConfig.PrimaryDomain)) { hasDomains = false; } if (hasDomains) { var allCredentials = await credentialsManager.GetStoredCredentials(); var allDomains = new List <string> { item.RequestConfig.PrimaryDomain }; if (item.RequestConfig.SubjectAlternativeNames != null) { allDomains.AddRange(item.RequestConfig.SubjectAlternativeNames); } allDomains = allDomains.Distinct().OrderBy(d => d).ToList(); // certificate summary var certDescription = new StringBuilder(); var ca = certificateAuthorities.FirstOrDefault(c => c.Id == item.CertificateAuthorityId); certDescription.AppendLine( $"A new certificate will be requested from the *{ca?.Title ?? "Default"}* certificate authority for the following domains:" ); certDescription.AppendLine($"\n**{item.RequestConfig.PrimaryDomain}** (Primary Domain)"); if (item.RequestConfig.SubjectAlternativeNames.Any(s => s != item.RequestConfig.PrimaryDomain)) { certDescription.AppendLine($" and will include the following *Subject Alternative Names*:"); foreach (var d in item.RequestConfig.SubjectAlternativeNames) { certDescription.AppendLine($"* {d} "); } } steps.Add(new ActionStep { Title = "Summary", Description = certDescription.ToString() }); // validation steps : // TODO: preview description should come from the challenge type provider var challengeInfo = new StringBuilder(); foreach (var challengeConfig in item.RequestConfig.Challenges) { challengeInfo.AppendLine( $"{newLine}Authorization will be attempted using the **{challengeConfig.ChallengeType}** challenge type." + newLine ); var matchingDomains = item.GetChallengeConfigDomainMatches(challengeConfig, allDomains); if (matchingDomains.Any()) { challengeInfo.AppendLine( $"{newLine}The following matching domains will use this challenge: " + newLine ); foreach (var d in matchingDomains) { challengeInfo.AppendLine($"{newLine} * {d}"); } challengeInfo.AppendLine( $"{newLine}**Please review the Deployment section below to ensure this certificate will be applied to the expected website bindings (if any).**" + newLine ); } else { challengeInfo.AppendLine( $"{newLine}*No domains will match this challenge type.* Either the challenge is not required or domain matches are not fully configured." ); } challengeInfo.AppendLine(newLine); if (challengeConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP) { challengeInfo.AppendLine( $"This will involve the creation of a randomly named (extensionless) text file for each domain (website) included in the certificate." + newLine ); if (CoreAppSettings.Current.EnableHttpChallengeServer) { challengeInfo.AppendLine( $"The *Http Challenge Server* option is enabled. This will create a temporary web service on port 80 during validation. " + $"This process co-exists with your main web server and listens for http challenge requests to /.well-known/acme-challenge/. " + $"If you are using a web server on port 80 other than IIS (or other http.sys enabled server), that will be used instead." + newLine ); } if (!string.IsNullOrEmpty(item.RequestConfig.WebsiteRootPath) && string.IsNullOrEmpty(challengeConfig.ChallengeRootPath)) { challengeInfo.AppendLine( $"The file will be created at the path `{item.RequestConfig.WebsiteRootPath}\\.well-known\\acme-challenge\\` " + newLine ); } if (!string.IsNullOrEmpty(challengeConfig.ChallengeRootPath)) { challengeInfo.AppendLine( $"The file will be created at the path `{challengeConfig.ChallengeRootPath}\\.well-known\\acme-challenge\\` " + newLine ); } challengeInfo.AppendLine( $"The text file will need to be accessible from the URL `http://<yourdomain>/.well-known/acme-challenge/<randomfilename>` " + newLine); challengeInfo.AppendLine( $"The issuing Certificate Authority will follow any redirection in place (such as rewriting the URL to *https*) but the initial request will be made via *http* on port 80. " + newLine); } if (challengeConfig.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_DNS) { challengeInfo.AppendLine( $"This will involve the creation of a DNS TXT record named `_acme-challenge.yourdomain.com` for each domain or subdomain included in the certificate. " + newLine); if (!string.IsNullOrEmpty(challengeConfig.ChallengeCredentialKey)) { var creds = allCredentials.FirstOrDefault(c => c.StorageKey == challengeConfig.ChallengeCredentialKey); if (creds != null) { challengeInfo.AppendLine( $"The following DNS API Credentials will be used: **{creds.Title}** " + newLine); } else { challengeInfo.AppendLine( $"**Invalid credential settngs.** The currently selected credential does not exist." ); } } else { challengeInfo.AppendLine( $"No DNS API Credentials have been set. API Credentials are normally required to make automatic updates to DNS records." ); } challengeInfo.AppendLine( newLine + $"The issuing Certificate Authority will follow any redirection in place (such as a substitute CNAME pointing to another domain) but the initial request will be made against any of the domain's nameservers. " ); } if (!string.IsNullOrEmpty(challengeConfig.DomainMatch)) { challengeInfo.AppendLine( $"{newLine}This challenge type will be selected based on matching domain **{challengeConfig.DomainMatch}** "); } else { if (item.RequestConfig.Challenges.Count > 1) { challengeInfo.AppendLine( $"{newLine}This challenge type will be selected for any domain not matched by another challenge. "); } else { challengeInfo.AppendLine( $"{newLine}**This challenge type will be selected for all domains.**"); } } challengeInfo.AppendLine($"{newLine}---{newLine}"); } steps.Add(new ActionStep { Title = $"{stepIndex}. Domain Validation", Category = "Validation", Description = challengeInfo.ToString() }); stepIndex++; // pre request tasks if (item.PreRequestTasks?.Any() == true) { var substeps = item.PreRequestTasks.Select(t => new ActionStep { Key = t.Id, Title = $"{t.TaskName} ({allTaskProviders.FirstOrDefault(p => p.Id == t.TaskTypeId)?.Title})", Description = t.Description }); steps.Add(new ActionStep { Title = $"{stepIndex}. Pre-Request Tasks", Category = "PreRequestTasks", Description = $"Execute {substeps.Count()} Pre-Request Tasks", Substeps = substeps.ToList() }); stepIndex++; } // cert request step var certRequest = $"A Certificate Signing Request (CSR) will be submitted to the Certificate Authority, using the **{item.RequestConfig.CSRKeyAlg}** signing algorithm."; steps.Add(new ActionStep { Title = $"{stepIndex}. Certificate Request", Category = "CertificateRequest", Description = certRequest }); stepIndex++; // deployment & binding steps var deploymentDescription = new StringBuilder(); var deploymentStep = new ActionStep { Title = $"{stepIndex}. Deployment", Category = "Deployment", Description = "" }; if ( item.RequestConfig.DeploymentSiteOption == DeploymentOption.Auto || item.RequestConfig.DeploymentSiteOption == DeploymentOption.AllSites || item.RequestConfig.DeploymentSiteOption == DeploymentOption.SingleSite ) { // deploying to single or multiple Site if (item.ItemType == ManagedCertificateType.SSL_LetsEncrypt_LocalIIS) { if (item.RequestConfig.DeploymentBindingMatchHostname) { deploymentDescription.AppendLine( "* Deploy to hostname bindings matching certificate domains."); } if (item.RequestConfig.DeploymentBindingBlankHostname) { deploymentDescription.AppendLine("* Deploy to bindings with blank hostname."); } if (item.RequestConfig.DeploymentBindingReplacePrevious) { deploymentDescription.AppendLine("* Deploy to bindings with previous certificate."); } if (item.RequestConfig.DeploymentBindingOption == DeploymentBindingOption.AddOrUpdate) { deploymentDescription.AppendLine("* Add or Update https bindings as required"); } if (item.RequestConfig.DeploymentBindingOption == DeploymentBindingOption.UpdateOnly) { deploymentDescription.AppendLine("* Update https bindings as required (no auto-created https bindings)"); } if (item.RequestConfig.DeploymentSiteOption == DeploymentOption.SingleSite) { if (!string.IsNullOrEmpty(item.ServerSiteId)) { try { var siteInfo = await serverProvider.GetSiteById(item.ServerSiteId); deploymentDescription.AppendLine($"## Deploying to Site" + newLine + newLine + $"`{siteInfo.Name}`" + newLine); } catch (Exception exp) { deploymentDescription.AppendLine($"Error: **cannot identify selected site.** {exp.Message} "); } } } else { deploymentDescription.AppendLine($"## Deploying to all matching sites:"); } // add deployment sub-steps (if any) var bindingRequest = await certifyManager.DeployCertificate(item, null, true); if (bindingRequest.Actions != null) { deploymentStep.Substeps = bindingRequest.Actions; } if (bindingRequest.Actions == null || !bindingRequest.Actions.Any()) { deploymentStep.Substeps = new List <ActionStep> { new ActionStep { Description = newLine + "**There are no matching targets to deploy to. Certificate will be stored but currently no bindings will be updated.**" } }; } else { deploymentDescription.AppendLine(" Action | Site | Binding "); deploymentDescription.Append(" ------ | ---- | ------- "); } } else { // no website selected, maybe validating http with a manually specified path deploymentDescription.AppendLine($"Manual deployment to site."); } } else if (item.RequestConfig.DeploymentSiteOption == DeploymentOption.DeploymentStoreOnly) { deploymentDescription.AppendLine("* The certificate will be saved to the local machines Certificate Store only (Personal/My Store)"); } else if (item.RequestConfig.DeploymentSiteOption == DeploymentOption.NoDeployment) { deploymentDescription.AppendLine("* The certificate will be saved to local disk only."); } deploymentStep.Description = deploymentDescription.ToString(); steps.Add(deploymentStep); stepIndex++; // post request deployment tasks if (item.PostRequestTasks?.Any() == true) { var substeps = item.PostRequestTasks.Select(t => new ActionStep { Key = t.Id, Title = $"{t.TaskName} ({allTaskProviders.FirstOrDefault(p => p.Id == t.TaskTypeId)?.Title})", Description = t.Description }); steps.Add(new ActionStep { Title = $"{stepIndex}. Post-Request (Deployment) Tasks", Category = "PostRequestTasks", Description = $"Execute {substeps.Count()} Post-Request Tasks", Substeps = substeps.ToList() }); stepIndex++; } stepIndex = steps.Count; } else { steps.Add(new ActionStep { Title = "Certificate has no domains", Description = "No domains have been added to this certificate, so a certificate cannot be requested. Each certificate requires a primary domain (a 'subject') and an optional list of additional domains (subject alternative names)." }); } return(steps); }
public Task <StatusMessage> RevokeCertificate(ILog log, ManagedCertificate managedCertificate) { throw new System.NotImplementedException(); }
/// <summary> /// Add identifiers to a managed cert e.g. certify add 89ccaf11-d7c4-427a-b491-9d3582835c48 test1.test.com;test2.test.com (optionally with --perform-request and 'new' instead of id) /// </summary> /// <param name="args"></param> /// <returns></returns> internal async Task AddIdentifiers(string[] args) { var managedCertId = args[1]; var domains = args[2]?.Split(";, ".ToCharArray()); var performRequestNow = false; if (args.Contains("--perform-request")) { performRequestNow = true; } if (domains != null && domains.Any()) { ManagedCertificate managedCert = null; if (managedCertId == "new") { if (!IsRegistered()) { Console.WriteLine("CLI automation is only available in the registered version of this application."); return; } // create a new managed cert with http validation and auto deployment managedCert = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = domains[0], IncludeInAutoRenew = true, ItemType = ManagedCertificateType.SSL_ACME }; managedCert.RequestConfig.Challenges = new System.Collections.ObjectModel.ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP } }); managedCert.RequestConfig.DeploymentSiteOption = DeploymentOption.Auto; } else { // update an existing managed cert managedCert = await _certifyClient.GetManagedCertificate(managedCertId); } foreach (var d in domains.Where(i => !string.IsNullOrEmpty(i)).Select(i => i.Trim().ToLower())) { var domainOption = managedCert.DomainOptions.FirstOrDefault(o => o.Domain == d); if (domainOption != null) { domainOption.IsSelected = true; } else { managedCert.DomainOptions.Add(new DomainOption { Domain = d, IsManualEntry = true, IsSelected = true, Type = "dns" }); } if (!managedCert.RequestConfig.SubjectAlternativeNames.Contains(d)) { var newSanList = managedCert.RequestConfig.SubjectAlternativeNames.ToList(); newSanList.Add(d); managedCert.RequestConfig.SubjectAlternativeNames = newSanList.ToArray(); } } // check we still have a primary domain, if not selected a default one if (!managedCert.DomainOptions.Any(o => o.IsPrimaryDomain)) { var defaultIdentifier = managedCert.DomainOptions.FirstOrDefault(o => o.IsSelected); if (defaultIdentifier != null) { defaultIdentifier.IsPrimaryDomain = true; managedCert.RequestConfig.PrimaryDomain = defaultIdentifier.Domain; } } var updatedManagedCert = await _certifyClient.UpdateManagedCertificate(managedCert); if (updatedManagedCert != null && performRequestNow) { await _certifyClient.BeginCertificateRequest(updatedManagedCert.Id, true); } } }
public async Task BindingMatchTestsVarious() { /* * Single Site - Default Match * www.test.com * test.com*/ /* * * All Sites(*.test.com) * All Sites(*.test.co.uk) */ var managedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = "TestSite..", GroupId = "test", UseStagingMode = true, RequestConfig = new CertRequestConfig { PrimaryDomain = "test.com", Challenges = new ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = "dns-01" } }), PerformAutomatedCertBinding = true, WebsiteRootPath = "c:\\inetpub\\wwwroot", DeploymentSiteOption = DeploymentOption.SingleSite }, ItemType = ManagedCertificateType.SSL_ACME }; var bindingManager = new BindingDeploymentManager(); var deploymentTarget = new MockBindingDeploymentTarget(); deploymentTarget.AllBindings = _allSites; managedCertificate.ServerSiteId = "ShouldNotMatch"; var preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsFalse(preview.Any(), " Should not match any bindings"); managedCertificate.ServerSiteId = "1.1"; preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsFalse(preview.Any(), "Should not match any bindings (same domain, different sudomains no wildcard)"); managedCertificate.ServerSiteId = "1"; preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsTrue(preview.Count == 1, "Should match one binding"); managedCertificate.ServerSiteId = "1"; managedCertificate.RequestConfig.PrimaryDomain = "*.test.com"; preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsTrue(preview.Count == 1, "Should match 1 binding (root level domain should be ignored using wildcard)"); managedCertificate.ServerSiteId = "1"; managedCertificate.RequestConfig.DeploymentSiteOption = DeploymentOption.AllSites; managedCertificate.RequestConfig.PrimaryDomain = "test.com"; preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsTrue(preview.Count == 1, "Should match 1 binding"); managedCertificate.ServerSiteId = "1"; managedCertificate.RequestConfig.DeploymentSiteOption = DeploymentOption.AllSites; managedCertificate.RequestConfig.PrimaryDomain = "*.test.com"; preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsTrue(preview.Count == 3, "Should match 3 bindings across all sites"); managedCertificate.ServerSiteId = "5"; managedCertificate.RequestConfig.DeploymentSiteOption = DeploymentOption.AllSites; managedCertificate.RequestConfig.PrimaryDomain = "altport.com"; preview = await bindingManager.StoreAndDeployManagedCertificate(deploymentTarget, managedCertificate, null, pfxPwd : "", isPreviewOnly : true); Assert.IsTrue(preview.Count == 2, "Should match 2 bindings across all sites"); Assert.IsTrue(preview.Count(b => b.Category == "Deployment.UpdateBinding" && b.Description.Contains(":9000")) == 1, "Should have 1 port 9000 binding"); Assert.IsTrue(preview.Count(b => b.Category == "Deployment.UpdateBinding" && b.Description.Contains(":9001")) == 1, "Should have 1 port 9001 binding"); foreach (var a in preview) { System.Diagnostics.Debug.WriteLine(a.Description); } }
public async Task TestBindingMatch() { // create test site with mix of hostname and IP only bindings var testStr = "abc123"; PrimaryTestDomain = $"test-{testStr}." + PrimaryTestDomain; var testBindingSiteName = "TestAllBinding_" + testStr; var testSiteDomain = "test" + testStr + "." + PrimaryTestDomain; if (await iisManager.SiteExists(testBindingSiteName)) { await iisManager.DeleteSite(testBindingSiteName); } // create site with IP all unassigned, no hostname var site = await iisManager.CreateSite(testBindingSiteName, "", PrimaryIISRoot, "DefaultAppPool", port : testSiteHttpPort); // add another hostname binding (matching cert and not matching cert) var testDomains = new List <string> { testSiteDomain, "label1." + testSiteDomain, "nested.label." + testSiteDomain }; await iisManager.AddSiteBindings(site.Id.ToString(), testDomains, testSiteHttpPort); // get fresh instance of site since updates site = await iisManager.GetIISSiteById(site.Id.ToString()); var bindingsBeforeApply = site.Bindings.ToList(); Assert.AreEqual(site.Name, testBindingSiteName); var dummyCertPath = Environment.CurrentDirectory + "\\Assets\\dummycert.pfx"; var managedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = testSiteName, ServerSiteId = site.Id.ToString(), UseStagingMode = true, RequestConfig = new CertRequestConfig { PrimaryDomain = testSiteDomain, Challenges = new ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = "http-01" } }), PerformAutoConfig = true, PerformAutomatedCertBinding = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = true, WebsiteRootPath = testSitePath, DeploymentSiteOption = DeploymentOption.SingleSite, DeploymentBindingMatchHostname = true, DeploymentBindingBlankHostname = true, DeploymentBindingReplacePrevious = true, SubjectAlternativeNames = new string[] { testSiteDomain, "label1." + testSiteDomain } }, ItemType = ManagedCertificateType.SSL_ACME, CertificatePath = dummyCertPath }; var actions = await new BindingDeploymentManager().StoreAndDeploy( iisManager.GetDeploymentTarget(), managedCertificate, dummyCertPath, "", false); foreach (var a in actions) { System.Console.WriteLine(a.Description); } // get cert info to compare hash var certInfo = CertificateManager.LoadCertificate(managedCertificate.CertificatePath); // check IIS site bindings site = await iisManager.GetIISSiteById(site.Id.ToString()); var finalBindings = site.Bindings.ToList(); Assert.IsTrue(bindingsBeforeApply.Count < finalBindings.Count, "Should have new bindings"); try { // check we have the new bindings we expected // blank hostname binding var testBinding = finalBindings.FirstOrDefault(b => b.Host == "" && b.Protocol == "https"); Assert.IsTrue(IsCertHashEqual(testBinding.CertificateHash, certInfo.GetCertHash()), "Blank hostname binding should be added and have certificate set"); // TODO: testDomains includes matches and not matches to test foreach (var d in testDomains) { // check san domain now has an https binding testBinding = finalBindings.FirstOrDefault(b => b.Host == d && b.Protocol == "https"); if (!d.StartsWith("nested.")) { Assert.IsNotNull(testBinding); Assert.IsTrue(IsCertHashEqual(testBinding.CertificateHash, certInfo.GetCertHash()), "hostname binding should be added and have certificate set"); } else { Assert.IsNull(testBinding, "nested binding should be null"); } } // check existing bindings have been updated as expected /*foreach (var b in finalBindings) * { * if (b.Protocol == "https") * { * // check this item is one we should have included (is matching domain or has * // no hostname) * bool shouldBeIncluded = false; * * if (!String.IsNullOrEmpty(b.Host)) * { * if (testDomains.Contains(b.Host)) * { * shouldBeIncluded = true; * } * } * else * { * shouldBeIncluded = true; * } * * bool isCertMatch = StructuralComparisons.StructuralEqualityComparer.Equals(b.CertificateHash, certInfo.GetCertHash()); * * if (shouldBeIncluded) * { * Assert.IsTrue(isCertMatch, "Binding should have been updated with cert hash but was not."); * } * else * { * Assert.IsFalse(isCertMatch, "Binding should not have been updated with cert hash but was."); * } * } * }*/ } finally { // clean up IIS either way await iisManager.DeleteSite(testBindingSiteName); if (certInfo != null) { CertificateManager.RemoveCertificate(certInfo); } } }
public Task <CertificateRequestResult> PerformCertificateRequest(ILog log, ManagedCertificate managedCertificate, IProgress <RequestProgressState> progress = null, bool resumePaused = false, bool skipRequest = false, bool failOnSkip = false) { throw new NotImplementedException(); }
/// <summary> /// Add identifiers to a managed cert e.g. certify add 89ccaf11-d7c4-427a-b491-9d3582835c48 test1.test.com;test2.test.com (optionally with --perform-request and 'new' instead of id) /// </summary> /// <param name="args"></param> /// <returns></returns> internal async Task AddIdentifiers(string[] args) { if (args.Length < 3) { Console.WriteLine("Not enough arguments"); return; } var managedCertId = args[1]; var domains = args[2]?.Split(";, ".ToCharArray()); var performRequestNow = false; if (args.Contains("--perform-request")) { performRequestNow = true; } if (domains != null && domains.Any()) { ManagedCertificate managedCert = null; if (managedCertId == "new") { InitPlugins(); if (!IsRegistered()) { Console.WriteLine("CLI automation is only available in the licensed version of this application."); return; } // optional load a single managed certificate tempalte from json ManagedCertificate templateCert = null; var jsonArgIndex = Array.IndexOf(args, "--template"); if (jsonArgIndex != -1) { if (args.Length + 1 >= jsonArgIndex + 1) { var pathArg = args[jsonArgIndex + 1]; try { var jsonTemplate = System.IO.File.ReadAllText(pathArg); templateCert = JsonConvert.DeserializeObject <ManagedCertificate>(jsonTemplate); } catch (Exception) { Console.WriteLine($"Failed to read or parse managed certificate template json. " + pathArg); } } else { Console.WriteLine($"Input file path argument is required for json template."); } } // create a new managed cert with http validation and auto deployment if (templateCert == null) { managedCert = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = domains[0], IncludeInAutoRenew = true, ItemType = ManagedCertificateType.SSL_ACME }; managedCert.RequestConfig.Challenges = new System.Collections.ObjectModel.ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP } }); managedCert.RequestConfig.DeploymentSiteOption = DeploymentOption.Auto; } else { managedCert = templateCert.CopyAsTemplate(); // if no managed cert name specifed, use first domain if (string.IsNullOrEmpty(managedCert.Name)) { managedCert.Name = domains[0]; } } } else { // update an existing managed cert managedCert = await _certifyClient.GetManagedCertificate(managedCertId); } foreach (var d in domains.Where(i => !string.IsNullOrEmpty(i)).Select(i => i.Trim().ToLower())) { var domainOption = managedCert.DomainOptions.FirstOrDefault(o => o.Domain == d); if (domainOption != null) { domainOption.IsSelected = true; } else { managedCert.DomainOptions.Add(new DomainOption { Domain = d, IsManualEntry = true, IsSelected = true, Type = "dns" }); } if (!managedCert.RequestConfig.SubjectAlternativeNames.Contains(d)) { var newSanList = managedCert.RequestConfig.SubjectAlternativeNames.ToList(); newSanList.Add(d); managedCert.RequestConfig.SubjectAlternativeNames = newSanList.ToArray(); } } // check we still have a primary domain, if not selected a default one if (!managedCert.DomainOptions.Any(o => o.IsPrimaryDomain)) { var defaultIdentifier = managedCert.DomainOptions.FirstOrDefault(o => o.IsSelected); if (defaultIdentifier != null) { defaultIdentifier.IsPrimaryDomain = true; managedCert.RequestConfig.PrimaryDomain = defaultIdentifier.Domain; } } var updatedManagedCert = await _certifyClient.UpdateManagedCertificate(managedCert); if (updatedManagedCert != null && performRequestNow) { await _certifyClient.BeginCertificateRequest(updatedManagedCert.Id, true, false); } } }
public Task <ManagedCertificate> UpdateManagedCertificate(ManagedCertificate site) { throw new NotImplementedException(); }
public async Task TestCreateManyManagedCertificates() { var itemManager = new ItemManager(TEST_PATH); var testItem = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = "TestSite..", GroupId = "test", UseStagingMode = true, RequestConfig = new CertRequestConfig { PrimaryDomain = "testsite.com", Challenges = new ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = "http-01" } }), PerformAutoConfig = true, PerformAutomatedCertBinding = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = true }, ItemType = ManagedCertificateType.SSL_ACME }; // create competing sets of tasks to create managed items var numItems = 100; // 100,000 items takes about 40 mins to generate // now attempt async creation of bindings var taskSet = new Task[numItems]; var timer = Stopwatch.StartNew(); for (var i = 0; i < numItems; i++) { testItem.Name = "MultiTest_" + i; testItem.Id = Guid.NewGuid().ToString(); taskSet[i] = itemManager.UpdatedManagedCertificate(testItem); } // create a large number of managed items, to see if we encounter isses saving/loading from DB async try { await Task.WhenAll(taskSet); timer.Stop(); System.Diagnostics.Debug.WriteLine($"Created {numItems} in { timer.ElapsedMilliseconds}ms avg:{ timer.ElapsedMilliseconds / numItems}ms"); } catch (Exception) { throw; } finally { // now clean up #if DEBUG await itemManager.DeleteManagedCertificatesByName("MultiTest_"); #endif } }
public async Task <List <ManagedCertificate> > GetManagedCertificates(ManagedCertificateFilter filter = null) { var list = new List <ManagedCertificate>(); if (await IsPresent()) { var directorySearch = new DirectoryInfo(Path.Combine(_settingsPath, "renewal")); if (directorySearch.Exists) { var configFiles = directorySearch.GetFiles("*.conf", SearchOption.AllDirectories); foreach (var config in configFiles) { try { var id = config.Name.Replace(".conf", ""); var managedCert = new ManagedCertificate { Id = "certbot://" + id, Name = id, ItemType = ManagedCertificateType.SSL_ExternallyManaged, SourceId = Definition.Id, SourceName = Definition.Title }; var certFile = new FileInfo(Path.Combine(_settingsPath, "live", id, "cert.pem")); if (certFile.Exists) { try { var cert = Certify.Management.CertificateManager.ReadCertificateFromPem(certFile.FullName); var parsedCert = new System.Security.Cryptography.X509Certificates.X509Certificate2(cert.GetEncoded()); managedCert.DateStart = cert.NotBefore; managedCert.DateExpiry = cert.NotAfter; managedCert.DateRenewed = cert.NotBefore; managedCert.DateLastRenewalAttempt = cert.NotBefore; managedCert.CertificateThumbprintHash = parsedCert.Thumbprint; managedCert.CertificatePath = certFile.FullName; managedCert.LastRenewalStatus = RequestState.Success; if (cert.NotAfter < DateTime.Now.AddDays(29)) { // assume certs with less than 30 days left have failed to renew managedCert.LastRenewalStatus = RequestState.Error; managedCert.RenewalFailureMessage = "Check certbot configuration. This certificate will expire in less than 30 days and has not yet automatically renewed."; } managedCert.RequestConfig = new CertRequestConfig { PrimaryDomain = parsedCert.SubjectName.Name }; var sn = ((System.Collections.ArrayList)cert.GetSubjectAlternativeNames()); List <string> sans = new List <string>(); foreach (System.Collections.ArrayList s in sn) { sans.Add(s[1].ToString()); } managedCert.RequestConfig.SubjectAlternativeNames = sans.ToArray(); managedCert.DomainOptions = new System.Collections.ObjectModel.ObservableCollection <DomainOption> { new DomainOption { Domain = managedCert.RequestConfig.PrimaryDomain, IsPrimaryDomain = true, IsManualEntry = true, IsSelected = true } }; } catch (Exception exp) { System.Diagnostics.Debug.WriteLine("Failed to parse cert: " + exp); } } //var cfg = ParseIni(File.ReadAllText(config.FullName)); managedCert.IsChanged = false; list.Add(managedCert); } catch (Exception exp) { System.Diagnostics.Debug.WriteLine($"Failed to parse config: [{config}] " + exp); } } } } return(list); }
/// <summary> /// Prepares IIS to respond to a http-01 challenge /// </summary> /// <returns> Test the challenge response locally. </returns> private async Task <ActionResult> PerformChallengeResponse_Http01(ILog log, ICertifiedServer iisManager, string domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth) { var requestConfig = managedCertificate.RequestConfig; var httpChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_HTTP); if (httpChallenge == null) { var msg = $"No http challenge to complete for {managedCertificate.Name}. Request cannot continue."; log.Warning(msg); return(new ActionResult { IsSuccess = false, Message = msg }); } log.Information($"Preparing challenge response for Let's Encrypt server to check at: {httpChallenge.ResourceUri} with content {httpChallenge.Value}"); log.Information("If the challenge response file is not accessible at this exact URL the validation will fail and a certificate will not be issued."); // get website root path (from challenge config or fallback to deprecated // WebsiteRootPath), expand environment variables if required var websiteRootPath = requestConfig.WebsiteRootPath; var challengeConfig = managedCertificate.GetChallengeConfig(domain); if (!string.IsNullOrEmpty(challengeConfig.ChallengeRootPath)) { websiteRootPath = challengeConfig.ChallengeRootPath; } if (!string.IsNullOrEmpty(managedCertificate.ServerSiteId)) { var siteInfo = await iisManager.GetSiteById(managedCertificate.ServerSiteId); if (siteInfo == null) { return(new ActionResult { IsSuccess = false, Message = "IIS Website unavailable. Site may be removed or IIS is unavailable" }); } // if website root path not specified, determine it now if (string.IsNullOrEmpty(websiteRootPath)) { websiteRootPath = siteInfo.Path; } if (!string.IsNullOrEmpty(websiteRootPath) && websiteRootPath.Contains("%")) { // if websiteRootPath contains %websiteroot% variable, replace that with the // current physical path for the site if (websiteRootPath.Contains("%websiteroot%")) { // sets env variable for this process only Environment.SetEnvironmentVariable("websiteroot", siteInfo.Path); } // expand any environment variables present in site path websiteRootPath = Environment.ExpandEnvironmentVariables(websiteRootPath); } } log.Information("Using website path {path}", websiteRootPath); if (string.IsNullOrEmpty(websiteRootPath) || !Directory.Exists(websiteRootPath)) { // our website no longer appears to exist on disk, continuing would potentially // create unwanted folders, so it's time for us to give up var msg = $"The website root path for {managedCertificate.Name} could not be determined. Request cannot continue."; log.Error(msg); return(new ActionResult { IsSuccess = false, Message = msg }); } // copy temp file to path challenge expects in web folder var destFile = Path.Combine(websiteRootPath, httpChallenge.ResourcePath); var destPath = Path.GetDirectoryName(destFile); if (!Directory.Exists(destPath)) { try { Directory.CreateDirectory(destPath); } catch (Exception exp) { // failed to create directory, probably permissions or may be invalid config var msg = $"Pre-config check failed: Could not create directory: {destPath}"; log.Error(exp, msg); return(new ActionResult { IsSuccess = false, Message = msg }); } } // copy challenge response to web folder /.well-known/acme-challenge. Check if it already // exists (as in 'configcheck' file) as can cause conflicts. if (!File.Exists(destFile) || !destFile.EndsWith("configcheck")) { try { File.WriteAllText(destFile, httpChallenge.Value); } catch (Exception exp) { // failed to create configcheck file, probably permissions or may be invalid config var msg = $"Pre-config check failed: Could not create file: {destFile}"; log.Error(exp, msg); return(new ActionResult { IsSuccess = false, Message = msg }); } } // prepare cleanup - should this be configurable? Because in some case many sites // renewing may all point to the same web root, we keep the configcheck file pendingAuth.Cleanup = () => { if (!destFile.EndsWith("configcheck") && File.Exists(destFile)) { log.Debug("Challenge Cleanup: Removing {file}", destFile); try { File.Delete(destFile); } catch { } } }; // if config checks are enabled but our last renewal was successful, skip auto config // until we have failed twice if (requestConfig.PerformExtensionlessConfigChecks) { if (managedCertificate.DateRenewed != null && managedCertificate.RenewalFailureCount < 2) { return(new ActionResult { IsSuccess = true, Message = $"Skipping URL access checks and auto config (if applicable): {httpChallenge.ResourceUri}. Will resume checks if renewal failure count exceeds 2 attempts." }); } // first check if it already works with no changes if (await _netUtil.CheckURL(log, httpChallenge.ResourceUri)) { return(new ActionResult { IsSuccess = true, Message = $"Verified URL is accessible: {httpChallenge.ResourceUri}" }); } // initial check didn't work, if auto config enabled attempt to find a working config if (requestConfig.PerformAutoConfig) { // FIXME: need to only overwrite config we have auto populated, not user // specified config, compare to our preconfig and only overwrite if same as ours? // Or include preset key in our config, or make behaviour configurable LogAction($"Pre-config check failed: Auto-config will overwrite existing config: {destPath}\\web.config"); var configOptions = Directory.EnumerateFiles(Environment.CurrentDirectory + "\\Scripts\\Web.config\\", "*.config"); foreach (var configFile in configOptions) { // create a web.config for extensionless files, then test it (make a request // for the extensionless configcheck file over http) var webConfigContent = File.ReadAllText(configFile); // no existing config, attempt auto config and perform test LogAction($"Testing config alternative: " + configFile); try { System.IO.File.WriteAllText(destPath + "\\web.config", webConfigContent); } catch (Exception exp) { LogAction($"Failed to write config: " + exp.Message); } if (await _netUtil.CheckURL(log, httpChallenge.ResourceUri)) { return(new ActionResult { IsSuccess = true, Message = $"Verified URL is accessible: {httpChallenge.ResourceUri}" }); } } } // failed to auto configure or confirm resource is accessible return(new ActionResult { IsSuccess = false, Message = $"Could not verify URL is accessible: {httpChallenge.ResourceUri}" }); } else { return(new ActionResult { IsSuccess = false, Message = $"Config checks disabled. Did not verify URL access: {httpChallenge.ResourceUri}" }); } }
public ProgressLogSink(IProgress <RequestProgressState> progress, ManagedCertificate item, ICertifyManager certifyManager) { _progress = progress; _item = item; _certifyManager = certifyManager; }
private Func <bool> PrepareChallengeResponse_TlsSni01(ILog log, ICertifiedServer iisManager, string domain, ManagedCertificate managedCertificate, PendingAuthorization pendingAuth) { var requestConfig = managedCertificate.RequestConfig; var tlsSniChallenge = pendingAuth.Challenges.FirstOrDefault(c => c.ChallengeType == SupportedChallengeTypes.CHALLENGE_TYPE_SNI); if (tlsSniChallenge == null) { log.Warning($"No tls-sni-01 challenge to complete for {managedCertificate.Name}. Request cannot continue."); return(() => false); } var sha256 = System.Security.Cryptography.SHA256.Create(); var z = new byte[tlsSniChallenge.HashIterationCount][]; // compute n sha256 hashes, where n=challengedata.iterationcount z[0] = sha256.ComputeHash(Encoding.UTF8.GetBytes(tlsSniChallenge.Value)); for (var i = 1; i < z.Length; i++) { z[i] = sha256.ComputeHash(z[i - 1]); } // generate certs and install iis bindings var cleanupQueue = new List <Action>(); var checkQueue = new List <Func <bool> >(); foreach (var hex in z.Select(b => BitConverter.ToString(b).Replace("-", "").ToLower())) { var sni = $"{hex.Substring(0, 32)}.{hex.Substring(32)}.acme.invalid"; log.Information($"Preparing binding at: https://{domain}, sni: {sni}"); var x509 = CertificateManager.GenerateSelfSignedCertificate(sni); CertificateManager.StoreCertificate(x509); var certStoreName = CertificateManager.GetDefaultStore().Name; // iisManager.InstallCertificateforBinding(certStoreName, x509.GetCertHash(), // managedCertificate.ServerSiteId, sni); // add check to the queue checkQueue.Add(() => _netUtil.CheckSNI(domain, sni).Result); // add cleanup actions to queue cleanupQueue.Add(() => iisManager.RemoveHttpsBinding(managedCertificate.ServerSiteId, sni)); cleanupQueue.Add(() => CertificateManager.RemoveCertificate(x509)); } // configure cleanup to execute the cleanup queue pendingAuth.Cleanup = () => cleanupQueue.ForEach(a => a()); // perform our own config checks return(() => checkQueue.All(check => check())); }
public async Task <ManagedCertificate> Update(ManagedCertificate site) { DebugLog(); return(await _certifyManager.UpdateManagedCertificate(site)); }
public async Task ImportCSV(string[] args) { InitPlugins(); if (!IsRegistered()) { Console.WriteLine("Import is only available in the registered version of this application."); return; } bool isNumeric(string input) { return(int.TryParse(input, out _)); } var filename = args[args.Length - 1]; Console.ForegroundColor = ConsoleColor.White; Console.WriteLine("Importing CSV: " + filename); var currentManagedCertificates = await _certifyClient.GetManagedCertificates(new ManagedCertificateFilter() { }); var rows = System.IO.File.ReadAllLines(filename); var csvHasHeaders = false; var rowID = 0; // set default column index values int?siteIdIdx = 0, nameIdx = 1, domainsIdx = 2, primaryDomainIdx = null, includeInAutoRenewIdx = null, performAutoConfigIdx = null, performChallengeFileCopyIdx = null, performExtensionlessConfigChecksIdx = null, performTlsSniBindingConfigChecksIdx = null, performAutomatedCertBindingIdx = null, enableFailureNotificationsIdx = null, preRequestPowerShellScriptIdx = null, postRequestPowerShellScriptIdx = null, webhookTriggerIdx = null, webhookMethodIdx = null, webhookUrlIdx = null, webhookContentTypeIdx = null, webhookContentBodyIdx = null; foreach (var row in rows) { try { // does the first row contain headers in the csv file? if ((rowID == 0) && row.Contains("siteid") && row.Contains("domains")) { csvHasHeaders = true; } // first row contains headers, we need to figure out the position of each column if ((rowID == 0) && csvHasHeaders) { var columnTitles = row.Split(','); var colID = 0; foreach (var title in columnTitles) { // because we never know how people are going to put data in the csv, // convert titles to lowercase before searching for the column index var cleanTitle = title.Trim().ToLower(); // set the column ids switch (cleanTitle) { case "siteid": siteIdIdx = colID; break; case "name": nameIdx = colID; break; case "domains": domainsIdx = colID; break; case "primarydomain": primaryDomainIdx = colID; break; case "includeinautorenew": includeInAutoRenewIdx = colID; break; case "performautoconfig": performAutoConfigIdx = colID; break; case "performchallengefilecopy": performChallengeFileCopyIdx = colID; break; case "performextensionlessconfigchecks": performExtensionlessConfigChecksIdx = colID; break; case "performtlssnibindingconfigchecks": performTlsSniBindingConfigChecksIdx = colID; break; case "performautomatedcertbinding": performAutomatedCertBindingIdx = colID; break; case "enablefailurenotifications": enableFailureNotificationsIdx = colID; break; case "prerequestpowershellscript": preRequestPowerShellScriptIdx = colID; break; case "postrequestpowershellscript": postRequestPowerShellScriptIdx = colID; break; case "webhooktrigger": webhookTriggerIdx = colID; break; case "webhookmethod": webhookMethodIdx = colID; break; case "webhookurl": webhookUrlIdx = colID; break; case "webhookcontenttype": webhookContentTypeIdx = colID; break; case "webhookcontentbody": webhookContentBodyIdx = colID; break; } colID++; } } else { // required fields SiteId, Name, Domain;Domain2;Domain3 var values = Regex.Split(row, @",(?![^\{]*\})"); // get all values separated by commas except those found between {} var siteId = values[(int)siteIdIdx].Trim(); var siteName = values[(int)nameIdx].Trim(); var domains = values[(int)domainsIdx].Trim().Split(';'); // optional fields bool IncludeInAutoRenew = true, PerformAutoConfig = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = true, PerformTlsSniBindingConfigChecks = true, PerformAutomatedCertBinding = true, EnableFailureNotifications = true; string primaryDomain = "", PreRequestPowerShellScript = "", PostRequestPowerShellScript = "", WebhookTrigger = Webhook.ON_NONE, WebhookMethod = "", WebhookUrl = "", WebhookContentType = "", WebhookContentBody = ""; if (primaryDomainIdx != null) { primaryDomain = values[(int)primaryDomainIdx].Trim(); } if (includeInAutoRenewIdx != null) { IncludeInAutoRenew = Convert.ToBoolean(values[(int)includeInAutoRenewIdx].Trim()); } if (performAutoConfigIdx != null) { PerformAutoConfig = Convert.ToBoolean(values[(int)performAutoConfigIdx].Trim()); } if (performChallengeFileCopyIdx != null) { PerformChallengeFileCopy = Convert.ToBoolean(values[(int)performChallengeFileCopyIdx].Trim()); } if (performExtensionlessConfigChecksIdx != null) { PerformExtensionlessConfigChecks = Convert.ToBoolean(values[(int)performExtensionlessConfigChecksIdx].Trim()); } if (performTlsSniBindingConfigChecksIdx != null) { PerformTlsSniBindingConfigChecks = Convert.ToBoolean(values[(int)performTlsSniBindingConfigChecksIdx].Trim()); } if (performAutomatedCertBindingIdx != null) { PerformAutomatedCertBinding = Convert.ToBoolean(values[(int)performAutomatedCertBindingIdx].Trim()); } if (enableFailureNotificationsIdx != null) { EnableFailureNotifications = Convert.ToBoolean(values[(int)enableFailureNotificationsIdx].Trim()); } if (preRequestPowerShellScriptIdx != null) { PreRequestPowerShellScript = values[(int)preRequestPowerShellScriptIdx].Trim(); } if (postRequestPowerShellScriptIdx != null) { PostRequestPowerShellScript = values[(int)postRequestPowerShellScriptIdx].Trim(); } if (webhookTriggerIdx != null) { WebhookTrigger = values[(int)webhookTriggerIdx].Trim(); // the webhook trigger text is case sensitive switch (WebhookTrigger.ToLower()) { case "none": WebhookTrigger = Webhook.ON_NONE; break; case "on success": WebhookTrigger = Webhook.ON_SUCCESS; break; case "on error": WebhookTrigger = Webhook.ON_ERROR; break; case "on success or error": WebhookTrigger = Webhook.ON_SUCCESS_OR_ERROR; break; } if (webhookMethodIdx != null) { var tmpWebhookMethod = values[(int)webhookMethodIdx].Trim(); WebhookMethod = tmpWebhookMethod.ToUpper(); if (WebhookMethod == "POST") { if (webhookUrlIdx != null) { WebhookContentType = values[(int)webhookContentTypeIdx].Trim(); } if (webhookContentBodyIdx != null) { WebhookContentBody = values[(int)webhookContentBodyIdx].Trim(); // cleanup json values from csv conversion WebhookContentBody = Regex.Replace(WebhookContentBody, @"(""|'')|(""|'')", ""); WebhookContentBody = WebhookContentBody.Replace("\"\"", "\""); } } } if (webhookUrlIdx != null) { WebhookUrl = values[(int)webhookUrlIdx].Trim(); } } if (string.IsNullOrEmpty(siteId) || !isNumeric(siteId)) { throw new Exception("Error: SiteID column is blank or contains non-numeric characters."); } var newManagedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), GroupId = siteId, ServerSiteId = siteId, Name = siteName, IncludeInAutoRenew = IncludeInAutoRenew, ItemType = ManagedCertificateType.SSL_ACME }; newManagedCertificate.RequestConfig.Challenges = new System.Collections.ObjectModel.ObservableCollection <CertRequestChallengeConfig>( new List <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_HTTP } }); newManagedCertificate.RequestConfig.PerformAutoConfig = PerformAutoConfig; newManagedCertificate.RequestConfig.PerformChallengeFileCopy = PerformChallengeFileCopy; newManagedCertificate.RequestConfig.PerformExtensionlessConfigChecks = PerformExtensionlessConfigChecks; newManagedCertificate.RequestConfig.PerformTlsSniBindingConfigChecks = PerformTlsSniBindingConfigChecks; newManagedCertificate.RequestConfig.PerformAutomatedCertBinding = PerformAutomatedCertBinding; newManagedCertificate.RequestConfig.EnableFailureNotifications = EnableFailureNotifications; newManagedCertificate.RequestConfig.PreRequestPowerShellScript = PreRequestPowerShellScript; newManagedCertificate.RequestConfig.PostRequestPowerShellScript = PostRequestPowerShellScript; newManagedCertificate.RequestConfig.WebhookTrigger = WebhookTrigger; newManagedCertificate.RequestConfig.WebhookMethod = WebhookMethod; newManagedCertificate.RequestConfig.WebhookUrl = WebhookUrl; newManagedCertificate.RequestConfig.WebhookContentType = WebhookContentType; newManagedCertificate.RequestConfig.WebhookContentBody = WebhookContentBody; var isPrimaryDomain = true; // if we have passed in a primary domain into the csv file, use that instead // of the first domain in the list if (primaryDomain != "") { isPrimaryDomain = false; } var sans = new List <string>(); foreach (var d in domains) { if (!string.IsNullOrWhiteSpace(d)) { var cleanDomainName = d.Trim(); if ((isPrimaryDomain) || (cleanDomainName == primaryDomain.Trim())) { newManagedCertificate.RequestConfig.PrimaryDomain = cleanDomainName; isPrimaryDomain = true; } var sanExists = false; // check for existing SAN entry foreach (var site in currentManagedCertificates) { if (!sanExists) { var filtered = site.DomainOptions.Where(options => options.Domain == cleanDomainName); if (filtered.Count() > 0) { Console.WriteLine("Processing Row: " + rowID + " - Domain entry (" + cleanDomainName + ") already exists in certificate (" + site.Name + ")"); sanExists = true; } } } // if the current san entry doesn't exist in our certificate list, // let's add it if (!sanExists) { newManagedCertificate.DomainOptions.Add(new DomainOption { Domain = cleanDomainName, IsPrimaryDomain = isPrimaryDomain, IsSelected = true, Title = d }); sans.Add(cleanDomainName); } isPrimaryDomain = false; } } // if the new certificate to be imported has sans, then add the certificate // request to the system if (sans.Count() > 0) { newManagedCertificate.RequestConfig.SubjectAlternativeNames = sans.ToArray(); // add managed site Console.WriteLine("Creating Managed Certificate: " + newManagedCertificate.Name); await _certifyClient.UpdateManagedCertificate(newManagedCertificate); // add the new certificate request to our in-memory list currentManagedCertificates.Add(newManagedCertificate); } } } catch (Exception exp) { Console.WriteLine("There was a problem importing row " + rowID + " - " + exp.ToString()); } rowID++; } }
public async Task <List <ActionStep> > PreviewActions(ManagedCertificate site) { DebugLog(); return(await _certifyManager.GeneratePreview(site)); }
/// <summary> /// Creates or updates the https 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> public async Task <List <ActionStep> > StoreAndDeployManagedCertificate(IBindingDeploymentTarget deploymentTarget, ManagedCertificate managedCertificate, string pfxPath, bool cleanupCertStore, bool isPreviewOnly) { var actions = new List <ActionStep>(); var requestConfig = managedCertificate.RequestConfig; if (!isPreviewOnly) { if (new System.IO.FileInfo(pfxPath).Length == 0) { throw new ArgumentException("InstallCertForRequest: Invalid PFX File"); } } //store cert against primary domain var certStoreName = CertificateManager.GetStore().Name; X509Certificate2 storedCert = null; byte[] certHash = null; // unless user has opted not to store cert, store it now if (requestConfig.DeploymentSiteOption != DeploymentOption.NoDeployment) { if (!isPreviewOnly) { try { storedCert = await CertificateManager.StoreCertificate(requestConfig.PrimaryDomain, pfxPath, isRetry : false, enableRetryBehaviour : _enableCertDoubleImportBehaviour); if (storedCert != null) { certHash = storedCert.GetCertHash(); actions.Add(new ActionStep { HasError = false, Title = "Certificate Stored", Category = "Certificate Storage", Description = "Certificate stored OK" }); } } catch (Exception exp) { actions.Add(new ActionStep { HasError = true, Title = "Certificate Storage Failed", Category = "Certificate Storage", Description = "Error storing certificate." + exp.Message }); return(actions); } } else { //fake cert for preview only storedCert = new X509Certificate2(); certHash = new byte[] { 0x00, 0x01, 0x02 }; } } if (storedCert != null) { //get list of domains we need to create/update https bindings for var dnsHosts = new List <string> { requestConfig.PrimaryDomain }; if (requestConfig.SubjectAlternativeNames != null) { foreach (var san in requestConfig.SubjectAlternativeNames) { dnsHosts.Add(san); } } dnsHosts = dnsHosts .Distinct() .Where(d => !string.IsNullOrEmpty(d)) .ToList(); // depending on our deployment mode we decide which sites/bindings to update: var deployments = await DeployToAllTargetBindings(deploymentTarget, managedCertificate, requestConfig, certStoreName, certHash, dnsHosts, isPreviewOnly); actions.AddRange(deployments); // if required, cleanup old certs we are replacing. Only applied if we have deployed // the certificate, otherwise we keep the old one // FIXME: need strategy to analyse if there are any users of cert we haven't // accounted for (manually added etc) otherwise we are disposing of a cert which // could still be in use /*if (!isPreviewOnly) * { * if (cleanupCertStore * && requestConfig.DeploymentSiteOption != DeploymentOption.DeploymentStoreOnly * && requestConfig.DeploymentSiteOption != DeploymentOption.NoDeployment * ) * { * //remove old certs for this primary domain * //FIXME: * CertificateManager.CleanupCertificateDuplicates(storedCert, requestConfig.PrimaryDomain); * } * }*/ } else { if (requestConfig.DeploymentSiteOption != DeploymentOption.NoDeployment) { actions.Add(new ActionStep { HasError = true, Title = "Certificate Storage", Description = "Certificate could not be stored." }); } } // deployment tasks completed return(actions); }
/// <summary> /// Convert legacy pre/post request scripts, webhooks and deployments to Pre/Post Deployment Tasks /// </summary> /// <param name="managedCertificate"></param> /// <returns></returns> public Tuple <ManagedCertificate, bool> MigrateDeploymentTasks(ManagedCertificate managedCertificate) { bool requiredMigration = false; if (managedCertificate.PreRequestTasks == null) { managedCertificate.PreRequestTasks = new System.Collections.ObjectModel.ObservableCollection <DeploymentTaskConfig>(); } if (managedCertificate.PostRequestTasks == null) { managedCertificate.PostRequestTasks = new System.Collections.ObjectModel.ObservableCollection <DeploymentTaskConfig>(); } if (!string.IsNullOrEmpty(managedCertificate.RequestConfig.PreRequestPowerShellScript)) { //add pre-request script task var task = new DeploymentTaskConfig { Id = Guid.NewGuid().ToString(), TaskTypeId = StandardTaskTypes.POWERSHELL, ChallengeProvider = StandardAuthTypes.STANDARD_AUTH_LOCAL, TaskName = "[Pre-Request Script]", IsFatalOnError = true, Parameters = new List <ProviderParameterSetting> { new ProviderParameterSetting("scriptpath", managedCertificate.RequestConfig.PreRequestPowerShellScript), new ProviderParameterSetting("inputresult", "true") } }; if (!managedCertificate.PreRequestTasks.Any(t => t.TaskName == "[Pre-Request Script]")) { managedCertificate.PreRequestTasks.Insert(0, task); requiredMigration = true; } managedCertificate.RequestConfig.PreRequestPowerShellScript = null; } if (!string.IsNullOrEmpty(managedCertificate.RequestConfig.PostRequestPowerShellScript)) { //add post-request script task var task = new DeploymentTaskConfig { Id = Guid.NewGuid().ToString(), TaskTypeId = StandardTaskTypes.POWERSHELL, ChallengeProvider = StandardAuthTypes.STANDARD_AUTH_LOCAL, TaskName = "[Post-Request Script]", IsFatalOnError = true, TaskTrigger = TaskTriggerType.ON_SUCCESS, Parameters = new List <ProviderParameterSetting> { new ProviderParameterSetting("scriptpath", managedCertificate.RequestConfig.PostRequestPowerShellScript), new ProviderParameterSetting("inputresult", "true") } }; if (!managedCertificate.PostRequestTasks.Any(t => t.TaskName == "[Post-Request Script]")) { managedCertificate.PostRequestTasks.Insert(0, task); requiredMigration = true; } managedCertificate.RequestConfig.PostRequestPowerShellScript = null; } if (!string.IsNullOrEmpty(managedCertificate.RequestConfig.WebhookUrl)) { //add post-request script task for webhook, migrate trigger type to task trigger type var triggerType = TaskTriggerType.ANY_STATUS; if (managedCertificate.RequestConfig.WebhookTrigger == Webhook.ON_NONE) { triggerType = TaskTriggerType.NOT_ENABLED; } else if (managedCertificate.RequestConfig.WebhookTrigger == Webhook.ON_SUCCESS) { triggerType = TaskTriggerType.ON_SUCCESS; } else if (managedCertificate.RequestConfig.WebhookTrigger == Webhook.ON_ERROR) { triggerType = TaskTriggerType.ON_ERROR; } else if (managedCertificate.RequestConfig.WebhookTrigger == Webhook.ON_SUCCESS_OR_ERROR) { triggerType = TaskTriggerType.ANY_STATUS; } var task = new DeploymentTaskConfig { Id = Guid.NewGuid().ToString(), ChallengeProvider = StandardAuthTypes.STANDARD_AUTH_LOCAL, TaskName = "[Post-Request Webhook]", IsFatalOnError = false, TaskTrigger = triggerType, TaskTypeId = StandardTaskTypes.WEBHOOK, Parameters = new List <ProviderParameterSetting> { new ProviderParameterSetting("url", managedCertificate.RequestConfig.WebhookUrl), new ProviderParameterSetting("method", managedCertificate.RequestConfig.WebhookMethod), new ProviderParameterSetting("contenttype", managedCertificate.RequestConfig.WebhookContentType), new ProviderParameterSetting("contentbody", managedCertificate.RequestConfig.WebhookContentBody) } }; if (!managedCertificate.PostRequestTasks.Any(t => t.TaskName == "[Post-Request Webhook]")) { managedCertificate.PostRequestTasks.Insert(0, task); requiredMigration = true; } managedCertificate.RequestConfig.WebhookUrl = null; managedCertificate.RequestConfig.WebhookTrigger = Webhook.ON_NONE; } // #516 check for any post-request webhooks incorrectly set to be powershell if (managedCertificate.PostRequestTasks?.Any(t => t.TaskTypeId == StandardTaskTypes.POWERSHELL && t.Parameters?.Any(p => p.Key == "url") == true) == true) { var webhookTask = managedCertificate.PostRequestTasks.First(t => t.TaskTypeId == StandardTaskTypes.POWERSHELL && t.Parameters?.Any(p => p.Key == "url") == true); if (webhookTask != null) { webhookTask.TaskTypeId = StandardTaskTypes.WEBHOOK; requiredMigration = true; } } return(new Tuple <ManagedCertificate, bool>(managedCertificate, requiredMigration)); }
public async Task <List <ActionStep> > GeneratePreview(ManagedCertificate item) => await new PreviewManager().GeneratePreview(item, _serverProvider, this);
public async Task TestChallengeRequestDNSWildcard() { var testStr = Guid.NewGuid().ToString().Substring(0, 6); PrimaryTestDomain = $"test-{testStr}." + PrimaryTestDomain; var wildcardDomain = "*.test." + PrimaryTestDomain; var testWildcardSiteName = "TestWildcard_" + testStr; if (await iisManager.SiteExists(testWildcardSiteName)) { await iisManager.DeleteSite(testWildcardSiteName); } var site = await iisManager.CreateSite(testWildcardSiteName, "test" + testStr + "." + PrimaryTestDomain, PrimaryIISRoot, "DefaultAppPool", port : testSiteHttpPort); ManagedCertificate managedCertificate = null; X509Certificate2 certInfo = null; try { managedCertificate = new ManagedCertificate { Id = Guid.NewGuid().ToString(), Name = testWildcardSiteName, GroupId = site.Id.ToString(), UseStagingMode = true, RequestConfig = new CertRequestConfig { PrimaryDomain = wildcardDomain, PerformAutoConfig = true, PerformAutomatedCertBinding = true, PerformChallengeFileCopy = true, PerformExtensionlessConfigChecks = true, WebsiteRootPath = testSitePath, Challenges = new ObservableCollection <CertRequestChallengeConfig> { new CertRequestChallengeConfig { ChallengeType = SupportedChallengeTypes.CHALLENGE_TYPE_DNS, ChallengeProvider = "DNS01.API.Cloudflare", ChallengeCredentialKey = _testCredStorageKey, ZoneId = ConfigSettings["Cloudflare_ZoneId"] } } }, ItemType = ManagedCertificateType.SSL_ACME }; var result = await certifyManager.PerformCertificateRequest(_log, managedCertificate); //ensure cert request was successful Assert.IsTrue(result.IsSuccess, "Certificate Request Not Completed"); //check details of cert, subject alternative name should include domain and expiry must be great than 89 days in the future var managedCertificates = await certifyManager.GetManagedCertificates(); managedCertificate = managedCertificates.FirstOrDefault(m => m.Id == managedCertificate.Id); //emsure we have a new managed site Assert.IsNotNull(managedCertificate); //have cert file details Assert.IsNotNull(managedCertificate.CertificatePath); var fileExists = System.IO.File.Exists(managedCertificate.CertificatePath); Assert.IsTrue(fileExists); //check cert is correct certInfo = CertificateManager.LoadCertificate(managedCertificate.CertificatePath); Assert.IsNotNull(certInfo); var isRecentlyCreated = Math.Abs((DateTime.UtcNow - certInfo.NotBefore).TotalDays) < 2; Assert.IsTrue(isRecentlyCreated); var expiresInFuture = (certInfo.NotAfter - DateTime.UtcNow).TotalDays >= 89; Assert.IsTrue(expiresInFuture); } finally { // remove IIS site await iisManager.DeleteSite(testWildcardSiteName); // remove managed site if (managedCertificate != null) { await certifyManager.DeleteManagedCertificate(managedCertificate.Id); } // cleanup certificate if (certInfo != null) { CertificateManager.RemoveCertificate(certInfo); } } }