using Autofac; using Newtonsoft.Json.Schema; using PKISharp.WACS.Clients.Acme; using PKISharp.WACS.Configuration; using PKISharp.WACS.Context; using PKISharp.WACS.DomainObjects; using PKISharp.WACS.Extensions; using PKISharp.WACS.Plugins.Base.Options; using PKISharp.WACS.Plugins.Interfaces; using PKISharp.WACS.Services; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace PKISharp.WACS { /// /// This part of the code handles the actual creation/renewal of ACME certificates /// internal class RenewalExecutor { private readonly MainArguments _args; private readonly IAutofacBuilder _scopeBuilder; private readonly ILifetimeScope _container; private readonly ILogService _log; private readonly IInputService _input; private readonly ExceptionHandler _exceptionHandler; private readonly RenewalValidator _validator; public RenewalExecutor( MainArguments args, IAutofacBuilder scopeBuilder, ILogService log, IInputService input, RenewalValidator validator, ExceptionHandler exceptionHandler, IContainer container) { _validator = validator; _args = args; _scopeBuilder = scopeBuilder; _log = log; _input = input; _exceptionHandler = exceptionHandler; _container = container; } /// /// Determine if the renewal should be executes /// /// /// /// public async Task HandleRenewal(Renewal renewal, RunLevel runLevel) { _input.CreateSpace(); _log.Reset(); using var ts = _scopeBuilder.Target(_container, renewal, runLevel); using var es = _scopeBuilder.Execution(ts, renewal, runLevel); // Generate the target var targetPlugin = es.Resolve(); var (disabled, disabledReason) = targetPlugin.Disabled; if (disabled) { return new RenewResult($"Target plugin is not available. {disabledReason}"); } var target = await targetPlugin.Generate(); if (target is INull) { return new RenewResult($"Target plugin did not generate a target"); } if (!target.IsValid(_log)) { return new RenewResult($"Target plugin generated an invalid target"); } // Check if our validation plugin is (still) up to the task var validationPlugin = es.Resolve(); if (!validationPlugin.CanValidate(target)) { return new RenewResult($"Validation plugin is unable to validate the target. A wildcard host was introduced into a HTTP validated renewal."); } // Create one or more orders based on the target var orderPlugin = es.Resolve(); var orders = orderPlugin.Split(renewal, target); if (orders == null || orders.Count() == 0) { return new RenewResult("Order plugin failed to create order(s)"); } _log.Verbose("Targeted convert into {n} order(s)", orders.Count()); // Check if renewal is needed if (!runLevel.HasFlag(RunLevel.ForceRenew) && !renewal.Updated) { _log.Verbose("Checking {renewal}", renewal.LastFriendlyName); if (!renewal.IsDue()) { var cs = es.Resolve(); var abort = true; foreach (var order in orders) { var cache = cs.CachedInfo(order); if (cache == null && !renewal.New) { _log.Information(LogType.All, "Renewal for {renewal} running prematurely due to detected target change", renewal.LastFriendlyName); abort = false; break; } } if (abort) { _log.Information("Renewal for {renewal} is due after {date}", renewal.LastFriendlyName, renewal.GetDueDate()); return new RenewResult() { Abort = true }; } } else if (!renewal.New) { _log.Information(LogType.All, "Renewing certificate for {renewal}", renewal.LastFriendlyName); } } else if (runLevel.HasFlag(RunLevel.ForceRenew)) { _log.Information(LogType.All, "Force renewing certificate for {renewal}", renewal.LastFriendlyName); } // If at this point we haven't retured already with an error/abort // actually execute the renewal var result = await ExecuteRenewal(es, orders.ToList(), runLevel); // Configure task scheduler if (result.Success && !result.Abort) { if ((renewal.New || renewal.Updated) && !_args.NoTaskScheduler) { if (runLevel.HasFlag(RunLevel.Test) && !await _input.PromptYesNo($"[--test] Do you want to automatically renew with these settings?", true)) { // Early out for test runs result.Abort = true; return result; } else { // Make sure the Task Scheduler is configured await es.Resolve().EnsureTaskScheduler(runLevel, false); } } } return result; } /// /// Run the renewal /// /// /// /// /// private async Task ExecuteRenewal(ILifetimeScope execute, List orders, RunLevel runLevel) { var result = new RenewResult(); foreach (var order in orders) { _log.Verbose("Handle order {n}/{m}: {friendly}", orders.IndexOf(order) + 1, orders.Count, order.FriendlyNamePart ?? "Main"); // Create the order details var orderManager = execute.Resolve(); order.Details = await orderManager.GetOrCreate(order, runLevel); // Create the execution context var context = new ExecutionContext(execute, order, runLevel, result); // Authorize the order (validation) await _validator.AuthorizeOrder(context, runLevel); if (context.Result.Success) { // Execute final steps (CSR, store, install) await ExecuteOrder(context); } } return result; } /// /// Steps to take on succesful (re)authorization /// /// private async Task ExecuteOrder(ExecutionContext context) { try { var certificateService = context.Scope.Resolve(); var csrPlugin = context.Target.CsrBytes == null ? context.Scope.Resolve() : null; if (csrPlugin != null) { var (disabled, disabledReason) = csrPlugin.Disabled; if (disabled) { context.Result.AddErrorMessage($"CSR plugin is not available. {disabledReason}"); return; } } var oldCertificate = certificateService.CachedInfo(context.Order); var newCertificate = await certificateService.RequestCertificate(csrPlugin, context.RunLevel, context.Order); // Test if a new certificate has been generated if (newCertificate == null) { context.Result.AddErrorMessage("No certificate generated"); return; } else { context.Result.AddThumbprint(newCertificate.Certificate.Thumbprint); } // Early escape for testing validation only if (context.Renewal.New && context.RunLevel.HasFlag(RunLevel.Test) && !await _input.PromptYesNo($"[--test] Do you want to install the certificate?", true)) { context.Result.Abort = true; return; } // Run store plugin(s) var storePluginOptions = new List(); var storePlugins = new List(); try { var steps = context.Renewal.StorePluginOptions.Count(); for (var i = 0; i < steps; i++) { var storeOptions = context.Renewal.StorePluginOptions[i]; var storePlugin = (IStorePlugin)context.Scope.Resolve(storeOptions.Instance, new TypedParameter(storeOptions.GetType(), storeOptions)); if (!(storePlugin is INull)) { if (steps > 1) { _log.Information("Store step {n}/{m}: {name}...", i + 1, steps, storeOptions.Name); } else { _log.Information("Store with {name}...", storeOptions.Name); } var (disabled, disabledReason) = storePlugin.Disabled; if (disabled) { context.Result.AddErrorMessage($"Store plugin is not available. {disabledReason}"); return; } await storePlugin.Save(newCertificate); storePlugins.Add(storePlugin); storePluginOptions.Add(storeOptions); } } } catch (Exception ex) { var reason = _exceptionHandler.HandleException(ex, "Unable to store certificate"); context.Result.AddErrorMessage($"Store failed: {reason}"); return; } // Run installation plugin(s) try { var steps = context.Renewal.InstallationPluginOptions.Count(); for (var i = 0; i < steps; i++) { var installOptions = context.Renewal.InstallationPluginOptions[i]; var installPlugin = (IInstallationPlugin)context.Scope.Resolve( installOptions.Instance, new TypedParameter(installOptions.GetType(), installOptions)); if (!(installPlugin is INull)) { if (steps > 1) { _log.Information("Installation step {n}/{m}: {name}...", i + 1, steps, installOptions.Name); } else { _log.Information("Installing with {name}...", installOptions.Name); } var (disabled, disabledReason) = installPlugin.Disabled; if (disabled) { context.Result.AddErrorMessage($"Installation plugin is not available. {disabledReason}"); return; } await installPlugin.Install(context.Target, storePlugins, newCertificate, oldCertificate); } } } catch (Exception ex) { var reason = _exceptionHandler.HandleException(ex, "Unable to install certificate"); context.Result.AddErrorMessage($"Install failed: {reason}"); return; } // Delete the old certificate if not forbidden, found and not re-used for (var i = 0; i < storePluginOptions.Count; i++) { if (!storePluginOptions[i].KeepExisting && oldCertificate != null && newCertificate.Certificate.Thumbprint != oldCertificate.Certificate.Thumbprint) { try { await storePlugins[i].Delete(oldCertificate); } catch (Exception ex) { _log.Error(ex, "Unable to delete previous certificate"); // not a show-stopper, consider the renewal a success context.Result.AddErrorMessage($"Delete failed: {ex.Message}", false); } } } } catch (Exception ex) { var message = _exceptionHandler.HandleException(ex); context.Result.AddErrorMessage(message); } } } }